User Defined Table Type in SQL Server

UDTTs are one of the most underutilized yet powerful features in the T-SQL. They allow you to pass entire tables of data into stored procedures and functions as a single parameter. In this comprehensive guide, I will take you through the architecture, benefits, and best practices of User Defined Table Types. We will move beyond the basics and look at why this feature is critical for modern, scalable application design.

User Defined Table Type in SQL Server

What is a User Defined Table Type (UDTT)?

At its core, a User Defined Table Type is a mechanism that allows you to define a table structure (schema) as a reusable object in your database. Think of it as a template or a “blueprint” for a table.

Once you create this type, it sits in your database schema just like a regular table or a stored procedure. However, it doesn’t hold data permanently. Instead, it defines the shape of the data that you intend to pass around.

The Technical Definition

In SQL Server, a UDTT is a strong-typed structure. When you declare a variable of this type, SQL Server creates a Table Variable. The magic happens when you use this type as a parameter for a stored procedure or function. This specific usage is known as a Table-Valued Parameter (TVP).

The Strategic Benefits

Why should you stop using XML or JSON parsing for bulk inserts and switch to UDTTs? The answer lies in three key areas: Performance, Type Safety, and Maintainability.

1. Superior Performance

When you pass a UDTT to a stored procedure, SQL Server treats it effectively like a table variable.

  • Reduced Round Trips: Instead of firing 1,000 separate INSERT statements from your application to the server, you send one batch. This drastically reduces network latency, which is critical if your app server is in an East Coast data center and your DB is in West US.
  • Set-Based Operations: Once the data is inside the procedure, you can join to it just like a regular table. This allows the SQL Optimizer to use set-based logic rather than iterative cursors.

2. Strong Type Safety

If you pass a CSV string like '101, Apple, $5.00 | 102, Banana, $2.00', SQL Server treats it as a generic blob of text. If “Banana” is misspelled or the price is alphanumeric, you won’t know until your parsing logic crashes at runtime.

With a UDTT, the structure is enforced at the definition level. If your type requires an INT for the ID and a MONEY type for the price, the database will reject invalid data before it even tries to process the logic.

3. Cleaner Code Maintenance

From a readability standpoint, UDTTs are superior. Your stored procedures stop looking like string-manipulation scripts and start looking like actual database logic.

The Syntax: How to Define a UDTT

Creating a User Defined Table Type is straightforward. It closely mimics the syntax of creating a standard table. You define the columns, data types, and even constraints (like Primary Keys or Unique constraints).

Here is the general structure of the syntax:

SQL

CREATE TYPE [SchemaName].[TypeName] AS TABLE
(
    [Column1] [DataType] [Constraints],
    [Column2] [DataType] [Constraints],
    ...
)

Key Components

  • Schema: It is best practice to assign a schema (e.g., dbo or Sales).
  • AS TABLE: This keyword explicitly tells SQL Server that this object is a table definition.
  • Constraints: You can—and should—add Primary Keys to your UDTTs. This helps the Query Optimizer create better execution plans when you join this data later.

Implementing UDTTs in Stored Procedures

Once your type is defined, the real power comes from passing it into a Stored Procedure. This is where the concept of Table-Valued Parameters (TVPs) comes into play.

There is one critical rule you must remember here, and I see developers forget it constantly: The READONLY keyword.

When you define a parameter in a stored procedure using a UDTT, you must mark it as READONLY. SQL Server does not allow you to modify the data inside the TVP within the body of the stored procedure. You cannot perform UPDATE or DELETE operations on the parameter itself.

Why READONLY?

This is a design choice by Microsoft to simplify the transaction management and memory overhead. If you need to modify the data passed in, you simply insert it into a local temp table or a standard table variable first.

A Typical Workflow

  1. Define the Type: Create dbo.CustomerListType.
  2. Create the Procedure: Create dbo.usp_ImportCustomers which accepts @NewCustomers dbo.CustomerListType READONLY.
  3. The Logic: Inside the procedure, write a statement like:SQLINSERT INTO dbo.Customers (Name, Region) SELECT Name, Region FROM @NewCustomers;

Behind the Scenes: Architecture and Memory

To truly master SQL Server, you need to understand what happens under the hood. When you declare a variable of your User Defined Table Type, where does that data live?

The Role of TempDB

Just like standard Table Variables, instances of UDTTs are materialized in TempDB. This is a crucial performance consideration.

  • If you are passing 5 rows, it’s negligible.
  • If you are passing 500,000 rows, you are putting significant write pressure on TempDB.

Comparison: UDTT vs. Other Method

To help you decide when to use a UDTT versus other data-passing techniques, I have compiled this comparison table.

FeatureUser Defined Table Type (TVP)Temp Table (#Table)Standard Table VariableXML / JSON Parsing
ScopePassed as ParameterLocal to Session/ProcLocal to BatchLocal Variable
I/O OverheadLow (Single Round Trip)MediumMediumHigh (CPU Intensive)
StatisticsNo (assumes 1 row)*Yes (Full Stats)No (assumes 1 row)*N/A
IndexingInline only (PK/Unique)Full Indexing SupportInline onlyN/A
Code ComplexityLow (Clean SQL)MediumMediumHigh (Parsing Logic)
Data TypesStrong TypingStrong TypingStrong TypingWeak (Text based)

Best Practices

Here are the rules regarding UDTTs.

1. Naming Conventions

In the USA, standard naming conventions usually follow PascalCase.

  • Bad: tbltype1, array_int
  • Good: IntListType, OrderDetailsType, GuidListType

I recommend appending “Type” to the name so it is instantly recognizable in the Object Explorer.

2. Define Primary Keys

Always define a Primary Key on your UDTT if the data allows it (e.g., a list of unique Customer IDs). This automatically creates a clustered index on the table variable instance, which speeds up joins significantly.

3. Handle NULLs Explicitly

Be very clear about nullability in your type definition. If a column should never be null (like an ID), mark it NOT NULL. This prevents bad data from entering your stored procedure silently.

4. Security Permissions

Remember that a user needs permission not just on the stored procedure, but also EXECUTE permission on the User Defined Table Type itself. This is a common “gotcha” when deploying to Production. If you grant EXEC on the proc but forget the type, the application will throw a permission error.

Limitations and Constraints

No technology is perfect, and UDTTs have their boundaries.

1. Schema Modifications

This is the biggest headache. Once a UDTT is created and referenced by a stored procedure, you cannot allow schema changes to it easily. You cannot simply ALTER TYPE.

To add a column to an existing UDTT, you typically have to:

  1. Drop all stored procedures referencing the type.
  2. Drop the type.
  3. Re-create the type with the new column.
  4. Re-create all the stored procedures. This dependency chain makes UDTTs rigid. If your requirements change frequently, this might be a bottleneck.

2. Indexing Limitations

You can only define indexes inline during the type creation (as constraints). You cannot create non-clustered indexes on a TVP variable after it has been declared inside the procedure.

3. The READONLY Restriction

As mentioned earlier, you cannot scrub or sanitize data inside the TVP. If you need to trim whitespace or validate logic, you must copy the data to a new temp table or table variable, which doubles your memory footprint for that batch.

Conclusion

User Defined Table Types are a milestone feature in SQL Server history. They bridged the gap between the application code and the database engine, allowing for clean, high-performance bulk data operations.

While they come with rigid schema dependencies and quirky READONLY limitations, the benefits of type safety and reduced network round-trips make them indispensable.

Mastering the UDTT is not just a “nice-to-have” skill—it is an architectural necessity. Start reviewing your stored procedures today, and give your database the structure it deserves.

You may also like the following articles: