2012-02-23 26 views
0

我有一个要求来审计我们的所有存储过程,其中成千上万个存储过程,并确定哪些是只读或读写。我想知道是否有人知道准确地做到这一点的好方法。用于确定存储过程是只读还是读写的脚本

我已经写了我自己的脚本到目前为止,但我只有〜85%的准确性。我绊倒了真正只读的存储过程,但他们创建了一些临时表。为了我的目的,这些是只读的。我也不能忽略这些,因为有很多读写过程也适用于临时表。

[编辑] 我在20名的程序,我知道找那是相当复杂的,他们比较我从查询得到的结果有大致〜85%的准确性。

这是我目前使用的查询:

CREATE TABLE tempdb.dbo.[_tempProcs] 
(objectname varchar(150), dbname varchar(150), ROUTINE_DEFINITION varchar(4000)) 
GO 
EXEC sp_MSforeachdb 
'USE [?] 
DECLARE @dbname VARCHAR(200) 
SET @dbname = DB_NAME() 
IF 1 = 1 AND (@dbname NOT IN (''master'',''model'',''msdb'',''tempdb'',''distribution'') 
BEGIN 
EXEC('' 
INSERT INTO tempdb.dbo.[_tempProcs](objectname, dbname, ROUTINE_DEFINITION) 
SELECT ROUTINE_NAME AS ObjectName, ''''?'''' AS dbname, ROUTINE_DEFINITION 
FROM [?].INFORMATION_SCHEMA.ROUTINES WITH(NOLOCK) 
WHERE ROUTINE_DEFINITION LIKE ''''%INSERT [^]%'''' 
    OR ROUTINE_DEFINITION LIKE ''''%UPDATE [^]%'''' 
    OR ROUTINE_DEFINITION LIKE ''''%INTO [^]%'''' 
    OR ROUTINE_DEFINITION LIKE ''''%DELETE [^]%'''' 
    OR ROUTINE_DEFINITION LIKE ''''%CREATE TABLE[^]%'''' 
    OR ROUTINE_DEFINITION LIKE ''''%DROP [^]%'''' 
    OR ROUTINE_DEFINITION LIKE ''''%ALTER [^]%'''' 
    OR ROUTINE_DEFINITION LIKE ''''%TRUNCATE [^]%'''' 
    AND ROUTINE_TYPE=''''PROCEDURE'''' 
'') 
END 
' 
GO 
SELECT * FROM tempdb.dbo.[_tempProcs] WITH(NOLOCK) 

我还没有细化它,此刻我只想把重点放在可写的查询,看看我能得到它准确。还有一个问题是ROUTINE_DEFINITION只给出了前4000个字符,所以我可能会错过任何正在写入4000个字符长度之后的字符。我可能会最终提出一些建议。获取这个查询返回的特效列表,然后进一步尝试Arrons的建议,看看我是否可以清除更多。我会很高兴95%的准确性。

我将在这一天左右再来看看我能否得到任何进一步的建议,但非常感谢。好吧,这是我最终做的,看起来我至少有95%的准确性,可能会更高。我试图迎合任何我可以想到的场景。

我将存储过程脚本化为文件,并编写了一个C#winform应用程序来解析这些文件并找到合法写入真实数据库的应用程序。

我很高兴发布此代码的状态引擎我用在这里,但没有保证。我承受着压力,实际上没有时间来美化代码,并用很好的变量名称等进行重构,并在其中添加了很好的评论,我有3个小时的时间来完成它,并且我只是挤压它,因此对于那些谁在乎,并且可能在未来帮助,那就是:

using System; 
using System.Collections.Generic; 
using System.Linq; 
using System.Text; 
using System.IO; 

namespace SQLParser 
{ 
    public class StateEngine 
    { 
     public static class CurrentState 
     { 
      public static bool IsInComment; 
      public static bool IsInCommentBlock; 
      public static bool IsInInsert; 
      public static bool IsInUpdate; 
      public static bool IsInDelete; 
      public static bool IsInCreate; 
      public static bool IsInDrop; 
      public static bool IsInAlter; 
      public static bool IsInTruncate; 
      public static bool IsInInto; 
     } 

     public class ReturnState 
     { 
      public int LineNumber { get; set; } 
      public bool Value { get; set; } 
      public string Line { get; set; } 
     } 

     private static int _tripLine = 0; 
     private static string[] _lines; 

     public ReturnState ParseFile(string fileName) 
     { 
      var retVal = false; 
      _tripLine = 0; 
      ResetCurrentState(); 

      _lines = File.ReadAllLines(fileName); 

      for (int i = 0; i < _lines.Length; i++) 
      { 
       retVal = ParseLine(_lines[i], i); 

       //return true the moment we have a valid case 
       if (retVal) 
       { 
        ResetCurrentState(); 
        return new ReturnState() { LineNumber = _tripLine, Value = retVal, Line = _lines[_tripLine] }; 
       } 
      } 

      if (CurrentState.IsInInsert || 
       CurrentState.IsInDelete || 
       CurrentState.IsInUpdate || 
       CurrentState.IsInDrop || 
       CurrentState.IsInAlter || 
       CurrentState.IsInTruncate) 
      { 
       retVal = true; 
       ResetCurrentState(); 
       return new ReturnState() { LineNumber = _tripLine, Value = retVal, Line = _lines[_tripLine] }; 
      } 

      return new ReturnState() { LineNumber = -1, Value = retVal }; 
     } 

     private static void ResetCurrentState() 
     { 
      CurrentState.IsInAlter = false; 
      CurrentState.IsInCreate = false; 
      CurrentState.IsInDelete = false; 
      CurrentState.IsInDrop = false; 
      CurrentState.IsInInsert = false; 
      CurrentState.IsInTruncate = false; 
      CurrentState.IsInUpdate = false; 
      CurrentState.IsInInto = false; 
      CurrentState.IsInComment = false; 
      CurrentState.IsInCommentBlock = false; 
     } 

     private static bool ParseLine(string sqlLine, int lineNo) 
     { 
      var retVal = false; 
      var _currentWord = 0; 
      var _tripWord = 0; 
      var _offsetTollerance = 4; 

      sqlLine = sqlLine.Replace("\t", " "); 

      //This would have been set in previous line, so reset it 
      if (CurrentState.IsInComment) 
       CurrentState.IsInComment = false; 
      var words = sqlLine.Split(char.Parse(" ")).Where(x => x.Length > 0).ToArray(); 
      for (int i = 0; i < words.Length; i++) 
      { 
       if (string.IsNullOrWhiteSpace(words[i])) 
        continue; 

       _currentWord += 1; 

       if (CurrentState.IsInCommentBlock && words[i].EndsWith("*/") || words[i] == "*/") { CurrentState.IsInCommentBlock = false; } 
       if (words[i].StartsWith("/*")) { CurrentState.IsInCommentBlock = true; } 
       if (words[i].StartsWith("--") && !CurrentState.IsInCommentBlock) { CurrentState.IsInComment = true; } 

       if (words[i].Length == 1 && CurrentState.IsInUpdate) 
       { 
        //find the alias table name, find 'FROM' and then next word 
        var tempAlias = words[i]; 
        var tempLine = lineNo; 

        for (int l = lineNo; l < _lines.Length; l++) 
        { 
         var nextWord = ""; 
         var found = false; 

         var tempWords = _lines[l].Replace("\t", " ").Split(char.Parse(" ")).Where(x => x.Length > 0).ToArray(); 

         for (int m = 0; m < tempWords.Length; m++) 
         { 
          if (found) { break; } 

          if (tempWords[m].ToLower() == tempAlias && tempWords[m - m == 0 ? m : 1].ToLower() != "update") 
          { 
           nextWord = m == tempWords.Length - 1 ? "" : tempWords[m + 1].ToString(); 
           var prevWord = m == 0 ? "" : tempWords[m - 1].ToString(); 
           var testWord = ""; 

           if (nextWord.ToLower() == "on" || nextWord == "") 
           { 
            testWord = prevWord; 
           } 
           if (prevWord.ToLower() == "from") 
           { 
            testWord = nextWord; 
           } 

           found = true; 

           if (testWord.StartsWith("#") || testWord.StartsWith("@")) 
           { 
            ResetCurrentState(); 
           } 

           break; 
          } 
         } 
         if (found) { break; } 
        } 
       } 

       if (!CurrentState.IsInComment && !CurrentState.IsInCommentBlock) 
       { 
        #region SWITCH 

        if (words[i].EndsWith(";")) 
        { 
         retVal = SetStateReturnValue(retVal); 
         ResetCurrentState(); 
         return retVal; 
        } 


        if ((CurrentState.IsInCreate || CurrentState.IsInDrop && (words[i].ToLower() == "procedure" || words[i].ToLower() == "proc")) && (lineNo > _tripLine ? 1000 : _currentWord - _tripWord) < _offsetTollerance) 
         ResetCurrentState(); 

        switch (words[i].ToLower()) 
        { 
         case "insert": 
          //assume that we have parsed all lines/words and got to next keyword, so return previous state 
          retVal = SetStateReturnValue(retVal); 
          if (retVal) 
           return retVal; 

          CurrentState.IsInInsert = true; 
          _tripLine = lineNo; 
          _tripWord = _currentWord; 
          continue; 

         case "update": 
          //assume that we have parsed all lines/words and got to next keyword, so return previous state 
          retVal = SetStateReturnValue(retVal); 
          if (retVal) 
           return retVal; 

          CurrentState.IsInUpdate = true; 
          _tripLine = lineNo; 
          _tripWord = _currentWord; 
          continue; 

         case "delete": 
          //assume that we have parsed all lines/words and got to next keyword, so return previous state 
          retVal = SetStateReturnValue(retVal); 
          if (retVal) 
           return retVal; 

          CurrentState.IsInDelete = true; 
          _tripLine = lineNo; 
          _tripWord = _currentWord; 
          continue; 

         case "into": 
          //assume that we have parsed all lines/words and got to next keyword, so return previous state 
          //retVal = SetStateReturnValue(retVal, lineNo); 
          //if (retVal) 
          // return retVal; 

          CurrentState.IsInInto = true; 
          _tripLine = lineNo; 
          _tripWord = _currentWord; 
          continue; 

         case "create": 
          //assume that we have parsed all lines/words and got to next keyword, so return previous state 
          retVal = SetStateReturnValue(retVal); 
          if (retVal) 
           return retVal; 

          CurrentState.IsInCreate = true; 
          _tripLine = lineNo; 
          _tripWord = _currentWord; 
          continue; 

         case "drop": 
          //assume that we have parsed all lines/words and got to next keyword, so return previous state 
          retVal = SetStateReturnValue(retVal); 
          if (retVal) 
           return retVal; 

          CurrentState.IsInDrop = true; 
          _tripLine = lineNo; 
          continue; 

         case "alter": 
          //assume that we have parsed all lines/words and got to next keyword, so return previous state 
          retVal = SetStateReturnValue(retVal); 
          if (retVal) 
           return retVal; 

          CurrentState.IsInAlter = true; 
          _tripLine = lineNo; 
          _tripWord = _currentWord; 
          continue; 

         case "truncate": 
          //assume that we have parsed all lines/words and got to next keyword, so return previous state 
          retVal = SetStateReturnValue(retVal); 
          if (retVal) 
           return retVal; 

          CurrentState.IsInTruncate = true; 
          _tripLine = lineNo; 
          _tripWord = _currentWord; 
          break; 

         default: 
          break; 

        } 

        #endregion 

        if (CurrentState.IsInInsert || CurrentState.IsInDelete || CurrentState.IsInUpdate || CurrentState.IsInDrop || CurrentState.IsInAlter || CurrentState.IsInTruncate || CurrentState.IsInInto) 
        { 
         if ((words[i].StartsWith("#") || words[i].StartsWith("@") || words[i].StartsWith("dbo.#") || words[i].StartsWith("[email protected]")) && (lineNo > _tripLine ? 1000 : _currentWord - _tripWord) < _offsetTollerance) 
         { 
          ResetCurrentState(); 
          continue; 
         } 

        } 

        if ((CurrentState.IsInInsert || CurrentState.IsInInto || CurrentState.IsInUpdate) && (((_currentWord != _tripWord) && (lineNo > _tripLine ? 1000 : _currentWord - _tripWord) < _offsetTollerance) || (lineNo > _tripLine))) 
        { 
         retVal = SetStateReturnValue(retVal); 
         if (retVal) 
          return retVal; 
        } 

       } 
      } 

      return retVal; 
     } 

     private static bool SetStateReturnValue(bool retVal) 
     { 
      if (CurrentState.IsInInsert || 
       CurrentState.IsInDelete || 
       CurrentState.IsInUpdate || 
       CurrentState.IsInDrop || 
       CurrentState.IsInAlter || 
       CurrentState.IsInTruncate) 
      { 
       retVal = (CurrentState.IsInInsert || 
       CurrentState.IsInDelete || 
       CurrentState.IsInUpdate || 
       CurrentState.IsInDrop || 
       CurrentState.IsInAlter || 
       CurrentState.IsInTruncate); 
      } 
      return retVal; 
     } 

    } 
} 

用法

var fileResult = new StateEngine().ParseFile(*path and filename*); 
+0

如果一个存储过程写入到另一个数据库,或者使用'RAISERROR WITH LOG'或XP_REGWRITE,或写入文件或发送电子邮件?基本上你正在试图建立一个非常宽的网络,我不认为在SQL Server中有这个捷径。 – 2012-02-23 21:54:16

+0

@AaronBertrand这就是为什么我发布这个问题的原因 – Ryk 2012-02-23 22:04:40

+0

不幸的是答案是否定的(我不是说你不应该问这个问题)。正如您已经意识到的那样,您可以在存储过程的文本中搜索某些内容,以便进行一些有教育的猜测。然而,这是“信任但验证”的情况。 RegEx和模式匹配只会显示数量,而不是质量。此外,还有各种其他变量尚未提出,例如发现的单词是否在评论,参数或变量名称,字符串文字等中找到。 – 2012-02-23 22:14:16

回答

1

您可以尝试将sys.sql_modules与单词解析表值函数结合使用。 编辑:将UDF重命名为fnParseSQLWords,它标识注释 编辑:向右行添加条件并将所有varchar更改为nvarchar 编辑:添加和w.id > 1;到主选择语句以避免在CREATE上过滤时在主CREATE PROC上命中。

create function [dbo].[fnParseSQLWords](@str nvarchar(max), @delimiter nvarchar(30)='%[^a-zA-Z0-9\_]%') 
returns @result table(id int identity(1,1), bIsComment bit, word nvarchar(max)) 
begin 
    if left(@delimiter,1)<>'%' set @delimiter='%'[email protected]; 
    if right(@delimiter,1)<>'%' set @delimiter+='%'; 
    set @str=rtrim(@str); 
    declare @pi int=PATINDEX(@delimiter,@str); 
    declare @s2 nvarchar(2)=substring(@str,@pi,2); 
    declare @bLineComment bit=case when @s2='--' then 1 else 0 end; 
    declare @bBlockComment bit=case when @s2='/*' then 1 else 0 end; 

    while @pi>0 begin  
     insert into @result select case when (@bLineComment=1 or @bBlockComment=1) then 1 else 0 end 
      , LEFT(@str,@pi-1) where @pi>1; 
     set @s2=substring(@str,@pi,2); 
     set @str=RIGHT(@str,len(@str)[email protected]); 
     set @pi=PATINDEX(@delimiter,@str); 
     set @bLineComment=case when @s2='--' then 1 else @bLineComment end; 
     set @bBlockComment=case when @s2='/*' then 1 else @bBlockComment end; 
     set @bLineComment=case when left(@s2,1) in (char(10),char(13)) then 0 else @bLineComment end; 
     set @bBlockComment=case when @s2='*/' then 0 else @bBlockComment end; 
    end 

    insert into @result select case when (@bLineComment=1 or @bBlockComment=1) then 1 else 0 end 
     , @str where LEN(@str)>0; 
    return; 
end 
GO 

-- List all update procedures 
select distinct ProcName=p.name --, w.id, w.bIsComment, w.word 
from sys.sql_modules m 
inner join sys.procedures p on p.object_id=m.object_id 
cross apply dbo.fnParseSQLWords(m.[definition], default) w 
where w.word in ('INSERT','UPDATE','DELETE','INTO','CREATE','DROP','ALTER','TRUNCATE') 
and w.bIsComment=0 
and w.id > 1; 
GO 
+0

现在正着手添加注释行和块检测。敬请期待... – 2012-02-24 03:41:13

+0

你能否解释一下,如何找到像'INSERT'这样的单词不同于问题中的脚本(假设OP将它改为使用sys.sql_modules)? – 2012-02-24 03:52:04

+0

这看起来很有希望,我会继续检查 – Ryk 2012-02-24 03:59:04

4

SQL Server不存储决定是否与存储的任何属性,特性或其他元数据过程执行任何写入操作。我说你可以剔除不包含字符串一样的存储过程:

INTO 
CREATE%TABLE 
DELETE 
INSERT 
UPDATE 
TRUNCATE 
OUTPUT 

这不是一个详尽的清单,短短即兴。但是,当然这会有一些误报,因为剩下的一些程序可能自然有这些词语(例如称为“GetIntolerables”的存储过程)。您必须对剩余的人进行一些手动分析,以确定实际上这些关键字是否按预期使用,或者它们只是副作用。您也无法确切知道创建#temp表的过程是否仅用于阅读目的(尽管您已经在您的问题中解释了这一点,但我不清楚这是否是一个“打”或不)。

在SQL Server 2012中,您可以稍微接近一点,或者至少确定存储过程不要返回结果集(暗示它们必须执行其他操作)。你可以写这样的动态查询:

SELECT QUOTENAME(OBJECT_SCHEMA_NAME(p.[object_id])) + '.' + QUOTENAME(p.name) 
FROM sys.procedures AS p OUTER APPLY 
sys.dm_exec_describe_first_result_set_for_object(p.[object_id], 1) AS d 
WHERE d.name IS NULL; 

的一个问题是,如果你的程序有任何在它的分支依赖于输入参数,一天的时间,系统状态,表中的数据,等等。那么它可能无法准确反映它的功能。但它可能有助于减少名单。它也可能返回存储过程的误报,这些存储过程插入到表中并使用SELECT返回标识值。

在早期版本中,您可以使用与SET FMTONLY ON类似的操作,但在这种情况下,您将必须执行所有的过程,而且这样做会很麻烦,因为您还需要了解任何必需的参数进出)并相应地设置。评估过程更为人性化,并且仍然倾向于上述参数问题。

你现在用什么方法达到85%?一旦你有两个(或三个)列表,你将如何处理这些信息?

我真的不能看到任何捷径。在一个理想的世界里,你的命名约定会规定存储过程应该被准确地命名为他们所做的事情,并且你应该能够立即区分它们(有些是临界的)。就目前来看,你似乎正在看交通摄像头,并试图确定哪些车可能在驾驶座下有枪。

+0

可能想在其中添加“DROP”和“ALTER”。我添加了'CREATE',因为他可能也想包含索引。 – JNK 2012-02-23 21:37:09

+0

@JNK谢谢,我不是说它是一个详尽的列表。我认为他已经有了一份名单,让他达到85%。 – 2012-02-23 21:48:49

+0

@AaronBertrand - 我更新了我的问题与我的所作所为 – Ryk 2012-02-28 04:52:15

2

有一些关键词,你可以检查sys.sql_modules

  • UPDATE
  • INSERT
  • INTO
  • DELETE
  • CREATE
  • DROP
  • ALTER
  • TRUNCATE

如果它不包含任何这些,我不能想办法它通过另一个proc子或函数(它将包含一个,除非其写入数据库的话)。

然后,您需要逐个检查,以确保它不是#temp表格。您还需要执行第二遍以继续查找包含其他对象中的对象的对象。

+1

添加为评论,所以我不是从aaron窃取它也'输出',我忘了那一个 – JNK 2012-02-23 21:40:25

+1

小偷!投降你的upvotes! – 2012-02-23 22:15:41

-1

一个彻底的解决方案是解析所有过程,并将一个调用插入到在第一行创建数据库快照的函数。最后一行会创建另一行,并将其与第一行进行比较。如果它们不同,你打电话给写程序。当然,你不能在生产环境中这样做,你需要调用你所有的测试用例,或者重播一个sql-server日志。

我不会想太久,这个...

+0

哇。这对我来说似乎不是一个好主意。你甚至可以开始解释你将如何构建包含所有必需参数的呼叫列表?您如何知道参数值以确保程序正确执行?你有什么样的系统可以创建数千个快照? – 2012-02-23 21:49:24

+0

我见过记录每次调用sql-server的工具,只是忘了它叫什么。离开那几天/周,你有一个很好的测试集。但我提到它是激进的,不值得太多的想法,所以我们不要,我已经得到我的鞭for提出这样的疯狂。 – mindandmedia 2012-02-23 21:55:20

+0

您可以重播跟踪(使用事件探查器或第三方工具),或者在SQL Server 2012中使用分布式重播实用程序,但我仍然认为这项工作中极其不可能的部分是您建议的主旨:(a)做出假设根据跟踪期间使用的参数以及(b)生成和比较数千个快照。 – 2012-02-23 22:11:53

相关问题