SQL Server 存储过程声明表值参数需先创建用户定义表类型(UDT),再以 @param_name READONLY 方式声明;必须严格匹配字段名、类型及NULL性,且仅支持INSERT INTO ... SELECT FROM @tvp语法,不支持直接VALUES插入;超10万行性能显著下降,C#传DataTable时列名、类型、顺序须与UDT完全一致。

SQL Server 存储过程怎么声明表值参数?
必须先创建用户定义表类型(UDT),再在存储过程中用 @param_name 声明为 READONLY 参数。不能直接把数组或 JSON 当参数传——SQL Server 不认。
- 创建表类型时字段名、类型、NULL 性必须和目标表严格一致,否则插入会失败
-
READONLY是强制要求,你不能在存储过程里对表值参数做UPDATE或DELETE - 表类型必须在数据库级创建,不能在存储过程里动态建;跨库调用需用三段式名引用(
db.schema.type)
INSERT INTO ... SELECT FROM @tvp 为什么报错?
常见错误是漏了 FROM 子句或用了不支持的语法,比如写成 INSERT INTO t VALUES (@tvp.col) —— 这种写法根本不存在。
- 正确写法只有
INSERT INTO target_table SELECT col1, col2 FROM @tvp - 如果目标表有自增列,而表值参数没提供该列,得显式关掉标识插入:
SET IDENTITY_INSERT target_table ON - 字段顺序不匹配会导致值插错列,建议始终显式列出目标字段名,别依赖
*
表值参数传入 50 万行数据会卡住吗?
不会卡住,但性能拐点在 10 万行左右。超过这个量级,客户端序列化/网络传输/服务器内存分配开销明显上升,比 BULK INSERT 慢一个数量级。
- 实测:10 万行以内,TVP 和
BULK INSERT耗时差距小于 15%;50 万行时,TVP 通常慢 3–4 倍 - 服务端内存占用随行数线性增长,
max server memory不足时会触发OutOfMemoryException - 不要在 TVP 里塞 BLOB 字段(如
VARBINARY(MAX)),序列化体积爆炸,极易超max_packet_size
从 C# 传 DataTable 到 TVP 需要什么前置条件?
DataTable 的列名、类型、顺序必须和 UDT 完全一致,且 SqlDbType.Structured 必须显式指定,否则抛 InvalidCastException。
- DataTable 列名大小写敏感,UDT 里叫
UserID,DataTable 里写userid就失败 - 数值类型要对齐:UDT 是
INT,DataTable 列就不能是long或short - 空值处理:DataTable 中
null对应 UDT 的NULL;但DBNull.Value才被识别,C# 的null(引用类型)会被当 0 或空字符串
真正容易被忽略的是:TVP 的元数据校验发生在 RPC 层,不是执行时。哪怕只有一行数据字段类型错,整个批次都会在参数绑定阶段失败,错误信息却常显示为“无法将参数转换为表类型”——看不出到底是哪列不对。调试时建议先用小数据集(3 行)验证结构,再放大。


















