如何使用C#执行包含Go、单行及多行注释的.sql文件语句
C# 执行含注释、GO批处理关键字的SQL文件实现方案
核心需要解决三个问题:正确过滤--单行注释、/* */多行注释,避免误删字符串内的同格式内容;正确识别独立的GO批处理关键字拆分语句块,避免误拆分标识符、字符串内的GO字符;逐批执行SQL语句,解决ADO.NET原生不识别GO关键字的问题。
实现步骤
- 第一步:SQL文本预处理与批处理拆分
不要直接用正则做全局替换、不要直接用Split('GO')做拆分,这两种方案都会误处理字符串、标识符内的注释符号和GO字符,生产环境极易出问题。推荐逐字符扫描文本,维护三个状态标记:是否在多行注释内、是否在单引号字符串内、是否在单行注释内:- 不在字符串、不在注释块内时,遇到
/*标记进入多行注释,直到遇到*/退出注释,中间内容全部跳过 - 不在字符串、不在注释块内时,遇到
--标记进入单行注释,直到遇到换行符退出注释,中间内容全部跳过 - 遇到单引号时,要额外判断是不是转义的连续两个单引号
'',正常切换字符串状态标记,字符串内的所有注释符号、GO字符都按普通内容处理 - 不在字符串、不在注释块内时,识别行首前后带空白的独立
GO关键字(不区分大小写),每遇到一个合法GO,就把之前累积的SQL内容作为一个独立批次,清空缓存继续扫描 - 文件扫描结束后,把最后剩余的非空SQL内容作为最后一个批次
- 不在字符串、不在注释块内时,遇到
- 第二步:数据库逐批执行
用对应数据库的ADO.NET驱动创建连接,遍历拆分好的所有SQL批次,跳过全空白的空批次,每个批次单独创建Command对象执行即可:- 如果需要原子性,可以把所有批次包裹在一个全局事务中,任意批次执行失败就整体回滚,注意长事务会带来锁表风险,大脚本谨慎使用
- 如果脚本体积很大,不要一次性把整个文件读入内存,改用文件流逐行读取处理,降低内存占用
- 可选:依赖官方组件减少自研成本
如果是SQL Server场景,可以直接引入SMO套件里的ServerConnection类,它原生支持带GO、带注释的SQL脚本执行,不需要自己写拆分逻辑,缺点是需要额外依赖SMO相关程序集,部署时要同步打包。
核心实现代码示例
using Microsoft.Data.SqlClient; using System.Text; // 读取SQL文件 string sqlContent = File.ReadAllText("your_script_path.sql", Encoding.UTF8); List<string> sqlBatches = SplitToSqlBatches(sqlContent); // 逐批执行 string connectionString = "你的数据库连接字符串"; using var conn = new SqlConnection(connectionString); await conn.OpenAsync(); // 需要全局事务时放开以下注释 // using var transaction = conn.BeginTransaction(); try { foreach (string batch in sqlBatches) { if (string.IsNullOrWhiteSpace(batch)) continue; using var cmd = new SqlCommand(batch, conn); // cmd.Transaction = transaction; await cmd.ExecuteNonQueryAsync(); } // transaction.Commit(); } catch (Exception ex) { // transaction.Rollback(); Console.WriteLine($"SQL执行异常:{ex.Message}"); throw; } // 拆分SQL批次、过滤注释的核心方法 List<string> SplitToSqlBatches(string rawSql) { List<string> batches = new List<string>(); StringBuilder currentBatch = new StringBuilder(); bool inMultiComment = false; bool inString = false; bool inLineComment = false; for (int i = 0; i < rawSql.Length; i++) { char curr = rawSql[i]; char next = i < rawSql.Length - 1 ? rawSql[i + 1] : '\0'; if (inMultiComment) { if (curr == '*' && next == '/') { inMultiComment = false; i++; } continue; } if (inLineComment) { if (curr == '\n') { inLineComment = false; currentBatch.Append(curr); } continue; } if (curr == '\'' && !inMultiComment && !inLineComment) { if (next == '\'') { currentBatch.Append(curr); currentBatch.Append(next); i++; continue; } inString = !inString; currentBatch.Append(curr); continue; } if (!inString) { if (curr == '/' && next == '*') { inMultiComment = true; i++; continue; } if (curr == '-' && next == '-') { inLineComment = true; i++; continue; } if (IsValidGoSeparator(rawSql, i, out int goEndPos)) { batches.Add(currentBatch.ToString().Trim()); currentBatch.Clear(); i = goEndPos; continue; } } currentBatch.Append(curr); } string lastBatch = currentBatch.ToString().Trim(); if (!string.IsNullOrEmpty(lastBatch)) { batches.Add(lastBatch); } return batches; } // 校验当前位置是否为合法的GO批处理分隔符 bool IsValidGoSeparator(string sql, int currIndex, out int endIndex) { endIndex = currIndex; int lineStart = currIndex; while (lineStart > 0 && char.IsWhiteSpace(sql[lineStart - 1]) && sql[lineStart - 1] != '\n' && sql[lineStart -1] != '\r') { lineStart--; } if (lineStart > 0 && sql[lineStart -1] != '\n' && sql[lineStart -1] != '\r') { return false; } if (currIndex + 1 >= sql.Length) return false; if (char.ToLower(sql[currIndex]) != 'g' || char.ToLower(sql[currIndex + 1]) != 'o') { return false; } int afterGoPos = currIndex + 2; while (afterGoPos < sql.Length) { char c = sql[afterGoPos]; if (c == '\r' || c == '\n') break; if (char.IsWhiteSpace(c)) { afterGoPos++; continue; } if (c == '-' && afterGoPos + 1 < sql.Length && sql[afterGoPos + 1] == '-') break; if (c == '/' && afterGoPos + 1 < sql.Length && sql[afterGoPos + 1] == '*') break; return false; } endIndex = afterGoPos - 1; return true; }
注意事项
- 如果是MySQL、PostgreSQL等其他数据库场景,需要在上述逻辑基础上额外适配对应数据库的语法规则,比如MySQL的
DELIMITER分隔符修改、存储过程内的分号处理等 - 不要信任用户上传的SQL文件,生产环境要做权限控制,避免SQL注入风险
内容的提问来源于stack exchange,提问作者Aravind C S
相关产品推荐
相关产品推荐

