从数百个存储过程中解析出完整的动态 SQL 表达式
Parsing out complete dynamic SQL expressions from hundreds of stored procedures
我继承了一个大型应用程序,它有数百个存储过程,其中许多使用动态 SQL。为了更好地处理我正在处理的 SQL 类型,如果我有办法解析 all 这些存储的查询文本,那将非常有用程序并提取包含在其中的任何动态 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:是的,我很清楚"I shouldn't be doing this"。
编辑
根据下面的建议,尝试了以下查询但在这种情况下并不是很有用:
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
初步估计,下面是使用 ScriptDom
在 C# 中执行此操作的方法。
获取所有存储过程定义的列表很容易。这可以在 T-SQL 中完成,甚至:
sp_msforeachdb 'select definition from [?].sys.sql_modules'
或者以通常的方式编写脚本数据库,或者使用 SMO。无论如何,我假设您可以通过某种方式将它们放入 List<string>
中,供代码使用。
Microsoft.SqlServer.TransactSql.ScriptDom
is available as a 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);
}
}
}
假设 procedures
是一个具有存储过程定义的 List<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(...)
的所有实例和sp_executesql
(不考虑变量名),也是可以的。
缺点当然是这不是纯T-SQL。您可以使用任何您喜欢的 .NET 语言(我使用 C# 因为我最熟悉它),但它仍然涉及编写外部代码。如果您知道所有代码都遵循一种特定模式,该模式对于 T-SQL 字符串操作分析来说足够简单,则更原始的解决方案(例如 CHARINDEX
处理字符串可能会奏效。
我继承了一个大型应用程序,它有数百个存储过程,其中许多使用动态 SQL。为了更好地处理我正在处理的 SQL 类型,如果我有办法解析 all 这些存储的查询文本,那将非常有用程序并提取包含在其中的任何动态 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:是的,我很清楚"I shouldn't be doing this"。
编辑
根据下面的建议,尝试了以下查询但在这种情况下并不是很有用:
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
初步估计,下面是使用 ScriptDom
在 C# 中执行此操作的方法。
获取所有存储过程定义的列表很容易。这可以在 T-SQL 中完成,甚至:
sp_msforeachdb 'select definition from [?].sys.sql_modules'
或者以通常的方式编写脚本数据库,或者使用 SMO。无论如何,我假设您可以通过某种方式将它们放入 List<string>
中,供代码使用。
Microsoft.SqlServer.TransactSql.ScriptDom
is available as a 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);
}
}
}
假设 procedures
是一个具有存储过程定义的 List<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(...)
的所有实例和sp_executesql
(不考虑变量名),也是可以的。
缺点当然是这不是纯T-SQL。您可以使用任何您喜欢的 .NET 语言(我使用 C# 因为我最熟悉它),但它仍然涉及编写外部代码。如果您知道所有代码都遵循一种特定模式,该模式对于 T-SQL 字符串操作分析来说足够简单,则更原始的解决方案(例如 CHARINDEX
处理字符串可能会奏效。