Страницы

понедельник, 30 июня 2014 г.

Подписывание хранимой процедуры сертификатом уровня сервера

В этой статье речь пойдет о том как дать пользователям доступ на выполнение процедур, которые для своей работы требуют мощных серверных разрешений, не давая при этом этих разрешений пользователям. Предположим, что мы разрабатываем базу данных, в которой высокоприоритетные соединения могут блокироваться неким низкоприоритетным процессом. И в таких ситуациях требуется делать kill для этого низкоприоритетного процесса, то есть отключать мешающее соединение. Предположим задача на отключение соединений возложена на младшего разработчика. Причем по реглементу ему нельзя предоставлять ни прав администратора, ни доступа на отключение всех соединений. Для сотрудника созданы такие аутентификационные данные:
create login testUser with password = 'testdata', check_policy = off, default_database = Data
create user testUser from login testUser with default_schema = dbo

Напишем для решения задачи такую процедуру:
use Data
go

if sessionproperty ( N'quoted_identifier' ) = 0
       set quoted_identifier on
go

if sessionproperty ( N'ansi_nulls' ) = 0
       set ansi_nulls on
go

create proc dbo.KillWrongSessions
as
begin
       set nocount, xact_abort on

       declare @sess int, @sql nvarchar ( max )
       declare cur cursor local static forward_only for
             select block.session_id
             from
                    sys.dm_exec_requests sess
                           inner join
                    sys.dm_exec_sessions block on sess.blocking_session_id = block.session_id
             where sess.blocking_session_id <> 0 and block.[host_name] = 'EDYNAK'
       open cur
       fetch next from cur into @sess
       while @@fetch_status = 0
       begin
             set @sql = 'kill ' + cast ( @sess as nvarchar ( 100 ) )
             --print @sql
             exec sp_executesql @sql
             fetch next from cur into @sess
       end
       close cur
       deallocate cur
end
go
grant exec on object::dbo.KillWrongSessions to testUser
use master
grant view server state to testUser

В процедуре происходит отключение соединений, которые кого-либо блокируют и при этом работают на определенном хосте. Это и есть наш критерий низко-приоритетного соединения.

Смоделируем ситуацию когда происходит блокирование. Создим таблицу:
create table dbo.Data ( i int )
На компьютере с именем EDYNAK запустим такой код:
begin tran
insert into dbo.Data ( i )
       select 1

Другое соединение пытается прочитать данные и становится в очередь:
select *
from dbo.Data

Теперь попробуем запустить процедуру от имени testUser:
exec as login = 'testUser'
exec dbo.KillWrongSessions
go
revert
Происходит ошибка с таким описанием:
Msg 6102, Level 14, State 1, Line 77
User does not have permission to use the KILL statement.
Как видно предоставление доступа через grant не обеспечивает наличия прав на kill через процедуру. Что же делать? Можно включить логин testUser в роль setupadmin, но тогда он сможет отлючать любые соединения. Помогут сертификаты!

Создадим в базе данных master самоподписанный сертификат:
use master
create certificate KillWrongSessCert
       encryption by password = N'$%#$T$#TE'
       with subject = N'Сертификат для отключения только некоторых соединений.',
             start_date = '2013-12-31',
             expiry_date = '2015-12-31'
Мы задали защиту сертификата через пароль, указали сроки его действия (это опционально), задали описание. Теперь на основе созданного сертификата создадим логин:
create login KillWongSessLog from certificate KillWrongSessCert

Под данным логином никто не сможет подключиться к серверу, поэтому ему можно спокойно дать доступ на kill любых соединений:
alter server role processadmin add member KillWongSessLog

Пока мы работали с базой данных master. Чтобы перейти к нашей базе Data требуется создать резервеую копию сертификата:
backup certificate KillWrongSessCert to file = 'C:\Users\Edynak\Desktop\KillWrongSessCert.cer'
       with private key
       (
             file = 'C:\Users\Edynak\Desktop\KillWrongSessCert.pvk',
             encryption by password = N'#$#D',
             decryption by password = N'$%#$T$#TE'
       )
В этом коде мы задали место хранения резервной копии сертификата, указали для дешифровки тот же пароль, который используется для защиты сертификата, а также задали пароль для хранения приватного ключа. Восстановим сохраненную копию в базе Data:
use Data

create certificate KillWrongSessCert
       from file = 'C:\Users\Edynak\Desktop\KillWrongSessCert.cer'
       with private key
       (
             file = 'C:\Users\Edynak\Desktop\KillWrongSessCert.pvk',
             encryption by password = N'$%#$T$#TE',
             decryption by password = N'#$#D'
       )

Теперь осталось подписать хранимую процедуру созданным сертификатом:
add signature to dbo.KillWrongSessions by certificate KillWrongSessCert with password = N'$%#$T$#TE'

Здесь также указывается пароль, защищающий сертификат.
Запускаем код:
exec as login = 'testUser'
exec dbo.KillWrongSessions
go
revert

Код отработал без ошибок! По аналогии можно создавать процедуры, которые делают, например, бэкапирование баз данных, манипуляции с файлами, связанными серверами или что-нибудь еще. И при этом, через подписание процедур сертификатами, давать доступ на их запуск пользователям, не давая последним никаких прав. Одну и ту же процедуру можно подписать любым числом сертификатов, что позволяет выполнять в одной процедуре несколько действий.

понедельник, 23 июня 2014 г.

Таблицы, оптимизированные для оперативной памяти

Выпуск Microsoft SQL Server 2014 ознаменовался появлением революционного компонента In-Memory OLTP, который позволяет хранить таблицы непосредственно в оперативной памяти! Использование памяти позволяет достичь многократного ускорения в скорости запросов на извлечение и изменение данных. Думаю, что в ближайшие годы эта технология изменит представления о хранении и обработке данных во всем мире. Подход Microsoft при разработке данного компонента оказался намного более гибким чем у других производителей СУБД. В частности в других СУБД требуется создавать в оперативной памяти целую базу данных. В MS SQL 2014 можно в зависимости от ресурсов и потребностей расположить часть таблиц в оперативной памяти, а часть на диске. При этом гарантируется полная транзакционная целостность данных. Данные безопасно сохранятся даже при нештатной перезагрузке сервера. Это достигается за счет использования так называемых checkpoint-файлов, в которых хранится вся необходимая информация о данных таблицы, находящейся в памяти. Доступ к этим файлам не замедляет работу движка In-Memory OLTP, поскольку доступ последовательный, только на добавление. При необходимости (например, для ETL процессов загрузки данных) для таблиц в памяти можно отключить транзакционное логирование, благодаря чему скорость обработки данных можно еще больше увеличить.
Кроме того, имеет место мультиверсионность строк в таблице. Эта версионность лучше чем классический уровень изоляции транзакций snapshot. В частности снижается уровень блокирования не только при параллельной работе читателей и писателей, но и при совместной работе нескольких писателей. Каждая строка имеет идентификатор начала и окончания работы транзакции. И каждая транзакция может видеть эту строку, только, если ее идентификатор находится в пределах этого диапазона.
При изменениях в таблице, которые приводят к изменению в индексах, изменения не логируются, что снижает уровень системных блокировок. А сами индексы пересоздаются при старте сервера.
Посмотрим как происходит работа с новыми видами таблиц. Создадим базу данных:
use master
create database tasks
Теперь нам понадобится создать специальную файловую группу в новой базе данных, в которой будет сохраняться информация об изменениях в таблице, которая создана в памяти - checkpoint-файлы. Эти файлы бывают двух типов: файлы данных и файлы дельт. Они используются в частности для возможности воссоздания таблиц в памяти при старте сервера. Чуть позже мы поговорим о них подробнее. Код на добавление файловой группы:
alter database tasks add filegroup MemData contains memory_optimized_data
alter database tasks add file
    (
        name = MemData,
        filename = 'C:\Документы\Временные\tmp\MemData'

    ) to filegroup MemData
Для исполнения этого кода требуется, чтобы директория "C:\Документы\Временные\tmp" уже существовала. Директории "C:\Документы\Временные\tmp\MemData" быть не должно, ее создаст сервер. Теперь можно создать таблицу, оптимизированную для оперативной памяти. Пусть нам требуется хранить 2 столбца: целочисленный идентификатор (iRowId int not null) и имя некоторой сущности (vcName char ( 20 )). Перед написанием кода на создание таблицы сделаем ряд замечаний.

Движок, реализующий In-Memory For OLTP, создан на совершенно других принципах в отличие от классического Database Engine. При хранении данных на жестком диске аллокация дисковой памяти оптимальна блоками. Это дало жизнь таким структурам данных как страница и экстент. При хранении данных в оперативной памяти ее выделение происходит байтами. Поэтому когда таблица хранится в памяти, нет таких понятий как страница или экстент. Хранение всех табличных данных сугубо построчное. В Database Engine для определения того какие страницы принадлежат той или иной таблице или индексу используются битовые карты распределения IAM (Index Allocation Map). В In-Memory For OLTP таких карт нет, поэтому для того, чтобы связать строки в массив данных, относящийся к конкретной таблице, требуется хранить строки упорядоченно. Поэтому у каждой строки в памяти должен быть уникальный индекс, являющийся первичным ключом. Кроме того именно в структуре строк хранится информация о том в какой последовательности хранятся ключи того или иного индекса, поэтому все индексы должны указываться при объявлении таблицы. Ниже код на создание таблицы Persons:
create table dbo.Persons
(
    iRwoId int not null primary key nonclustered hash with ( bucket_count = 1048416 ),
    vcName varchar ( 20 ) collate Latin1_General_100_BIN2 not null,
    index IX_vcName nonclustered ( vcName )
) with ( memory_optimized = on, durability = schema_and_data )
В этом коде в конце предложения create table мы видим опцию memory_optimized со значением on. Это означает, что таблица будет храниться в памяти. Если бы было указано значение off, которое используется по умолчанию, то таблица хранилась бы на диске. Также указана опция durability со значением schema_and_data. Это значение является умолчальным и гарантирует транзакционную целостность. Если бы мы написали durability = schema_only, то это бы означало, что для таблицы транзакционное логирование отключено. Если же происходит перезагрузка сервера, то данные для такой таблицы теряются (поскольку они хранятся только в памяти), а остается только сама пустая таблица (информация о структуре таблицы хранится в метаданных на диске).
При определении текстового столбца мы используем ключевое слово collate для указания бинарной сортировки. Это требование является обязательным. Также для таблицы в памяти нельзя использовать длинные столбцы (image, text, xml, varchar ( max ) и т. д.). Максимальная длина строки составляет 8060 байт, однако в отличие от дисковых таблиц, здесь нет механизма вынесения длинных столбцов, которые не умещаются на строке (нет аналога страниц типа row overflow data). В частности, нельзя создать таблицу со двумя столбцами типа varchar ( 4000 ) и varchar ( 5000 ), поскольку 4000 + 5000 = 9000 > 8060. Когда таблица создана ее нельзя изменить, возможно только пересоздание.
При создании таблицы сервер создает библиотеку dll, которая отвечают за извлечение, вставку, обновление и удаление строк. Она хранятся в одной из директорий каталога, в который инсталлирована СУБД. Получается, что при модификации данных сервер ничего не делает, а работает эта dll. Набор библиотек свой для каждой таблицы, они написаны на языке программирования C, и их код оптимизирован под конкретную структуру таблицы.

В одной из последних строк в create table создается индекс на столбце vcName. Это также новый синтаксис MS SQL 2014, который позволяет создавать индекс на одном или нескольких столбцах сразу при создании таблицы. Индексов может быть не больше 8. Ниже мы увидим, что любой индекс является покрывающим.

Теперь обсудим первую строку в коде на создание, где используется ключевое слово hash. Это hash-индекс, являющийся первичным ключом. Он может быть только некластерным. В скобках задается число блоков для hash-таблицы, которая лежит в основе первичного ключа. Как его выбрать? Для ответа на этот вопрос посмотрим как хранятся строки таблицы. Мы выбрали поле iRowId в качестве первичного ключа. Это значит, что в памяти создается хэш-таблица с заданным в разделе bucket_count числом блоков, а хэш-функция применяется к значениям iRowId. Если есть несколько iRowId, которым соответствует одно значение хэш-функции, то они образуют цепочку. Значение из хэш-таблицы указывает на первый элемент цепочки. Этот элемент является строкой. В строке есть заголовок. В конце строки перечисляются значения столбцов и есть раздел, в котором хранится указатель на следующий элемент цепочки, то есть на следующую строку. Она в свой очередь имеет указатель на очередную строку и так далее. Если мы имеем запрос с условием равенства на iRowId, то для iRowId находится значение хэш-функции и происходит поиск по цепочке. В схеме ниже показан алгоритм построения хэш-индекса для таблицы со столбцами Row1, Row2, Row3, у которой есть два хэш-индекса: один по столбцу Row1, второй по столбцам Row2, Row3:

То есть для того чтобы поиск был быстрее необходимо, чтобы цепочка состояла из возможно меньшего числа элемента, в идеале из одного. Поэтому лучше чтобы число блоков совпадало с числом строк. Следует быть внимательным и не делать число блоков большим чем число строк в таблице. Число блоков на самом деле округляется до ближайшей (вверх) степени двойки. Если оно превышает число строк в таблице, то это приведет к бесполезному расходу памяти. И может замедлиться скорость запросов, которые будут выполнять сканирование таблицы по такому индексу: нужно будет сканировать блоки, которые не указывают ни на какие строки. Можно проверить истинное значение числа блоков по системному каталогу sys.hash_indexes (поле bucket_count).

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

Существуют представления, которые позволяют получить статистику по хэш-индексу. По представлению sys.dm_db_xtp_hash_index_stats можно определеить общее число блоков хэш-функции, число занятых блоков, среднюю и максимальную длину цепочки элементов, соответствующих строкам с одинаковым значением хэш-функции.

Из архитектуры хэш-индекса понятно, что он поможет выполнять поиск по запросу с условием равенства на столбец, входящий в хэш-индекс. Но, если, например, запрос содержит условие where ColName like 'data%' или iRowId between, то поиска по индексу не будет, поскольку неизвестно по какому хэш-значению делать поиск. Для поиска по таким условиям нужно использовать другой тип индексов: диапазонные. Такой индекс создан на таблице dbo.Persons, на столбце vcName. Такие индексы свободны от лэтчей, изменения в них не логируются, поскольку индекс пересоздается при рестарте сервера.

Данные индексы имеют похожую на классическую структуру двоичного дерева. Но страницы могут иметь разный размер (максимум 8 Кб). Страницы индексов никогда не обновляются. Например, при обновлении строки, запись на странице индекса помечается как удаленная. Создается просто компенсирующая страница, на которой содержится новая строка. В такой дельта-странице содержится только одна строка. Когда таких дельта-страниц становится слишком много, то происходит так называемое слияние страниц. Страницы с устаревшими записями помечаются, как предназначенные для удаления сборщиком мусора. Есть и специальные алгоритмы, которые выполняют разбиение страницы, если она полностью заполнены. Ели страница содержит мало записей, может происходить ее слияние.

Теперь проверим хэш-индексы в деле. Посмотрим на скорость вставки и операций поиска и сканирования по хэш-индексу. Сравним ее с аналогичными операциями для дисковых таблиц с обычным индексом. Сравним также скорость вставки в таблицу, хранящуюся в памяти с отключенным журналированием транзакций. Поскольку на моем компьютере памяти мало (4 Гб), то число записей в таблице будет небольшим: 2 млн. строк. Наполним таблицу dbo.Persons:
insert into dbo.Persons ( iRowId, vcName )
       select top 2000000, row_number () over ( order by data1.number ), left (
             replace ( cast ( binary_checksum ( newid () ) as varchar ( 100 ) ), '-', '' ) +
             replace ( cast ( binary_checksum ( newid () ) as varchar ( 100 ) ), '-', '' ) +
             replace ( cast ( binary_checksum ( newid () ) as varchar ( 100 ) ), '-', '' ), 20 )
       from
             master.dbo.spt_values data1
                    cross join
             master.dbo.spt_values data2
Вставка длилась 18 секунд. Теперь создадим и наполним дисковую таблицу с такой же структурой/ Запрос на вставку будет аналогичным:
create table dbo.Entity
(
       iRowId int identity ( 1, 1 )      not null,
       vcName char ( 20 )                      not null,
       constraint AK_Entity_vcName unique nonclustered ( vcName asc ) on [PRIMARY],
       constraint PK_Entity_iRowId primary key clustered ( iRowId asc ) on [PRIMARY]
) on [PRIMARY]

insert into dbo.Entity ( vcName )
       select top 2000000 left (
             replace ( cast ( binary_checksum ( newid () ) as varchar ( 100 ) ), '-', '' ) +
             replace ( cast ( binary_checksum ( newid () ) as varchar ( 100 ) ), '-', '' ) +
             replace ( cast ( binary_checksum ( newid () ) as varchar ( 100 ) ), '-', '' ), 20 )
       from
             master.dbo.spt_values data1
                    cross join
             master.dbo.spt_values data2
Теперь вставка длилась 49 секунд, в 2.7 раз медленнее.
Проверим скорость работы запросов. Все запросы будет запускаться с предварительно очищенным кэшем (с помощью инструкции dbcc dropcleanbuffers). Проверим скорость сканирования таблиц. Выполним запросы:
select *
from dbo.Entity with ( index ( PK_Entity_iRowId ) )
where vcName = '12345678901234567890'
select *
from dbo.Persons with ( index ( 3 ) )
where vcName = '12345678901234567890'
Первый запрос (к дисковой таблице) отработал на холодном кэше за 5819 миллисекунд, второй работал 499 мс. То есть скосроть выросла в 11.6 раза!
Проверим теперь скорость работы при поиске по первичному ключу. Запустим такие запросы:
select *
from dbo.Entity --with ( index ( PK_Entity_iRowId ) )
where iRowId = 1000000
select *
from dbo.Persons --with ( index ( 3 ) )
where iRowId = 1000000
Первый запрос отработал за 363 мс, второй - за 101 мс, скосрость выросла в 3.6 раза.
Следующий тест покажет эффективность работы запроса с условием like:
select *
from dbo.Entity
where vcName like '%1%2%3%4%5%6%7%8%9%0%'
select *
from dbo.Persons
where vcName like '%1%2%3%4%5%6%7%8%9%0%'
Первый запрос отработал за 7290 мс, второй - за 995 мс, то есть скорость выросла 7.3 раза.

Теперь проверим поиск по некластерному индексу дисковой таблицы и по диапазонному индексу таблицы в памяти:
select *
from dbo.Entity
where vcName = '12345678901234567890'
select *
from dbo.Persons
where vcName = '12345678901234567890'
Скорость первого запроса составила 207 мс, второго - 82 мс, то есть второй запрос отработал быстрее первого в 2.5 раза.

Теперь проверим как влияет на скорость вставки наличие транзакционного логирования. Создадим копию таблицы Persons:
create table dbo.Persons_Add
(
       iRowId int not null primary key nonclustered hash with ( bucket_count = 1048416 ),
       vcName varchar ( 20 ) collate Latin1_General_100_BIN2 not null
       index IX_vc_Addnonclustered ( vcName )
) with ( memory_optimized = on, durability = schema_only )
insert into dbo.Persons_Add( iRowId, vcName )
       select top 2000000 row_number () over ( order by data1.number ), left (
             replace ( cast ( binary_checksum ( newid () ) as varchar ( 100 ) ), '-', '' ) +
             replace ( cast ( binary_checksum ( newid () ) as varchar ( 100 ) ), '-', '' ) +
             replace ( cast ( binary_checksum ( newid () ) as varchar ( 100 ) ), '-', '' ), 20 )
       from
             master.dbo.spt_values data1
                    cross join
             master.dbo.spt_values data2
В ходе создания таблицы мы указали здесь значение опции durability, равное schema_only.
Теперь проверим скорость работы таких запросов на обновление:
update dbo.Entity
       set vcName = '12345678901234567890'
update dbo.Persons
       set vcName = '12345678901234567890'
update dbo.Persons_Add
       set vcName = '12345678901234567890'
Предварительно я удалил уникальный ключ на vcName в таблице Entity, заменив его неуникальным некластерным индексом на vcName.
Первый запрос отработал за 1 минуту 6 секунд, второй - за 6 секунд, 3-ий - за 2 секунды!!! То есть при использовании таблицы в памяти скорость обновления выросла в 11 раз, а при использовании таблицы в памяти без транзакционного логирования скорость обновления выросла в 33 раза (и в 3 раза по сравнению с таблицами в памяти, для которых логирование включено)! Видно, что даже при использовании таблицы в памяти, для которой пишется транзакционный лог, его объем намного меньше по сравнению с дисковыми таблицами.

При написании хранимых процедур, которые обращаются только к таблицам в памяти, можно создавать эти процедуры с опцией машинной компиляции. Это означает, что при создании процедуры происходит создание и загрузка библиотеки dll, которая решает все задачи, описанные в процедуре. Библиотека содержит скомпилированный код на языке C. Она использует движок In Memory OLTP, а также библиотеки, созданную для таблиц, к которым обращается процедура. Машинная компиляция, позволяет избежать стадии интерпретации и построения плана выполнения при исполнении процедуры. План выполнения также создается и компилируется в машинный код, а затем всегда используется. Машинная компиляция позволяет повысить производительность в высоко нагруженных OLTP системах, где могут быть сотни или тысячи активных соединений, которые постоянно запускают процедуры. В таких средах благодаря машинной компиляции снизится время, необходимое для выполнения большого числа запросов. Ниже приведен простой пример создания нативной процедуры:
create proc dbo.Test
with native_compilation, schemabinding, exec as owner
as
begin atomic with ( transaction isolation level = snapshot, language = N'us_english' )
       select iRowId, vcName
       from dbo.Persons

end
Если в таблицах, которые используются в процедуре, изменится распределение данных, то используемый план выполнения может стать неоптимальным. В этом случае можно пересоздать процедуру или перезагрузхить сервер. В последнем случае все нативные процедуры перекомпилируются. В частности, библиотеки dll для таблиц и процедур не входят в бэкап. Они пересоздаются при старте базы данных.
Список библиотек можно посомтреть с помощью такого запроса:
select *
from sys.dm_os_loaded_modules
where description = 'XTP Native DLL'
Обсудим теперь архитектуру checkpoint-файлов. Данные об изменениях в таблицах, оптимизированных для памяти, постепенно перетекают из журнала в checkpoint-файл. Файлы checkpoint хранятся в файловой группе filestream, которая была создана в самом начале. Все файлы разбиты на пары: файл данных и файл дельты. В файле данных хранятся сведения и том, какие записи были вставлены в таблицы в памяти, а в файле дельт хранится информация о том, какие записи были помечены для удаления. Каждая пара checkpoint-файлов соовтетствует 100 Мб данных журнала. Со временем объем файлов дельт увеличивается, а объем неустаревших данных в файле данных уменьшается.  В этом случае может происходить слияние checkpoint-файлов. Оно происходит автоматически, но при возникновении проблем можно запускать слияние вручную, вызвав хранимую процедуру sys.sp_xtp_merge_checkpoint_files. С помощью представления sys.dm_db_xtp_checkpoint_files можно посмотреть количество и статус checkpoint-файлов.

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

Движок In Memory OLTP интегрирован с регулятором ресурсов. С помощью процедур sp_xtp_bind_db_resource_pool, sp_xtp_unbind_resource_pool можно привязать или отвязать базу данных к определенному пулу ресурсов, задав таким образом верхний предел объема памяти, доступной для таблиц в памяти в данной базе данных.

Для таблиц в памяти имеются некоторые синтаксические ограничения. Это связано с тем, что для реализации той или иной функции (например, truncate table) необходимо писать новый код для таблиц в памяти. Для основных методов доступа к данных (select, insert и пр.) эти функции реализованы, для некоторых (select *, identity-поля) пока не успели. Но там, где есть синтаксические ограничения, их можно обойти, проявив смекалку. С выходом новых версий синтаксические возможности без сомнения будут расширяться.

среда, 11 июня 2014 г.

Новые аналитические функции MS SQL 2012

С выходом новой версии СУБД Microsoft SQL Server 2012 разработчикам доступны новые аналитические функции, которые позволяют разработчикам писать более эффективные и быстрые запросы, используя гораздо меньший объем кода. Всем известна классическая задача, когда для таблицы требуется написать запрос, который возвращает в текущей строке 2 столбца, первый из которых соответствует столбцу текущей строки, а второй соответствует тому же столбцу предыдущей строки. Пусть нам надо хранить цену за определенный товар на каждый день. Построим таблицу, в которой будет столбец с датой, идентификатором товара и ценой этого товара на день:
create table #goods
(
       dtDate date   not null,
       iGoodId int   not null,
       fPrice float  not null,
       primary key clustered ( iGoodId asc, dtDate asc ) on [PRIMARY]
) on [PRIMARY]

insert into #goods ( dtDate, iGoodId, fPrice )
       values
             ( '2014-01-01', 1, 23 ),
             ( '2014-01-02', 1, 24 ),
             ( '2014-01-03', 1, 25 ),
             ( '2014-01-01', 2, 13 ),
             ( '2014-01-02', 2, 14 ),
             ( '2014-01-03', 2, 15 )


Требуется написать запрос, который для каждой строки таблицы покажет цену на данный товар за предыдущий день, который есть в таблице. Обычно для решения этой задачи использовался джойн таблицы на себя с условием равенства на iGoodId, а также выполнялся поиск максимальной даты из правой таблицы, которая меньше даты из левой таблицы. Либо делалось то же самое, но через связанный подзапрос. Теперь можно просто воспользоваться функцией lag, и сделать задачу одним запросом без всяких джойнов:
select dtDate, iGoodId, fPrice CurPrice,
       isnull ( lag ( fPrice ) over ( partition by iGoodId order by iGoodId asc, dtDate asc ), -1 ) PrevPrice
from #goods

Функция lag может, например, использоваться для поиска тех дней, когда происходили изменения в ценах на товар. Для этого можно написать такой запрос:
;
with Data
as
(
       select dtDate, iGoodId, fPrice CurPrice, isnull ( lag ( fPrice ) over ( partition by iGoodId order by iGoodId asc, dtDate asc ), -1 ) PrevPrice
       from #goods
)
select dtDate, iGoodId, CurPrice
from Data

where CurPrice <> PrevPrice

Если требуется получить цену не за предыдущий, а за следующий день, то используется функция lead:
select dtDate, iGoodId, fPrice CurPrice,
       isnull ( lead ( fPrice ) over ( partition by iGoodId order by iGoodId asc, dtDate asc ), -1 ) NextPrice
from #goods


Добавим теперь к таблице новый столбец с объемом продаж товара за дату:
alter table #goods add mSells money not null default ( 100 )


Теперь посчитаем нарастающий итог. То есть для каждого товара и даты посчитаем сумму продаж с начальной даты в заданной партиции до текущей даты. Это опять-таки можно сделать, используя один запрос, без всяких джойнов. Используется расширение предложения over в агрегирующей функции:
select dtDate, iGoodId, sum ( mSells ) over ( partition by iGoodId order by iGoodId asc, dtDate asc rows between unbounded preceding and current row )
from #goods


Еще можно для каждой строки указать цены за самую первую и за самую последнюю даты, с
которой и до которой торгуется данный товар:
select dtDate, iGoodId, fPrice CurPrice,
       first_value ( fPrice ) over ( partition by iGoodId order by iGoodId asc, dtDate asc ) FirstPrice,
       last_value ( fPrice ) over ( partition by iGoodId order by iGoodId asc, dtDate asc ) LastPrice
from #goods