一个项目如果写的是“sql测试脚本-未完成”,十有八九不是代码写不出来,而是写到了一半发现需求比想象中大。我手头就有一份这样的脚本,本来只是想着快速校验几个上线前的SQL文件,结果越做越往里钻,最后变成一个兼顾语法检查、注入风险扫描、回归对比的测试框架。虽然仓库名上还挂着“未完成”,但已经跑通的模块在项目里顶了不少事。这篇就把它拆开聊聊:做了什么、为什么这么做、哪些地方还没做完,以及中途踩进去又能爬出来的坑。
这个项目适合谁参考?正在给团队做自动化测试,尤其是数据库变更脚本、接口联调SQL、批量数据修复SQL的验证场景的人,应该能从我这里找到几条能直接抄作业的思路。哪怕你只是一个人维护几个库,里面关于如何设计“可重复执行、能报出有效告警”的SQL测试脚本的方法,也能帮你省掉几顿加班。
1. 项目背景与整体设计思路
1.1 这个脚本到底要解决什么问题
先说场景。我这边经常要接收开发提交上来的SQL变更文件,有的是建表、加索引,有的是数据订正,还有一部分是复杂报表查询。原来上线前靠人肉过一遍:把SQL贴到SSMS里执行一下,感觉没问题就发上线单。但实际坑过几次之后就明白了,人肉检查有三个靠不住的地方。
第一,语法正确不代表业务正确。一条SQL能在查询编辑器里跑出结果,可不代表它会命中你想操作的数据范围。比如一个UPDATE语句漏了WHERE条件,执行前和环境正常,一执行就是全表覆盖。第二,同一套SQL在不同环境的表现不一样,开发库能跑,测试库可能因为权限、隔离级别、统计数据差异直接报错或者性能差十几倍。第三,注入风险这类问题靠人眼看,容易漏。尤其当脚本是动态拼出来的时候,肉眼很难追完每一处字符串拼接。
所以这个脚本的目标就三个:
- 快速对一批SQL文件做静态检查,识别高风险写法。
- 能连上目标数据库做动态校验,至少保证语法和权限没问题。
- 输出一个统一格式的检查报告,能接进发布流程,也能丢给开发自己看。
这三个目标听起来不大,但做下去之后涉及到的模块一个都不少,包括文件遍历、SQL解析、数据库连接、执行策略、日志记录和报告渲染。
1.2 为什么选脚本化而非纯手工验证
很多人觉得,就几份SQL文件,打开SSMS跑一遍不就行了吗,搞什么自动化脚本,杀鸡用牛刀。但真到发布窗口那几分钟,你就知道手工执行的问题了:你今天记得开事务,明天可能就忘;你把生产库的UPDATE执行完才想起来没看影响行数;你为了省事,把所有脚本一次性全选执行,结果第二个语句挂了,前面的事务状态一头雾水。
脚本化能解决的是“流程一致性”。同样的检查步骤,每次跑出来都是固定结果格式;同样的规则,对每个文件都生效;同样的连接参数,不用反复在GUI里配。更关键的是,脚本可以留痕,跑完留下日志和报告,以后审计或复盘的时候能精确知道当时执行了什么、结果如何。
从投入产出比看,第一次脚本化的成本确实不低,但只要SQL提交频率稍微高一点,第二周就能回本。而且自动化脚本还能在天没亮的时候自己跑,把结果发到群里,人只需要处理异常,不用守着窗口期。这才是测试脚本最大的价值。
1.3 整体架构与模块划分
整个项目我最初设计成了五个模块,分工很清楚:
| 模块 | 职责 | 状态 |
|---|---|---|
| 文件采集器 | 扫描指定目录下的SQL文件,支持按正则过滤 | 已完成 |
| 静态分析器 | 不连数据库,检查危险关键字和注入特征 | 已完成 |
| 动态执行器 | 连接目标库,按配置执行SQL或获取执行计划 | 已完成 |
| 报告生成器 | 输出Markdown/HTML格式的检查报告 | 大部分完成 |
| 定时调度器 | 接入Jenkins/计划任务,定时触发 | 未完成 |
这个模块划分最主要的原因是为了隔离风险。静态分析不需要连库,普通开发自己就能跑;动态执行必须连库,所以要有独立的权限配置和执行控制;报告生成单独拆开,因为同一个检查结果可能需要输出多种格式,拆开之后后续扩展方便。文件采集器是最先写的,因为不管是静态还是动态检查,第一步都是拿到“要测什么”。
实际开发中,代价最小的模块是报告生成器,但价值却很明显。一开始我老盯着分析逻辑,后来发现团队里其他人最关心的是报告能不能一眼看出问题。后来我把报告输出排在优先级前面,效果立竿见影。
2. 核心细节解析与实操要点
2.1 静态分析模块:危险语句与注入特征扫描
静态分析器的目标是不碰数据库,先把文件里明显有问题的写法抓出来。这个模块不追求穷尽所有问题,因为SQL语义复杂,纯静态很难做全面,但几个高频风险点一定要能识别。
我实现了三类检测逻辑。
第一类是危险语句检测。重点抓无WHERE条件的UPDATE/DELETE、TRUNCATE TABLE、DROP TABLE、DBCC命令这类高影响操作。实现思路很简单:逐行扫描,遇到关键字就记录位置,然后看同一语句范围内是否有WHERE子句。判断方法没有用那种重型SQL解析器,而是靠分号切分语句,再对每段做关键字计数,能覆盖绝大多数场景。如果你要更严谨,可以考虑引入jsqlparser这个Java库或者sqlparse这个Python库,能把AST解析出来再判断,准确率更高,但成本也更大。
第二类是注入特征检测。这里不是要做渗透工具,而是帮助测试人员快速发现“可能被外部输入污染”的写法。我维护了一个特征正则库,匹配常见的注入模式:' OR '1'='1、' OR 1=1--、拼接双引号的字符串常量、UNION SELECT出现在动态SQL里、EXEC配合字符串变量、sp_executesql的第一个参数不是常量等。每命中一条,就会标记风险等级并给出建议。这里要特别说明,正则命中不等于一定存在注入漏洞,比如业务里确有合法查询包含“OR 1=1”这种参数,所以报告里会区分为“高风险”、“需人工确认”、“提示”三个级别,避免误报淹没有效告警。
第三类是格式与可维护性检查。这个属于锦上添花,但实际使用率很高。检查项包括:是否存在SELECT *(在正式变更里很多时候是不允许的)、是否有超长行、是否混用了Tab和空格、关键SQL是否包含注释。这些都是经验积累,因为上线后出问题最多的往往不是语法,而是可读性差导致评审没看出问题。
动态SQL扫描的伪代码大概长这样:
import re RISK_PATTERNS = [ (re.compile(r"EXEC\s*\(.*\+", re.I), "动态SQL字符串拼接"), (re.compile(r"'OR\s+'1'\s*=\s*'1", re.I), "可能的认证绕过注入特征"), (re.compile(r"UNION\s+SELECT", re.I), "UNION SELECT出现在非预期位置"), (re.compile(r"--\s*$", re.M), "行尾注释包裹后续条件"), ] def scan_injection(filetext: str): findings = [] for lineno, line in enumerate(filetext.splitlines(), 1): for pattern, desc in RISK_PATTERNS: if pattern.search(line): findings.append({"line": lineno, "detail": desc, "level": "review"}) return findings第一版我塞了好几个正则进去,结果把团队逼疯了,因为合法SQL也疯狂告警。后来我总结了实操心得:注入扫描的正则宁可少而精,也不要贪多。真正有效的不是靠正则穷举,而是靠“参数化检查”,也就是看拼SQL的时候是否强制做了类型转换和参数绑定,这个后面动态执行部分会一起做。
2.2 文件遍历与执行顺序控制
静态扫描文件的时候,顺序无所谓;一旦进入动态执行阶段,文件顺序就变得重要。比如一个目录里有10个SQL文件,其中3号文件建了一张临时表,5号文件要查询这张表,如果按照文件名排序依次执行,顺序没问题;但如果是通过通配符随机执行,或者并发执行,大概率会失败。
所以文件采集器不只是扫描文件,还要支持一个“执行顺序控制”的配置。我定义了一个简单的清单规则:
[order] 001_create_table.sql = 1 002_insert_data.sql = 2 003_create_index.sql = 3 004_query_report.sql = 4未在清单里的文件按文件名升序排列。这样既灵活,又能满足大多数场景。另一种可行的方案是读取文件的头部注释,约定好-- DEPENDS ON: xxx.sql这样的格式,自动构建执行拓扑,但投入比较大,我目前没有实现,只在文档里留了设计。
采集器还有一个容易被忽略的点:编码识别。Windows环境下来的SQL文件经常是GBK或GB2312编码,Python按UTF-8读取直接乱码。我研究了一套很土的方案:先用chardet检测编码,检测失败就尝试常见编码逐个decode,最后兜底用errors="ignore"。这个细节不写进代码里没人会记得,但第一次跑崩之后你就记住了。
def read_sql_file(path): raw = path.read_bytes() for enc in ["utf-8", "gbk", "gb2312", "latin-1"]: try: return raw.decode(enc) except UnicodeDecodeError: continue return raw.decode("utf-8", errors="ignore")这套读取逻辑单独写成了工具函数,因为后续的静态分析、动态执行、报告生成都要读文件,统一入口能少很多麻烦。
2.3 动态执行器:连接数据库与执行策略
动态执行器是这个脚本里最容易被低估的部分。很多人以为连上库、然后把SQL字符串丢给数据库执行就行了。实际操作起来,要命的问题一堆。
连接配置我单独做了一个config.yml,里面区分了dev、test、prod三种环境。每种环境包括host、port、database、authentication,但权限级别不同:测试库可以执行写操作,生产库默认只允许只读校验。为了防止误操作,我在代码里加了环境确认步骤,如果目标环境是prod且包含DROP/TRUNCATE语法,脚本会直接拒绝执行并给出警告。
执行策略方面,我分成了三种模式:
- dry_run模式:只做语法解析和执行计划分析,不真正执行DML。
- transaction模式:把多个SQL包在事务里,出错回滚。
- batch模式:按文件顺序逐条执行,每条记录耗时和影响行数,遇到错误跳出并保存断点。
事务模式是我用得最多的。上线前把整个变更脚本放进一个事务里执行,如果中间任何一条出问题直接回滚,环境不会被弄脏。批量模式适合不需要回滚的场景,比如同步历史数据。这里有一个重要的实现细节:如果你用Python的pyodbc连接SQL Server,事务控制不能直接使用conn.autocommit = False就把所有语句包住。SQL Server的某些DDL语句,比如CREATE INDEX,可能隐式提交事务。所以最稳妥的方式是把事务模式下的所有语句拼成一个批次,利用SQL Server自身的BEGIN TRANSACTION ... COMMIT包裹,而不是依赖驱动层事务。
伪代码示例:
import pyodbc def execute_with_transaction(cursor, statements): cursor.execute("SET XACT_ABORT ON") cursor.execute("BEGIN TRANSACTION") try: for stmt in statements: cursor.execute(stmt) cursor.execute("COMMIT") return {"status": "success"} except Exception as e: cursor.execute("ROLLBACK") return {"status": "failed", "error": str(e)}为什么要SET XACT_ABORT ON?因为默认情况下,某些错误并不算严重错误,SQL Server只回滚当前语句,事务还会继续执行,这会导致部分提交。XACT_ABORT ON能保证任何运行时错误直接回滚整个事务,减少脏数据风险。
动态执行器里还要考虑SQL超时。默认情况下,pyodbc的超时时间可能是0,表示永不超时。如果测试环境有个跑不完的查询,调度任务会卡死。我把超时参数统一设置成30秒,防止单条语句把整个巡检拖死。同时用cursor.description获取结果集结构,只记录结果集的行数和前几行数据摘要,避免大查询结果把内存打爆。
3. 实操过程与核心环节实现
3.1 搭建最小可用的执行框架
整个脚本我选了Python,核心原因有三个:正则处理方便、pyodbc连SQL Server成熟、后续接报告模板生态丰富。如果你对别的主语言更熟,比如Java+JDBC、Go+database/sql,也完全可以,这套设计思想是语言无关的。
最小可用框架包括4个文件:
sql_test_runner/ ├── config.yml ├── runner.py ├── static_scan.py └── report.pyrunner.py是入口,负责读取配置、加载SQL文件列表、调用静态分析、进行动态执行、最后生成报告。static_scan.py里放静态分析函数,report.py负责生成报告。
一个完整的执行调用链路:
def main(): config = load_config("config.yml") files = collect_sql_files(config["input_dir"]) static_results = [] for f in files: static_results.append(scan_file(f)) dynamic_results = [] if config["execute"]["enabled"]: conn = create_connection(config["target_env"]) dynamic_results = execute_sql_files(conn, files, mode=config["execute"]["mode"]) conn.close() report_text = render_report(static_results, dynamic_results, config) write_report(report_text, config["output_path"])这个框架第一版跑通只需要3个小时,后面的复杂逻辑都是在这个骨架上长出来的。我的心得是,先把主链路跑通,哪怕结果只是打印在控制台,也比一开始就追求完美报告要强。
3.2 如何把检测规则写进可维护的配置
检测规则最容易变成一堆散落的正则和if-else,维护起来非常痛苦。后来我把规则抽成JSON配置文件,每次加规则不用动代码,只动配置。
{ "sql_injection_patterns": [ { "name": "auth_bypass_or", "regex": "'\\sOR\\s+'1'\\s*=\\s*'1", "level": "high", "description": "检测到OR条件恒真,疑似绕过登录认证" }, { "name": "union_select", "regex": "UNION\\s+SELECT", "level": "high", "description": "检测到UNION SELECT,可能用于数据越权" }, { "name": "comment_bypass", "regex": "--", "level": "low", "description": "行注释可能被注释掉后续安全检查" } ], "dangerous_statements": { "TRUNCATE TABLE": "high", "DROP TABLE": "high", "DROP DATABASE": "critical", "DBCC SHRINKFILE": "medium" } }这个做法有多好用?团队里后来有安全工程师加入,他只需要新增一个JSON片段,就能把新的注入模式纳入测试范围。只要保证每次规则变更前跑一遍历史样本,看看有没有误报,就能稳定维护。
规则命中之后,报告格式我会写成这样:
| 规则名 | 文件 | 行号 | 风险级别 | 说明 |
|---|---|---|---|---|
| auth_bypass_or | auth_check.sql | 12 | high | 检测到OR条件恒真,疑似绕过登录认证 |
| union_select | report.sql | 45 | high | 检测到UNION SELECT,可能用于数据越权 |
这个表格在报告里非常醒目,开发人员一看到行号和级别,马上就能定位。
3.3 用执行计划代替真实执行
有一类校验是“这条SQL到底能不能走索引”。单纯执行一遍,如果数据量小,根本看不出问题;等到了生产大数据量才爆发。所以我在动态执行器里加了一个模式:不真正跑完整个查询,而是获取预估执行计划。
在SQL Server上,获取预估执行计划的命令是:
SET SHOWPLAN_ALL ON; GO SELECT * FROM your_table WHERE column_a = 'x'; GO SET SHOWPLAN_ALL OFF;如果你用pyodbc执行这段,返回的第一个结果集就是执行计划明细,里面包含StmtText字段。解析StmtText可以判断有没有出现Table Scan、Index Scan、RID Lookup这类低效操作。
实操代码:
def get_query_plan(cursor, sql): cursor.execute("SET SHOWPLAN_ALL ON") rows = cursor.execute(sql).fetchall() cursor.execute("SET SHOWPLAN_ALL OFF") for row in rows: text = row[0] # StmtText列 if "Table Scan" in text or "RID Lookup" in text: return {"has_scan": True, "plan": text[:500]} return {"has_scan": False, "plan": ""}有一说一,这个模块判断逻辑比较粗糙,它只看有没有“Table Scan”关键字,覆盖率有限。更专业的方案是查询DMV,比如sys.dm_exec_query_stats里看执行计划,或者接入SentinelOne那样做全量采集,但这些都属于锦上添花。小团队能在一开始就有这个意识,已经很好了。
执行计划校验最大的价值在于让开发在提交时就自查,而不是等DBA上线前review才返工。我把这个功能做进了CI,提交SQL变更时自动触发,如果预估计划里有全表扫描,CI直接给出警告。这个改动上线之后,测试环境慢查询的数量肉眼可见下降。
3.4 把报告做成团队看得懂的样子
报告输出必须人性化。最开始我输出的是一堆纯文本日志,开发反馈“看不懂、不想看”。后来我把报告改成Markdown格式,每条问题都有文件、行号、级别、建议。再后来做成HTML,因为可以在浏览器里点开,而且可以直接发邮件。
我的HTML报告结构:
<!DOCTYPE html> <html> <head><title>SQL Test Report</title></head> <body> <h1>SQL Test Report</h1> <h2>静态扫描结果</h2> <table border="1"> <tr><th>规则</th><th>文件</th><th>行号</th><th>级别</th><th>说明</th></tr> <!-- 动态生成 --> </table> <h2>动态执行结果</h2> <table border="1"> <tr><th>文件</th><th>状态</th><th>耗时(ms)</th><th>影响行数</th><th>错误信息</th></tr> <!-- 动态生成 --> </table> </body> </html>看起来技术含量不高,但实用。团队里每个人打开邮件都能秒懂:绿色是通过,红色是失败,黄色是告警。相比以前在群里贴截图,这个方案专业太多。
4. 尚未完成的部分:为什么“未完成”
4.1 未完成清单与原因分析
我的项目名字叫“未完成”,不是自谦,是真的还有几个模块做得不完整。
第一是定时调度器。目前只能手动执行,或者通过命令行触发。虽然这也算自动化,但没有真正接入Jenkins的构建步骤。按我原来的规划,应该做到每次发布时自动拉取变更脚本、自动执行全量检查、自动将报告关联到发布单。这个没做完的主要原因是CI平台的权限和网络策略没有完全打通:测试脚本要连的数据库,在CI服务器上访问受限。这个问题需要运维侧配合,我暂时搁置了。
第二是静态分析模块对存储过程、函数这类复杂对象的覆盖。目前文件级的正则扫描能覆盖简单TSQL,但对于存储过程内部多层嵌套的动态SQL,正则很容易误判。要彻底解决,需要引入TSQL解析器,比如ANTLR的TSQL语法文件,然后做AST层面的分析。这块工程量比较大,而且公司内部的存储过程数量有限,投入产出比一般,所以暂时没做。
第三是风险评分体系。现在报告只会列出“有问题/没问题”,但决策者不一定清楚“这次变更的风险到底高不高”。我设想过一个综合评分模型:根据文件数量、涉及表数量、高风险语句数量、是否包含DDL、执行时长等因子,计算一个0-100的风险分。超过80分需要DBA人工复核,低于50分可以自动发布。这个模型我已经写了第一版,但没有经过大量样本训练,还没敢直接用。
4.2 剩余模块的技术选型设想
调度模块如果继续做,我会优先考虑以下方案:
- 轻量级:用Linux cron或Windows计划任务,写一个shell/bat脚本调用runner.py。
- 正规化:写一个Jenkins Pipeline步骤,将SQL测试作为构建流水线中的一环。Jenkins的Publish HTML Report插件可以直接展示HTML报告。
- 进阶:引入Airflow或DolphinScheduler这类数据调度平台,适合团队里已经有相关平台的情况。
事件触发方式上,我倾向于“代码仓库变更触发”,也就是开发提交PR时自动触发,用Webhook调用runner。这样集成度最高,但需要后端支持,优先级排在后面。
TSQL解析器这块,我调研过的路线有两条。第一条是使用Python的sqlparse做基础词法分析,虽然它不会解析AST,但能对语句进行分割、Token级别识别,用在存储过程体扫描场景时,比纯正则可控很多。第二条是走Java生态的antlr4 + tsql.g4,这是一套官方语法文件,解析出来的AST非常准确,但需要写大量遍历逻辑,小脚本场景下性价比不划算。第三是调用SQL Server官方提供的一些系统函数,比如sys.dm_exec_describe_first_result_set,可以帮你推断查询的返回结构,算是一种轻量级的语义校验。
风险评分模型,我认为后续最有价值。可以先做出规则权重表,比如:
| 检查项 | 权重 | 说明 |
|---|---|---|
| 高风险语句数量 | 30 | DROP/TRUNCATE/无WHERE的UPDATE |
| 涉及核心表数量 | 20 | 根据表名匹配核心表清单 |
| 是否动态执行 | 20 | EXEC/sp_executesql |
| 预估执行计划扫描 | 20 | 全表扫描/大表索引缺失 |
| 文件变更数量与体量 | 10 | 单次变更规模过大扣分 |
每个维度打分后按权重求和,再加一个“一票否决”清单,比如出现DROP DATABASE直接判定100分。评分模型做好了,发布流程里就能自动拦截明显有风险的变更。
4.3 项目继续推进的优先级建议
如果我现在还有时间继续做,我的优先级排序是这样的:
- 先把调度模块打通。因为这是让脚本真正“自动化”的关键,没有调度,它仍然是个手动工具。
- 然后做风险评分模型。有了评分,才能和发布流程衔接。
- 最后才是存储过程AST解析,因为这是最复杂、ROI最低的部分。
另外,测试脚本本身也需要测试。我在“未完成”期间最大的体会是,给SQL测试脚本写单元测试太重要了,否则你自己改了一版规则,都不知道把之前的什么功能改坏了。现在我的仓库里至少有10个用例是验证静态分析器本身的,比如造了几条已知有注入特征的SQL,确保它们能被扫出来。
5. 踩坑实录:SQL测试脚本落地过程中的问题与排查
5.1 环境与连接类问题速查表
这类问题在自动化脚本中最常见,经常让人怀疑是代码写错了,其实多半是环境问题。
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
Cannot open server "xxx" requested by the login - Client with IP address is not allowed | SQL Server未启用远程连接 | 在SQL Server配置管理器中启用TCP/IP,并放行防火墙端口 |
Login failed for user 'sa'. Reason: Password did not match | 连接字符串或账号密码错误 | 核对默认库、用户映射和账号锁定状态 |
The server principal "xxx" is not able to access the database "yyy" | 用户缺少库级别的guest权限 | 执行USE [yyy]; CREATE USER xxx FOR LOGIN xxx; EXEC sp_addrolemember 'db_datareader', 'xxx'; |
The OLE DB provider "MSDASQL" has not been registered | 32位/64位驱动不匹配 | 确保pyodbc连接串里指定正确的ODBC Driver,如DRIVER={ODBC Driver 17 for SQL Server} |
我在环境问题里最常踩的坑是SQL Server的“ad hoc distributed queries”被阻止。有一次脚本里用到了OPENROWSET去读另一台服务器上的数据,本地SSMS能跑,但自动化脚本直接报错。后来明白这是SQL Server的Ad Hoc Distributed Queries组件默认关闭了。解决方式是需要打开高级选项:
EXEC sp_configure 'show advanced options', 1; RECONFIGURE; EXEC sp_configure 'ad hoc distributed queries', 1; RECONFIGURE;不过这里要提醒一下,开启这个组件有一定安全风险,会让服务器更容易被利用来访问外部数据源。如果不是业务硬性要求,不建议在自动化脚本中开启。我是因为这个功能被专项审计否过,才长记性。
5.2 脚本执行本身的诡异坑
有些问题是SQL Server特有的“坑”,不跑一遍自动化脚本你根本遇不到。
第一个坑是sp_executesql与事务的相互作用。我有一段脚本测试事务模式,把所有语句拼进一个事务里,但其中一个语句是EXEC sp_executesql N'...',内部又开了隐式事务,结果事务提交顺序错乱。后来我明确要求测试脚本里不直接使用事务内的动态SQL,或者把动态SQL改成单条独立语句,减少不确定性。
第二个坑是GETDATE()这类函数的不确定性。回归测试里,我要对比两次执行结果是否一致,但SQL里如果包含GETDATE()、NEWID()、RAND()这类不确定性函数,自然会不一致。后来我在回归对比时,会对结果集做一次序列化,然后忽略掉这些函数导致的差异,只对比业务字段。具体做法是,在测试SQL外面包一层SELECT,把不确定字段先转换成字符串“忽略此列”,再对比。
第三是字符集和排序规则的问题。SQL Server默认的排序规则可能是Chinese_PRC_CI_AS,对中文大小写不敏感,但到测试环境如果排序规则不同,某些字符串查询行为会不一样。我第一次跑脚本时,开发库查出来10行,测试库查出来12行,排查了半天,最后发现是Collate不同。建议在配置阶段就统一所有环境的排序规则,或者在关键SQL里显式加上COLLATE DATABASE_DEFAULT。
5.3 自动化与脚本化特有的坑
作为测试脚本,它本身也容易引入新问题。
第一个是死锁。当多个测试脚本同时连接数据库,互相等待锁资源。自动化巡检如果在业务高峰期跑,特别容易造成阻塞。我的对策是避开业务高峰期,并且把巡检会话设置成READ UNCOMMITTED(脏读),保证不阻塞业务。在SQL Server里,可以通过在连接串里加IsolationLevel=ReadUncommitted,或者在脚本开头执行SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED。
第二个坑是日志文件膨胀。测试脚本如果频繁插入大量数据,即使最终回滚,也可能导致日志文件瞬间暴涨。尤其是用事务模式大批量插入时,日志会线性增长。这个问题我在压测场景里遇到过,那个服务器的日志盘直接写满。后来我给执行器加了一个“最大回滚大小”检查,如果预估影响行数超过阈值,直接跳过执行。
第三个是隐藏的账号权限问题。自动化脚本最好用专门的测试账号,不要用开发或DBA的超管账号。这样一旦脚本本身被攻击或者执行了恶意SQL,危害范围可控。我在设计连接配置时就强制要求,生产环境不允许使用sa或sysadmin权限账号。这一点刚开始团队有意见,觉得变麻烦了,但等真出过一两次问题之后,大家就都理解了。
写在最后的实战心得
我这段时间最大的感受是,测试脚本这东西,核心不是写代码,而是定流程。代码只是一个载体,把“怎么检查SQL”的经验固化下来。写的过程中你会慢慢发现,很多以前靠人肉保证的事情,其实都能变成规则。规则多了,流程就硬了,流程硬了,发布的时候心就稳了。
如果你也想搞一个类似的sql测试脚本,我的建议是别从零开始追求完美,先把最简单的文件扫描和关键字检测跑通,哪怕只检查一个“无WHERE条件的UPDATE”也好。用起来,再迭代,比憋大招有用得多。项目虽然写着“未完成”,但它已经在帮我干活了,这比一个“完成但没人用”的系统强一百倍。