Весной передо мной поставили задачу оптимизации большой старой базы данных. Собранная с помощью sp_BlitzCache статистика содержала тысячи однотипных запросов, и для одного из графиков нагрузки потребовалось объединить их количественную статистику. Для этого однотипные запросы нужно было "нормализовать" - заменить конкретные значения на символ параметра (например, "LIKE 'xzy%'" заменить на "LIKE '?%'" или "CONVERT(DATETIME,'01/01/2020 00:00',101)" заменить на "CONVERT(DATETIME,?,101)"...), убрать переводы строк, убрать лишние пробелы/табы... То есть моя задача - разобрать запрос на токены, почистить лишнее и собрать его снова.
Пол дня я искал и пробовал бесплатные парсеры SQL. Но это было потраченное в пустую время. Все найденные мной парсеры оказались значительно хуже парсера который есть у Delphi в модуле Data.DBCommon. Процедура для разбора SQL запроса на отдельные токены с его помощью имеет всего несколько строк:
Как вы поняли, мой выбор - TMSParser. Используя его я легко написал свой класс, который смог мне распарсить и нормализовать даже сложные SQL запросы.
Пол дня я искал и пробовал бесплатные парсеры 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)
В поисках специальной библиотеки для парсинга SQL-запроса я совсем забыл об используемой мной библиотеке для доступа к базам данных - Universal Data Access Components (UniDAC). Оказалось, что у нее есть свой парсер для каждой реализованной в ней СУБД (модули *ParserUni). Процедура для разбора SQL запроса на отдельные токены с модулем MSParserUni получается еще проще и понятнее: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
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;Проверим ее на тестовом запросе:
Как вы видите, парсер UniDAC предоставляет еще более подробную информацию о содержимом SQL запроса. Составить запрос снова на основании этой информации не составит труда. Минимальный текст процедуры, который составляет запрос из отдельных токенов может выглядеть так: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 )
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 запросы.
Судя по тому, как ты каждый раз нахваливаешь UniDAC при наличии в Delphi встроенного FireDAC, складывается впечатление, что статьи заказные ;-)
ОтветитьУдалитьИли может что FireDAC очень плох?
ОтветитьУдалить