动态拼接表名必须用QUOTENAME防注入,年月变量用CHAR(6)固定长度,建表前须查sys.tables确认不存在,EXEC建表后表仅在动态批处理内可见,应将DDL+DML封装于同一EXEC中。

动态拼接表名时,必须用 QUOTENAME 防注入
直接拼字符串建表(比如 'CREATE TABLE tbl_' + @year_month + ' (...))等于把 SQL 注入大门敞开。哪怕参数来自内部系统,只要未来有扩展可能(比如加个前端配置入口),就立刻变成高危点。
SQL Server 里必须用 QUOTENAME 包裹动态部分,它会自动加方括号并转义特殊字符:
DECLARE @table_name NVARCHAR(128) = QUOTENAME('tbl_202405');
常见错误现象:Invalid object name 'tbl_2024-05' 或建出带破折号的非法标识符——因为没处理分隔符或空格。
-
QUOTENAME默认用[]包裹,适合普通表名;若需双引号(如兼容 PostgreSQL 风格),传第二个参数'"' - 年月变量建议用
CHAR(6)或VARCHAR(6)固定长度,避免末尾空格影响拼接 - 别用
CONVERT(VARCHAR, GETDATE(), 112)直接截取——它返回 8 位(yyyymmdd),要年月得LEFT(CONVERT(VARCHAR, GETDATE(), 112), 6)
EXEC(@sql) 前必须先检查表是否已存在
重复执行存储过程建同名表会直接报错 There is already an object named '...' in the database,中断后续逻辑。
不能靠 try-catch 掩盖问题,得在拼完 @sql 后、执行前主动查元数据:
IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE name = @table_name_no_bracket)
注意:@table_name_no_bracket 是去掉方括号的原始名(如 'tbl_202405'),因为 sys.tables.name 存的是裸名。
- 别用
OBJECT_ID(@table_name)判断——它接受带括号的格式,但容易因数据库上下文误判 - 如果业务允许覆盖,可加
DROP TABLE IF EXISTS,但生产环境慎用,尤其有历史数据依赖时 - 建表语句里字段定义尽量固化,避免把列名也动态化——那会极大增加校验和维护成本
分区场景下,优先考虑原生分区表而非动态建表
如果目标是按月隔离数据(如日志、订单),硬生生每月建一张物理表,后期会遇到明显瓶颈:跨月查询要 UNION ALL 手动拼 N 张表,维护索引、统计信息、备份策略全得脚本化。
SQL Server 2016+ 原生分区表更稳:
- 用
$PARTITION函数路由数据,查询仍写单表名,优化器自动裁剪 - 每月只需
SPLIT RANGE新增边界,比建新表+改应用逻辑轻量得多 - 归档旧月数据用
SWITCH PARTITION秒级转移,不用COPY + DELETE
只有当各月结构差异大(比如字段增减频繁)、或权限需彻底隔离(不同部门查不同月表)时,才值得选动态建表。
存储过程中 EXEC 的作用域限制很关键
在存储过程里用 EXEC(@sql) 建的表,默认只在该动态批处理内可见。如果后续语句想往这张表插数据,会报 Invalid object name。
根本原因是:动态 SQL 在独立作用域运行,建的表属于“临时作用域”,结束后即不可见。
- 解决方案只有两个:把所有操作(建表 + 插入 + 索引)全塞进同一个
EXEC字符串里 - 或者改用全局临时表(
##temp),但注意并发冲突和清理时机 - 绝对不要指望
EXEC后直接写INSERT INTO @table_name——语法都通不过,变量名不支持动态解析
这地方卡住的人最多:以为建完就能用,结果第二句就崩。得把整套 DDL+DML 当成一个原子块来组织。










