05 августа 2020

Парсинг SQL запросов

    Весной передо мной поставили задачу оптимизации большой старой базы данных. Собранная с помощью sp_BlitzCache статистика содержала тысячи однотипных запросов, и для одного из графиков нагрузки потребовалось объединить их количественную статистику. Для этого однотипные запросы нужно было "нормализовать" - заменить конкретные значения на символ параметра (например, "LIKE 'xzy%'" заменить на "LIKE '?%'" или "CONVERT(DATETIME,'01/01/2020 00:00',101)" заменить на "CONVERT(DATETIME,?,101)"...), убрать переводы строк, убрать лишние пробелы/табы... То есть моя задача - разобрать запрос на токены, почистить лишнее и собрать его снова.
    Пол дня я искал и пробовал бесплатные парсеры SQL. Но это было потраченное в пустую время. Все найденные мной парсеры оказались значительно хуже парсера который есть у Delphi в модуле Data.DBCommon. Процедура для разбора SQL запроса на отдельные токены с его помощью имеет всего несколько строк:
procedure ParseSQL(const sSQL: String);
var
  cTokenStart: PChar;
  sToken: String;
  Token, CurSection: TSQLToken;
  IdOption: IDENTIFIEROption;
begin
  cTokenStart := PWideChar(sSQL);
  CurSection  := stUnknown;
  IdOption := idMixCase;
  repeat
    Token := NextSQLTokenEx(cTokenStart, sToken, CurSection, IdOption);
    CurSection := Token;
    WriteLn(GetEnumName(TypeInfo(TSQLToken), Integer(Token)), ' ', sToken);
  until Token = stEnd;
end;
Например, если ему передать такой запрос:
SELECT t1.field1, Field2, t2.FIELD3 
  FROM Table1 t1 inner join TaBle2 t2 
    on t2.ID = t1.Id
 where f4 in (65, 123, 124)
То получим перечень токенов и их типов:
stSelect SELECT
stTableName t1
stFieldName field1
stFieldName Field2
stTableName t2
stFieldName FIELD3
stFrom FROM
stTableName Table1
stFieldName t1
stFieldName inner
stFieldName join
stFieldName TaBle2
stFieldName t2
stFieldName on
stTableName t2
stFieldName ID
stPredicate =
stTableName t1
stFieldName Id
stWhere where
stFieldName f4
stFieldName in
stNumber 65
stNumber 123
stNumber 124
stFieldName )
stEnd
    В поисках специальной библиотеки для парсинга SQL-запроса я совсем забыл об используемой мной библиотеке для доступа к базам данных - Universal Data Access Components (UniDAC). Оказалось, что у нее есть свой парсер для каждой реализованной в ней СУБД (модули *ParserUni). Процедура для разбора SQL запроса на отдельные токены с модулем MSParserUni получается еще проще и понятнее:
procedure ParseSQL(const sSQL: String);
var
  Parser: TMSParser;
begin
  Parser := TMSParser.Create(sSQL);
  try
    Parser.AdvancedStringParsing := True;
    while Parser.GetNextToken <> 0 do
      Writeln(Parser.Token, ' ', Parser.Lexem);
  finally
    Parser.Free;
  end;
end;
Проверим ее на тестовом запросе:
147 SELECT
-103 t1
14 .
-103 field1
12 ,
-103 Field2
12 ,
-103 t2
14 .
-103 FIELD3
116 FROM
-103 Table1
-103 t1
120 INNER
125 JOIN
-103 TaBle2
-103 t2
132 ON
-103 t2
14 .
-103 ID
19 =
-103 t1
14 .
-103 Id
155 WHERE
-103 f4
-103 in
8 (
-105 65
12 ,
-105 123
12 ,
-105 124
9 )
Как вы видите, парсер UniDAC предоставляет еще более подробную информацию о содержимом SQL запроса. Составить запрос снова на основании этой информации не составит труда. Минимальный текст процедуры, который составляет запрос из отдельных токенов может выглядеть так:
procedure ParseSQL(const sSQL: String);
var
  Parser: TMSParser;
  iPrevToken: Integer;
  sNewSQL: String;
begin
  iPrevToken := 0;
  Parser := TMSParser.Create(sSQL);
  try  
    Parser.AdvancedStringParsing := True;
    while Parser.GetNextToken <> 0 do
     begin
       Writeln(Parser.Token, ' ', Parser.Lexem);
       if iPrevToken = 0
         then sNewSQL := Parser.Lexem
         else case Parser.Token of
               -106: sNewSQL := sNewSQL + ' ' 
                                        + QuotedStr(Parser.Lexem);//string
               12,  //,
               14,  //.
               9    //)
                   : sNewSQL := sNewSQL + Parser.Lexem;
               else if (iPrevToken = 14) or //.
                       (iPrevToken = 8 ) or // (
                       (iPrevToken = 22) //@
                      then sNewSQL := sNewSQL + Parser.Lexem
                      else sNewSQL := sNewSQL + ' ' + Parser.Lexem;
       end;
       iPrevToken := Parser.Token;
     end;
  finally
    Parser.Free;
  end;
  writeLn(sNewSQL);
end;
Скормив этой процедуре тестовый запрос, приведенный выше, мы на выходе получим его полную копию, только без переводов строки.
    Как вы поняли, мой выбор - TMSParser. Используя его я легко написал свой класс, который смог мне распарсить и нормализовать даже сложные SQL запросы.

2 комментария:

  1. Судя по тому, как ты каждый раз нахваливаешь UniDAC при наличии в Delphi встроенного FireDAC, складывается впечатление, что статьи заказные ;-)

    ОтветитьУдалить
  2. Или может что FireDAC очень плох?

    ОтветитьУдалить