Страницы

суббота, 26 июля 2014 г.

Автоматическое логирование значений параметров процедур

При запусках хранимых часто возникает необходимость логирования их работы. Для отладки часто требуется также логировать информацию о том, с какими параметрами запускается хранимая процедура. Я уже писал о логировании в статье Лог работы sql-операторов. Однако там использовался профайлер. Если использовать эту методику, то для определения значений параметров, с которыми запускалась процедура, понадобится выполнить парсинг логируемого кода.
Есть компактный и единообразный способ логирования параметров, с которыми запускается процедура. В начале тела процедуры нужно будет наполнить временную таблицу значениями параметров процедур и вызвать специальную процедуру, которая будет записывать их лог. Процедуру логирования можно будет использовать в любой базе данных. Для начала создадим таблицы. У нас будут такие сущности: "Процедуры", "Параметры", "Запуски", "Лог".
create table dbo.Procs
(
    iProcId      int identity ( 1, 1 )      not null,
    vcProcName   nvarchar ( 450 )           not null,
    constraint PK_Procs_iProcId primary key clustered ( iProcId asc ) on [PRIMARY],
    constraint AK_Procs_vcProcName unique nonclustered ( vcProcName asc ) on [PRIMARY]
) on [PRIMARY]
go

create table dbo.Prms
(
    iProcId int                        not null,
    iPrmId  int identity ( 1, 1 )      not null,
    vcPrm   nvarchar ( 200 )           not null,
    constraint PK_Prms_iProcId_iPrmId primary key clustered ( iProcId asc, iPrmId asc ) on [PRIMARY],
    constraint AK_Prms_iProcId_vcPrm unique nonclustered ( iProcId asc, vcPrm asc ) on [PRIMARY],
    constraint FK_Prms_iProcId_Procs_iProcId foreign key ( iProcId ) references dbo.Procs ( iProcId ) on update cascade on delete no action
) on [PRIMARY]
go

create table dbo.ExecLog
(
    iProcId      int                    not null,
    iExecId      int identity ( 1, 1 )  not null,
    dtDate       datetime               not null
    constraint DF_ExecLog_dtDate default getdate (),
    constraint PK_ExecLog_iProcId_iPrmId_iExecId primary key clustered ( iProcId asc, iExecId asc ),
       constraint FK_ExecLog_iProcId_Procs_iProcId foreign key ( iPRocId ) references dbo.Procs ( iProcId ) on update cascade on delete no action
) on [PRIMARY]
go

create table dbo.PrmsLog
(
    iProcId      int          not null,
    iExecId      int          not null,
    iPrmId       int          not null,
    varPrmVal    sql_variant  null,
    constraint PK_PrmsLog_iProcId_iPrmId_iExecId primary key clustered ( iProcId asc, iExecId asc, iPrmId asc ),
    constraint FK_PrmsLog_iProcId_iExecId_ExecLog_iProcId_iExecId foreign key ( iProcId, iExecId )
        references dbo.ExecLog ( iProcId, iExecId ) on update cascade on delete no action,
    constraint FK_PrmsLog_iProcId_iPrmId_Prms_iProcId_iPrmId foreign key ( iPRocId, iPrmId )
        references dbo.Prms ( iProcId, iPrmId ) on update no action on delete no action
) on [PRIMARY]

go

Теперь напишем код процедуры логирования:
if sessionproperty ( N'quoted_identifier' ) = 0
       set quoted_identifier on
go
if sessionproperty ( N'ansi_nulls' ) = 0
       set ansi_nulls on
go
if object_id ( N'dbo.LogProcsPrm', N'P' ) is null
       exec ( N'create proc dbo.LogProcsPrm as return 1' )
go
alter proc dbo.LogProcsPrm
(
       @iDbId       int,
       @iDbProcId   int
)
as
begin
       set nocount, xact_abort on

       declare @vcProcName nvarchar ( 600 ), @iProcId int, @vcPrm nvarchar ( 200 ), @varPrmVal sql_variant, @sql nvarchar ( max ), @iPrmId int, @iExecId int

       if object_id ( N'tempdb..#DbPrms', N'U' ) is not null
             drop table #DbPrms
       create table #DbPrms
       (
             iItemId int identity ( 1, 1 )     not null,
             vcPrm  nvarchar ( 200 )           not null,
             primary key clustered ( iItemId asc ) on [PRIMARY],
             unique nonclustered ( vcPrm asc ) on [PRIMARY]
       ) on [PRIMARY]

       set @sql = '
             insert into #DbPrms ( vcPrm )
                    select name
                    from ' + quotename ( db_name ( @iDbId ) ) + '.sys.parameters
                    where [object_id] = ' + cast ( @iDbProcId as nvarchar ( 100 ) ) + '
       '
       exec sp_executesql @sql
      
       set @vcProcName = quotename ( db_name ( @iDbId ) ) + '.' + quotename ( object_schema_name ( @iDbProcId, @iDbId ) ) + '.' + quotename ( object_name ( @iDbProcId, @iDbId ) )
      
       set @iProcId = ( select iProcId
             from dbo.Procs
             where vcProcName = @vcProcName
       )
       if @iProcId is null
       begin
             insert into dbo.Procs ( vcProcName )
                    values ( @vcProcName )
             set @iProcId = scope_identity ()
       end

       insert into dbo.Prms ( iProcId, vcPrm )
             select @iProcId, dbprm.vcPrm
             from
                    #DbPrms dbprm
                           left outer join
                    dbo.Prms prm on
                           dbprm.vcPrm = prm.vcPrm and
                           prm.iProcId = @iProcId
             where prm.iPrmId is null
      
       insert into dbo.ExecLog ( iProcId )
             values ( @iProcId )
       set @iExecId = scope_identity ()

       declare cur cursor local static forward_only for
             select dbprm.vcPrm, col.varPrmVal, prm.iPrmId
             from
                    #DbPrms dbprm
                           inner join
                    #prmCol col on dbprm.iItemId = col.iItemId
                           inner join
                    dbo.Prms prm on dbprm.vcPrm = prm.vcPrm and prm.iProcId = @iProcId
       open cur
       fetch next from cur into @vcPrm, @varPrmVal, @iPrmId
       while @@fetch_status = 0
       begin
             set @sql = 'insert into dbo.PrmsLog ( iProcId, iExecId, iPrmId, varPrmVal ) values ( ' + cast ( @iProcId as nvarchar ( 100 ) ) + ', ' +
                    cast ( @iExecId as nvarchar ( 100 ) ) + ', ' + cast ( @iPrmId as nvarchar ( 100 ) ) + ', @varPrmVal )'
             exec sp_executesql @sql, N'@varPrmVal sql_variant', @varPrmVal = @varPrmVal

             fetch next from cur into @vcPrm, @varPrmVal, @iPrmId
       end
       close cur
       deallocate cur
end
go

Процедура предполагает, что создана таблица #prmCol, в которой есть столбец типа sql_variant для хранения значений параметров. Процедура определяет значения идентификатора процедуры и ее параметров из таблиц dbo.Procs и dbo.Prms. При необходимости в эти таблицы делаются вставки для ссылочной целостности. В таблицу ExecLog делается вставка записи о выполнении процедуры. Затем параметры перебираются в цикле, на каждой итерации которого делается вставка значения параметра в лог. Для перебора параметров используется временная таблица #DbPrms, которая наполняется на основе каталога sys.parameters в базе данных, которой принадлежит процедура, параметры которой логируются.

Рассмотрим теперь такую тестовую процедуру:
create proc dbo.test
(
       @prm   int,
       @data  datetime
)
as
begin

       select @prm, @data
end
go

Для логирования значений параметров этой процедуры при ее запусках, добавим в ее тело такой код:
alter proc dbo.test
(
       @prm   int,
       @data  datetime
)
as
begin
       declare @prmCol dbo.ProcPrm, @iDbId int = db_id ()
       if object_id ( N'tempdb..#prmCol', N'U' ) is not null drop table #prmCol
       create table #prmCol ( iItemId int identity ( 1, 1 ) not null, varPrmVal sql_variant null,
             primary key clustered ( iItemId asc ) on [PRIMARY] ) on [PRIMARY]
       insert into #prmCol ( varPrmVal )
             select *
             from ( values ( cast ( @prm as sql_variant ) ), ( cast ( @data as sql_variant ) ) ) data ( varPrmVal )
       exec TestDb.dbo.LogProcsPrm @iDbId, @@procid

       select @prm, @data
end
go

В начале тела процедуры создается временная таблицу, в нее кладутся значения параметров. В этом месте их требуется единственный раз перечислить, после чего идет вызов логирующей процедуры.
Для просмотра лога создадим индекс на dbo.ExecLog:
create nonclustered index IX_dtDate_iProcId_iExecId on dbo.ExecLog ( dtDate asc, iProcId asc, iExecId asc )

Теперь можно создать табличную функцию с помощью которой удобно изучать лог для определенной процедуры и за определенный период:
create function dbo.GetProcPrmLog
(
       @vcProcName         nvarchar ( 600 ),
       @dtStartDate datetime,
       @dtEndDate          datetime
)
       returns table
as
       return
             select prm.vcPrm, exlog.dtDate, prlog.varPrmVal
             from
                    dbo.Procs procs
                           inner join
                    dbo.Prms prm on procs.iProcId = prm.iProcId
                           inner join
                    dbo.ExecLog exlog on procs.iProcId = exlog.iProcId
                           inner join
                    dbo.PrmsLog prlog on
                           exlog.iProcId = prlog.iProcId and
                           exlog.iExecId = prlog.iExecId and
                           prm.iPrmId = prlog.iPrmId
             where procs.vcProcName = @vcProcName and exlog.dtDate between @dtStartDate and @dtEndDate

пятница, 25 июля 2014 г.

Колоночные индексы.

В MS SQL 2012 появился новый вид индексов - колоночные индексы. Эти индексы помогают лучше оптимизировать запросы некоторых видов к таблицам с большим числом строк. Колоночные индексы помогают оптимизировать запросы, в которых используется группировка и агрегирующие функции. А также запросы к таблицам с большим числом столбцов, в случае, когда в запросе используется значительно меньший набор столбцов. Данный вид индексов полезен при разработке хранилищ данных, в которых есть громадные таблицы, используется схема данных типа звезда.

Почему же колоночные индексы так называются? Данные в этих индесах хранятся не построчно, а векторно. Представим, что у нас есть таблица со столбцами t1, t2, t2, со строками Row1, Row2, Row3, Row4. В обычном индексе эти столбцы будут храниться так:

[ Row1 (t1, t2, t3) ],  [ Row2 (t1, t2, t3) ],  [ Row3 (t1, t2, t3) ],  [ Row4 (t1, t2, t3) ]

Это классический пример построчного хранения данных. В частности, если в запросе требуется вернуть столбцы t1, t3, то столбец t2 все равно приходится читать. Он между t1 и t3.

В случае, если столбцы t1, t2, t3 входят в колоночный индекс, то хранение данных будет таким:

[(Row1, t1), (Row2, t1), (Row3, t1), (Row4, t1)], [(Row1, t2), (Row2, t2), (Row3, t2), (Row4, t2)], [(Row1, t3), (Row2, t3), (Row3, t3), (Row4, t3)]

Понятно, что если требуется читать не все столбцы таблицы, а только часть из них, то при работе с колоночным индексом не требуется делать лишних операций чтения. В результате на больших объемах скорость многих запросов может вырастать на порядок. Продемонстрируем это на примере. Создадим таблицу:
if object_id ( N'dbo.DataVal', N'U' ) is not null
begin
       ; throw 50000, 'Таблица dbo.DataVal уже существует в базе данных.', 1
       return
end

create table dbo.DataVal
(
       iItemId int identity ( 1, 1 )     not null,
       dtDate date                       not null,
       vcVal1 money                      not null,
       vcVal2 money                      not null,
       vcVal3 money                      not null,
       vcVal4 money                      not null,
       constraint PK_Data_iItemId primary key clustered ( iItemId asc ) on [PRIMARY]
) on [PRIMARY]

Наполним таблицу тестовыми данными:
insert into dbo.DataVal with ( tablock ) ( dtDate, vcVal1, vcVal2, vcVal3, vcVal4 )
       select
             dateadd ( [dd], abs ( binary_checksum ( newid () ) ) / power ( 10, 8 ), '2014-01-01' ),
             abs ( binary_checksum ( newid () ) ) / power ( 10., 5 ),
             abs ( binary_checksum ( newid () ) ) / power ( 10., 5 ),
             abs ( binary_checksum ( newid () ) ) / power ( 10., 5 ),
             abs ( binary_checksum ( newid () ) ) / power ( 10., 5 )
       from
             master.dbo.spt_values tab1
                    cross join
             master.dbo.spt_values tab2
go 2

Изучим производительность такого запроса:
select dtDate, max ( vcVal1 ), max ( vcVal2 )
from dbo.DataVal
group by dtDate

Сперва создадим обычный некластерный индекс:
create nonclustered index IX_dtDate_vcVal1_incl_vcVal2 on dbo.DataVal ( dtDate asc, vcVal1 asc ) include ( vcVal2 ) on [PRIMARY]

Запустим запрос, очистив предварительно буферный пул:
dbcc dropcleanbuffers ()
select dtDate, max ( vcVal1 ), max ( vcVal2 )
from dbo.DataVal with ( index ( IX_dtDate_vcVal1_incl_vcVal2 ) )
group by dtDate

На моем домашнем компьютере запрос отработал за 6 секунд. Теперь создадим колоночный индекс:
create columnstore index IX_clst_dtDate_vcVal on dbo.DataVal ( dtDate, vcVal1, vcVal2 ) on [PRIMARY]

Из схемы хранения данных в колоночном индексе понятно что все столбцы в нем равноправны, поэтому неприменимы такие понятия как сортировка столбца или включенные столбцы. Снова запустим запрос, задав в хинте уже колоночный индекс:
dbcc dropcleanbuffers ()
select dtDate, max ( vcVal1 ), max ( vcVal2 )
from dbo.DataVal with ( index ( IX_clst_dtDate_vcVal ) )
group by dtDate

Время работы составило 2 секунды. Скорость выросла в 3 раза. Посмотрим теперь, что будет, если запускать эти запросы постоянно в ситуации, когда в буферном пуле уже достаточно данных:
set statistics io on
go

select getdate ()

select dtDate, max ( vcVal1 ), max ( vcVal2 )
from dbo.DataVal with ( index ( IX_dtDate_vcVal1_incl_vcVal2 ) )
group by dtDate

select getdate ()

select dtDate, max ( vcVal1 ), max ( vcVal2 )
from dbo.DataVal with ( index ( IX_clst_dtDate_vcVal ) )
group by dtDate

select getdate ()

set statistics io off
go

Первый запрос отработал за 717 миллисекунд, второй - за 47 миллисекунд, то поиск с помощью колоночного индекса теперь работает в 15 раз быстрее! Статистика ввода-вывода показала такие результаты:
Table 'DataVal'. Scan count 5, logical reads 39852, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

Table 'DataVal'. Scan count 4, logical reads 25442, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

Мы видим, что для колоночного делается на одно сканирование меньше, число логических чтений также меньше. Посмотрим на план выполнения запроса с колоночным индексом:








Видно, что сканирование индекса выоплняется в пакетном режиме. Это также новинка MS SQL 2012. Поскольку хранение данных колоночное, то за раз сервер может считывать данные, относящиеся к большему числу строк.

В MS SQL 2012 у колоночных индексов есть существенный недостаток: таблицы с такими индексами нельзя обновлять. Индекс приходится удалять, делать обновление, а затем создавать заново колоночный индекс. Это ограничение было преодолено в MS SQL 2014. В новой версии MS SQL Server таблицы с колоночными индексами уже обновляемые. Появились и другие улучшения. В колоночные индексы можно включать столбцы с типами данных, которые ранее не поддерживались. Например, снято ограничение, при котором точность столбца типа decimal в индексе не могла превышать 18. Можно включать в индекс столбец типа uniqueidentifier. Для большего класса запросов доступна пакетная обработка. Колоночные индексы могут быть кластерными и некластерными. Для создания кластерного колоночного индекса приняется такой синтаксис:
create clustered columnstore index IX on dbo.DataVal on [PRIMARY]

Здесь нет перечня столбцов. Это связано с тем, что с одной стороны все столбцы в индексе равноправны, а с другой стороны кластерный индекс должен описывать всю таблицу. То есть в такой индекс включены все столбцы.

Для кластерного колоночного индекса автоматически применяется специальный тип сжатия, поэтому такая таблица будет занимать меньше места. При обновлениях кластерного колоночного индекса его структура не поддерживается "на лету", чтобы не замедлять операции insert, update, delete. Вместо этого создаются 2 дополнительные структуры: хранилище дельт для учета операций insert и битовая карта удалений для учета операций delete. А операция update разделяется на insert и delete, которые выполняются одновременно. Операции, читающие индекс, учитывают наличие этих хранилищ. На сервере есть фоновый процесс, который постепенно выправляет структуру колоночного индекса, выполняя слияние хранилища дельт и карты удалений. Это важно, чтобы колоночный индекс не вырастал в размерах и чтобы запросы на извлечение данных не замедлялись. Также для колоночных индексов введен новый тип сжатия, который еще сильнее уменьшает таблицу в размерах. Его можно применять как на уровне всей таблицы, так и на уровне отдельных секий, если таблица секционирована. Сжатие делается так:
alter index IX on TabName rebuild with ( data_compression = columnstore_archive )

Для того, чтобы привести индекс к прежней степени сжатия, надо выполнить такой код:
alter index IX on TabName rebuild with ( data_compression = columnstore )

Такой вид сжатия серьезно замедлит операции чтения и записи. Поэтому применять его нужно для старых данных, которыми редко кто пользуется.