tbo*_*one 3 t-sql sql-server parsing stored-procedures scriptdom
我继承了一个包含数百个存储过程的大型应用程序,其中许多使用动态 SQL。为了更好地处理我正在处理的 SQL 类型,如果我有办法解析所有这些存储过程的查询文本并提取其中包含的任何动态 SQL 的完整表达式,那将非常有用。
一个简化的表达可能是:
declare @query nvarchar(max)
set @query = 'SELECT col1,col2,col3 from ' + @DatabaseName + '.dbo.' + @TableName + ' WHERE {some criteria expression that also contains inline quotes}'
Run Code Online (Sandbox Code Playgroud)
我正在寻求上述输出(最终将在解析所有存储过程的单个查询中调用)是:
SELECT col1, col2, col3
FROM ' + @DatabaseName + '.dbo.' + @TableName + '
WHERE {some criteria expression that also contains inline quotes}
Run Code Online (Sandbox Code Playgroud)
因此,不是传入参数值之后的表达式,而是存储过程文本中的表达式文本,包括参数名称。
我对动态 SQL 参数名称为 的完全不安全的假设没有意见@query,因此在 SQL 表达式中搜索它以用作提取文本的起始位置是可以容忍的,但是因为有单引号内联,我没有简单的方法知道变量的赋值在哪里完成。
我在这个问题中包含了 [antlr] 和 [parsing] 标签,因为我觉得这超出了 T-SQL 的能力。
PS:是的,我很清楚“我不应该这样做”。
编辑
根据下面的建议,尝试了以下查询,但在这种情况下并没有真正有用:
SELECT
db_name(dbid) DB_NAME
,cacheobjtype, objtype, object_name(objectid) ObjectName
,objectid
,x.text
,usecounts
-- , x.*,z.* ,db_name(dbid)
FROM
sys.dm_exec_cached_plans z
CROSS APPLY sys.dm_exec_sql_text(plan_handle) x
WHERE
--usecounts > 1
--objType = 'Proc' and -- include if you only want to see stored procedures
db_name(dbid) not like 'ReportServer%' and db_name(dbid) <> 'msdb' and db_name(dbid) not like 'DBADB%' and db_name(dbid) <> 'master'
--ORDER BY usecounts DESC
ORDER BY objtype
Run Code Online (Sandbox Code Playgroud)
粗略地说,以下是您在 C# 中使用ScriptDom.
获取所有存储过程定义的列表很容易。这可以在 T-SQL 中完成,甚至:
sp_msforeachdb 'select definition from [?].sys.sql_modules'
Run Code Online (Sandbox Code Playgroud)
或以通常的方式编写脚本数据库,或使用 SMO。在任何情况下,我假设您可以以List<string>某种方式将这些放入代码中以供使用。
Microsoft.SqlServer.TransactSql.ScriptDom可作为NuGet包使用,因此将其添加到全新的应用程序中。我们问题的核心是编写一个访问者,该访问者将从 T-SQL 脚本中提取我们感兴趣的节点:
sp_msforeachdb 'select definition from [?].sys.sql_modules'
Run Code Online (Sandbox Code Playgroud)
假设procedures是一个List<string>具有存储过程定义的,然后我们像这样应用访问者:
class DynamicQueryFinder : TSqlFragmentVisitor {
public List<ScalarExpression> QueryAssignments { get; } = new List<ScalarExpression>();
public string ProcedureName { get; private set; }
// Grab "CREATE PROCEDURE ..." nodes
public override void Visit(CreateProcedureStatement node) {
ProcedureName = node.ProcedureReference.Name.BaseIdentifier.Value;
}
// Grab "SELECT @Query = ..." nodes
public override void Visit(SelectSetVariable node) {
if ("@Query".Equals(node.Variable.Name, StringComparison.OrdinalIgnoreCase)) {
QueryAssignments.Add(node.Expression);
}
}
// Grab "SET @Query = ..." nodes
public override void Visit(SetVariableStatement node) {
if ("@Query".Equals(node.Variable.Name, StringComparison.OrdinalIgnoreCase)) {
QueryAssignments.Add(node.Expression);
}
}
// Grab "DECLARE @Query = ..." nodes
public override void Visit(DeclareVariableElement node) {
if (
"@Query".Equals(node.VariableName.Value, StringComparison.OrdinalIgnoreCase) &&
node.Value != null
) {
QueryAssignments.Add(node.Value);
}
}
}
Run Code Online (Sandbox Code Playgroud)
.Script() 是我拼凑起来的一个方便的方法,因此我们可以将片段转回纯文本:
foreach (string procedure in procedures) {
TSqlFragment fragment;
using (var reader = new StringReader(procedure)) {
IList<ParseError> parseErrors;
var parser = new TSql130Parser(true); // or a lower version, I suppose
fragment = parser.Parse(reader, out parseErrors);
if (parseErrors.Any()) {
// handle errors
continue;
}
}
var dynamicQueryFinder = new DynamicQueryFinder();
fragment.Accept(dynamicQueryFinder);
if (dynamicQueryFinder.QueryAssignments.Any()) {
Console.WriteLine($"===== {dynamicQueryFinder.ProcedureName} =====");
foreach (ScalarExpression assignment in dynamicQueryFinder.QueryAssignments) {
Console.WriteLine(assignment.Script());
}
}
}
Run Code Online (Sandbox Code Playgroud)
这将打印分配给名为 的变量的所有存储过程中的所有表达式@Query。
这种方法的好处是,您可以轻松地解析语句,因此更复杂的处理,例如将字符串表达式转回其未转义形式或寻找EXEC(...)and 的所有实例sp_executesql(无论涉及的变量名称如何),也是可能的。
当然,缺点是这不是纯 T-SQL。您可以使用任何您喜欢的 .NET 语言(我使用过 C#,因为我最熟悉它),但它仍然涉及编写外部代码。CHARINDEX如果您知道所有代码都遵循一个特定模式,该模式足够简单以供 T-SQL 字符串操作分析,那么更原始的解决方案(例如仅通过字符串使用您的方式)可能会起作用。