必须先在SQL Server中创建用户定义表类型,再在存储过程中声明READONLY的TVP参数,否则报错;CREATE TYPE需显式指定schema,字段名、顺序、类型、NULL性须与客户端严格一致;TVP参数声明格式为@TVP typename READONLY,不可修改;调用时客户端需按驱动要求正确绑定,大小写敏感。

必须先在 SQL Server 中创建用户定义表类型,再在存储过程中声明 READONLY 的 TVP 参数,否则会报错“必须声明表值参数”或“参数类型无效”。
如何创建表类型(CREATE TYPE)
表值参数依赖数据库端的用户定义表类型,不是 Go/Java/C# 侧能直接定义的。它必须用 T-SQL 显式创建,且结构需与客户端传入数据严格一致:
- 字段名、顺序、数据类型、NULL 性必须完全匹配(例如
LocationName VARCHAR(50)不能写成location_name NVARCHAR(50)) - 不支持主键、索引等约束定义(TVP 本身是
READONLY,这些无意义) - 建议使用
SCHEMA::dbo显式指定 schema,避免权限或解析歧义
示例语句:
CREATE TYPE dbo.LocationTableType AS TABLE (
LocationName VARCHAR(50),
CostRate INT
);
存储过程中如何正确声明和使用 TVP 参数
TVP 必须作为 READONLY 输入参数声明,且不能在过程体内做 INSERT/UPDATE/DELETE 操作 —— 这是硬性限制,违反会直接报错 "Cannot modify table variable 'xxx' because it is declared READONLY":
- 参数声明格式固定:
@TVP dbo.LocationTableType READONLY,缺一不可 - 不能用
SELECT INTO或INSERT EXEC直接写入 TVP(但可从 TVPSELECT出来插入其他表) - 推荐用
MERGE或INSERT ... SELECT一次性落地,避免循环逐行处理
正确用法示例:
CREATE PROCEDURE dbo.usp_InsertProductionLocation
@TVP dbo.LocationTableType READONLY
AS
BEGIN
SET NOCOUNT ON;
INSERT INTO Location (Name, CostRate, Availability, ModifiedDate)
SELECT LocationName, CostRate, 0, GETDATE()
FROM @TVP;
END
调用时传参失败的常见原因
即使表类型和存储过程都正确,调用仍可能失败,多数源于客户端驱动或参数绑定细节:
- go-mssqldb:必须用
mssql.TVP{ TypeName: "dbo.LocationTableType", Value: data },TypeName要带 schema,且Value必须是切片(不是单个 struct) - JDBC:需用
SQLServerDataTable构造数据,并通过setStructured绑定;驱动版本必须 ≥ 6.0 - ODBC(Linux/macOS):
SQLSetDescFieldW必须传 UTF-16 字符串,用SQLSetDescFieldA会静默失败 - 所有场景:TVP 名称大小写需与数据库中定义完全一致(SQL Server 默认大小写敏感,尤其启用了区分大小写排序规则时)
为什么不能在 TVP 上做 UPDATE 或返回 OUTPUT?
这是 SQL Server 引擎层的设计限制,不是驱动或语法问题。TVP 在内部被当作只读表变量处理,所有 DML 写操作都会被拒绝:
-
UPDATE @TVP SET ...→ 报错"Must declare the table variable '@TVP'"(实际是权限检查拦截) -
DELETE FROM @TVP→ 同样报错,且无法绕过 -
OUTPUT INSERTED.*不能用于 TVP,因为 TVP 不产生变更行集
如果业务需要“先校验再修改”,只能把 TVP 数据先 INSERT 到临时表或表变量,再对那个对象操作 —— 这一步不能省略。











