3

我继承了一个具有数百个存储过程的大型应用程序,其中许多使用动态 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}'

我正在寻找上述输出(最终将在解析所有存储过程的单个查询中调用)是:

SELECT col1, col2, col3 
FROM ' + @DatabaseName + '.dbo.' + @TableName + ' 
WHERE {some criteria expression that also contains inline quotes}

因此,不是传入参数值之后的表达式,而是存储过程文本中的表达式文本,包括参数名称。

我可以接受动态 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
4

1 回答 1

5

大致来说,这是在 C# 中使用ScriptDom.

获取所有存储过程定义的列表很容易。这可以在 T-SQL 中完成,甚至:

sp_msforeachdb 'select definition from [?].sys.sql_modules'

或者以通常的方式编写数据库脚本,或者使用 SMO。无论如何,我假设您可以以List<string>某种方式将它们放入其中,以供代码使用。

Microsoft.SqlServer.TransactSql.ScriptDom可作为NuGet包使用,因此请将其添加到全新的应用程序中。我们问题的核心是编写一个访问者,它将从 T-SQL 脚本中提取我们感兴趣的节点:

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);
    }
  }
}

假设proceduresList<string>具有存储过程定义的,然后我们像这样应用访问者:

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());
    }
  }
}

.Script()是我拼凑起来的一种方便的方法,因此我们可以将片段转换回纯文本:

public static class TSqlFragmentExtensions {
  public static string Script(this TSqlFragment fragment) {
    return String.Join("", fragment.ScriptTokenStream
      .Skip(fragment.FirstTokenIndex)
      .Take(fragment.LastTokenIndex - fragment.FirstTokenIndex + 1)
      .Select(t => t.Text)
    );
  }
}

这将打印分配给名为 的变量的所有存储过程中的所有表达式@Query

这种方法的好处是您可以轻松解析语句,因此更复杂的处理,例如将字符串表达式转回其未转义形式或寻找EXEC(...)and的所有实例sp_executesql(不管涉及的变量名),也是可能的。

当然,缺点是这不是纯 T-SQL。您可以使用任何您喜欢的 .NET 语言(我使用过 C#,因为我最熟悉它),但它仍然涉及编写外部代码。CHARINDEX如果您知道所有代码都遵循一种对 T-SQL 字符串操作来说足够简单的特定模式来分析,那么更原始的解决方案(例如仅遍历字符串)可能会起作用。

于 2016-06-16T19:51:34.337 回答