When working with any technology, I want to take advantage of everything that makes that technology unique. SQL Server is a powerful technology, but features can hide behind abstractions created by developers.
In this blog post, I’ll tell you what table-valued parameters
are, how to use them in your calls to SQL Server, some caveats, and how to use them from your .NET Application.
What Is A User-Defined Table Type
SQL Server is a relational database, and in many instances, as a developer, we will be dealing with tables, columns, and rows. The table structure allows us to express the idea of “many” when outputting results. SQL Server enables us to create tables as containers for information, and these are known as user-defined table types
. Developers commonly use these types as input parameters to stored procedures and user-defined functions.
If you want to see if any of your current databases have any user-defined table types
, you will need to go to the following structure in your database.
Programmability > Types > User-Defined Table Types
Creating a User-Defined Table Type
Creating a user-defined table type
is as simple as creating a regular table.
CREATE TYPE FunTableType
AS TABLE
( ActivityName VARCHAR(50)
, Rating INT );
Here I defined a type of FunTableType
, which we will use in a simple stored procedure that echoes our rows back.
/* Create a procedure to receive data for the table-valued parameter. */
CREATE PROCEDURE dbo.sp_EchoFun
@TVP FunTableType READONLY
AS
SET NOCOUNT ON
select * from @TVP
Finally, let’s call our stored procedure.
/* Declare a variable that references the type. */
DECLARE @activities AS FunTableType;
/* Add data to the table variable. */
INSERT INTO @activities (ActivityName, Rating) Values ('swimming', 5)
/* Pass the table variable data to a stored procedure. */
EXEC sp_EchoFun @activities;
We are creating our user-defined table
in memory, inserting values into it, then calling our stored procedure. Putting it all together, you can see the results.
When Is This Useful
So when would you want to use a user-defined table
? In my personal experience, the following scenarios provide the most value.
- Input parameters into stored procedures that need to take in logical concepts as rows.
- Inserting multiple rows more efficiently without resorting to SqlBulkCopy.
There is considerable overhead to using this feature, so use it judiciously. I explain some caveats in the next section.
Table-Valued Caveats
Before you start using user-defined table types
, you should consider their strengths and weaknesses.
- User-defined tables cannot be used to define columns in other database tables.
- User-defined tables require permission to access and use them in stored procedures.
- As the name implies, the data needs to be tabular.
- Managing user-defined tables is like any other schema-based object.
- Depending on your implementation, building the parameter in your codebase can be cumbersome.
With SqlCommand and SqlParameter
If you are not a fan of frameworks or libraries, you can use user-defined tables
using ADO.NET.
await using var connection = new SqlConnection(connectionString);
await connection.OpenAsync();
var table = new DataTable();
table.Columns.Add("ActivityName", typeof(string));
table.Columns.Add("Rating", typeof(int));
var row = table.NewRow();
row["ActivityName"] = "swimming";
row["Rating"] = 1;
table.Rows.Add(row);
var command = connection.CreateCommand();
command.CommandText = "dbo.sp_EchoFun";
command.CommandType = CommandType.StoredProcedure;
var parameter = command.CreateParameter();
parameter.TypeName = "dbo.FunTableType";
parameter.Value = table;
parameter.ParameterName = "@TVP";
command.Parameters.Add(parameter);
var reader = await command.ExecuteReaderAsync();
var result = new DataTable();
result.Load(reader);
There are a couple of notable parts of this implementation:
- We need to use the
DataTable
type to construct our table in-memory. - We need to add the rows to the data table.
- The command is of type
CommandType.StoredProcedure
. This type isn’t necessary, but if used, make sure parameter names match your stored procedure definition. - The parameter
TypeName
is that of theuser-defined table type
in the database. In our case, it isdbo.FunTableType
.
For simplicity, I just reloaded the result set back into a DataTable. You can see the results here.
With Dapper
You can make your code more accessible to other developers by using a library like [Dapper][]. Here is an example of using table-valued parameters with the Micro-ORM Dapper.
public class Fun
{
public string ActivityName { get; set; }
public int Rating { get; set; }
}
async static Task Main(string[] args)
{
await using var connection = new SqlConnection(connectionString);
await connection.OpenAsync();
var table = new DataTable();
table.Columns.Add("ActivityName", typeof(string));
table.Columns.Add("Rating", typeof(int));
var row = table.NewRow();
row["ActivityName"] = "swimming";
row["Rating"] = 1;
table.Rows.Add(row);
var result = await connection.QueryAsync<Fun>("EXEC sp_EchoFun @activities", new
{
activities = table.AsTableValuedParameter("dbo.FunTableType")
});
}
Let’s look at the result.
Things to note in this example:
- We still need to make a
DataTable
. - We call
AsTableValuedParameter
to convert our table into a parameter. - We still need to tell Dapper what the database type is.
- Results are serialized directly into our C# object.
We could make this codebase even more friendly by creating a helper method that converts C# objects into DataTables.
listOfActivities.ToDataTable();
listOfActivities.ToTableValuedParameter("dbo.FunTableType");
Conclusion
Table-valued parameters
and user-defined table types
are a powerful combination. As a .NET developer, it can help you solve some complex problems while still being performance-minded. I hope you found this blog post enlightening and feel free to leave any comments.
Some referenced material: