Если вы разбираетесь «почему тормозит база» и у вас есть трейс, созданный MS SQL profiler, то что вы делаете первым делом? Правильно, сохраняете его в таблицу, чтобы поразбираться с ним с помощью родного SQL, а не в GUI.
Очень хотелось бы сделать group by TextData, но увы — так не получится из-за разных параметров у процедур и кверей. А выразительных способностей SQL не хватет, чтобы эффективно ‘нормализовать’ трейс.
Но ведь можно скрестить ежа и ужа, SQL и Python, и решить задачу в несколько строк! Полезные скрипты ниже
Итак, какие преобразования нам хотелось бы сделать?
Общую нормализацию (много пробелов подряд, табуляции и переводы строк).
Далее, заменим числа (как целые, так и hex) на #. А все строки заменим на ‘~’, например:
select 1,'alpha',0x478ddaaee,X,N'str' from TAB -- превратится в select #,'~',#,X,N'~' from TAB
Это не решает проблему списков разной длины:
select * from TAB where id in (1,2,3) select * from TAB where id in (5,6,7,8) -- получится select * from TAB where id in (#,#,#) select * from TAB where id in (#,#,#,#)
Поэтому заменим списки чисел на <nlist>, а строк на <slist>, но только если до списка есть хоть одна открывающая скобка, иначе это список параметров процедуры:
exec prc 1,2,3,'alpha','beta' select * from TAB where id in (5,6,7,8) select * from STAB where names in ('alpha','beta') -- получится exec prc #,#,#,'~','~' select * from TAB where id in (<nlist>) select * from STAB where names in (<slist>)
Обратите внимание, что мы не приводим параметры процедуры к списку.
Далее, для sp_executesql и sql_cursorprepexec мы ‘выкусываем’ сам оператор, который потом обрабатывается по общим правилам:
exec sp_executesql N'SELECT * FROM TAB where CAT=N''X'' and id=@p__linq__0' ,N'@p__linq__0 int',@p__linq__0=725 -- получится SELECT * FROM TAB WHERE CAT=N'~' and id=@p__linq__0
Наконец, часто встречаются временные таблицы с именами, сгенеренными автоматически:
select * INTO [#sells_a5250e98_2ec7_495a_abe2_e314d0a9b5e6] from X drop table [#f42350e3_4aa3_1234_25e4_e314d0adf3e2] -- получится select * INTO [#sells...] from X drop table [#G]
В первом случае имя усекается по первому подчеркиванию, во втором, когда все имя — GUID, остается #G
А теперь сами скрипты
Вначале подготовка, должно быть включено:
EXECUTE sp_configure 'external scripts enabled', 1; GO RECONFIGURE; GO
Далее создадим табличку в базе, где мы сохраняем трейсы. Назовем ее T. Список колонок вы можете пополнять по своему вкусу.
create table T (rowNumber int, TextData varchar(max), CPU int, Reads int, Writes int, Duration int)
А теперь главное. Обратите внимание, что в коде Python все одинарные кавычки удвоены, так как вы передаете это из SQL:
set nocount on truncate table T insert into T EXECUTE sp_execute_external_script @language = N'Python' , @script = N' import re def norm(s): # initial cleanup s = s.replace("\n", " ").replace("\r", " ").replace("\t", " ") # eols & tabs s = s.replace(" ", " ").strip() s = re.sub("\s+"," ",s)+" " # get rid of multiple spaces, last space for parsing numbers at the end # open sql_executesql if (s[0:19].casefold() == "exec sp_executesql "): s = s[21:] s = re.sub("(.*?[^''])''.*\Z","\\1",s) # leftmost quote which is not double quote # sp_cursorprepexec prep = re.compile(".*\Wexec sp_cursorprepexec\W", re.IGNORECASE) if prep.match(s): s = s.rsplit(",N''",1)[1] # extract statement after N s = re.sub("(.*?[^''])''.*\Z","\\1",s) # leftmost quote which is not double quote s = "sp_cursorprepexec " + s.replace("''''","''") + " " s = re.sub("''''", "",s) # replace double single quotes with nothing s = re.sub("(\\W)#(\w{36,36})(\\W)", "\\1#G\\3", s) # replace #<guid name> with #G s = re.sub("(\\W)(#[^\W_]+)(\w+)(\\W)", "\\1\\2...\\4", s) # replace unique temp table names #tab_12345 with #tab... # replace any number with # loop because , is eaten by last \W so some values are skipped while True: new = re.sub("(\\W)(\\d+)(\\W)", "\\1#\\3", s) if new == s: break s = new s = re.sub("-#", "#", s) # negative numbers # hex numbers while True: new = re.sub("(\\W)(0x[0-9A-Fa-f]+)(\\W)", "\\1#\\3", s) if new == s: break s = new s = re.sub("(\'')(.*?)(\'')", "''~''", s) # replace any string with ''~'' # remove spaces around , so lists can be always collapsed s = re.sub("\s,", ",", s) s = re.sub(",\s", ",", s) # collapse numeric lists, there must be at least one ( before the list, otherwise these are parameters while True: new = re.sub("(\(.*?)#,#", "\\1<nlist>", s) if new == s: break s = new while True: new = re.sub("<nlist>,<nlist>", "<nlist>", s) if new == s: break s = new s = re.sub("<nlist>,#", "<nlist>", s) # collapse string lists while True: new = re.sub("(\(.*?)''~'',''~''", "\\1<slist>", s) if new == s: break s = new while True: new = re.sub("<slist>,<slist>", "<slist>", s) if new == s: break s = new s = re.sub("<slist>,''~''", "<slist>", s) return s OutputDataSet = InputDataSet n = 0 for r in OutputDataSet["RowNumber"]: s = OutputDataSet["TextData"][n] OutputDataSet["TextData"][n] = norm(s) n += 1 ', @input_data_1 = N'SELECT RowNumber,convert(nvarchar(max),TextData) as TextData, CPU,Reads,Writes,Duration/1000 as Duration FROM trc where textData is not null'
Особое внимание на последние две строчки: замените trc на название вашей таблицы с трейсом, и не забудьте добавить сюда еще колонки, если добавляли из в таблицу T. Duration приводится к миллисекундам из микросекунд.
Сама группировка тривиальна:
declare @totalCPU float, @totalReads float, @totalWrites float, @totalDuration float select @totalCPU=sum(CPU), @totalReads=sum(Reads), @totalWrites=sum(Writes), @totalDuration=sum(Duration) from T select count(*) as cnt, sum(CPU) as CPU,sum(Reads) as Reads, sum(Writes) as Writes, sum(Duration) as Duration, sum(CPU)/(@totalCPU+0.001) as PctCPU, sum(Reads)/(@totalReads+0.001) as PctReads, sum(Writes)/(@totalWrites+0.001) as PctWrites,sum(Duration)/(@totalDuration+0.001) as PctDuration, TextData from T group by TextData order by 1 desc
Выводим количество операторов и суммы CPU, Reads, Writes, Duration как есть и в процентах к полному трейсу. В конце ORDER BY по вкусу.
Скорость обработки составляет около 120 строк в секунду, так что на трейс из миллиона записей у вас уйдет около трех часов. Наверное, это можно ускорить — но зачем? Пока обрабатывается трейс, вы сможете погулять или посмотреть фильм не коря себя тем, что вы якобы ничего не делаете.
И еще полезность
Мне приходилось исследовать зависимость скорости выполнения процедуры от параметров. Например, в трейсе идут записи:
exec MyReport @from='2022-01-01',@to='2022-01-20',@flag='all' exec MyReport @from='2020-01-01',@to='2022-01-20',@flag='some' exec MyReport @from='2022-01-20',@to='2022-01-20',@flag='all'
Некоторые вызовы были куда медленнее чем обычно, и я предположил, что дело в интервале времен, которые обрабатывает отчет. Поможет функция:
create function [dbo].[getpar] (@par varchar(32), @t varchar(max)) returns varchar(8000) as begin declare @i int set @i = charindex('@'+@par,@t) if @i < 0 return '' set @t=substring(@t,@i+len(@par)+2,8000) if left(@t,1)='N' set @t=substring(@t,2,8000) if left(@t,1)='''' set @t=substring(@t,2,8000) set @i = charindex(',@',@t) if @i > 0 set @t=substring(@t,1,@i-1) if left(reverse(@t),1)='''' set @t=substring(@t,1,len(@t)-1) return @t end
Теперь вы можете проанализировать вызовы так:
select 1+datediff(dd, convert(datetime,dbo.getpar('from',TextData)), convert(datetime,dbo.getpar('to',TextData))) as days , CPU,Reads,Writes,Duration/1000 as Duration from MyTrace where TextData like 'exec%MyReport%'
Теперь можно проверить гипотезу, построив Excel X-Y scatter diagram (X-days, Y-Duration или CPU)
ссылка на оригинал статьи https://habr.com/ru/post/647449/
Добавить комментарий