CDC
По материалам постов Чт Ноя 29, 2007 11:55, Пт ноя 21, 2008 18:00, Сб дек 06, 2008 02:00 на sqlclub.ru.
Дема показывалась на лонче SQL Server 2008 и на TechDays Fall'08. Издание третье, исправленное и дополненное.
Отслеживание изменений является классической задачей при погружении данных в хранилище. Объемы таковы, что позволить себе роскошь каждый вечер перезаливать все по-новой, не может никто. С инкрементным приращением таблиц фактов дело обстоит более-менее цивилизованно. В них всегда имеется поле типа даты совершения операции, по которому можно отсечь старые записи, которые уже имеются в хранилище. Хуже, когда бизнес-юзеры делают исправления задним числом, причем не сторнирующей проводкой, а напрямую. Справочники также имеют обыкновение не только прирастать, но и правиться, причем в операционной базе это, как правило, изменения in-place. Перетерли старое значение – и до свидания, а вы уже там в своем хранилище решайте, медленно меняющееся измерение какого рода оно у вас там будет. Раньше, как я писал в посте "Аудит в SQL Server. Триггер на логон", для отслеживания изменений использовались таймстэмпы, триггеры, профайлер, а также читалки лога, покупные или самописные, потому что глупо изобретать какой-то дополнительный механизм, если все изменения по определению лежат в логе, и все, что в данном случае требуется, это человеческий способ их там посмотреть. В 2008-м, наконец, такой способ появился под названием Change Data Capture (CDC). Поскольку хранилища – вещь серьезная, он входит в редакцию Enterprise (и, понятно, в Developer и Evaluation). Я буду иллюстрировать и по ходу пояснять.
Для начала проверяем, запущен ли SQL Agent, и, если нет, запускаем. Сейчас мы увидим, для чего он нам нужен.
if not exists(select 1 from sys.sysprocesses where program_name like 'SQLAgent%')
exec master..xp_servicecontrol 'start', 'SQLSERVERAGENT'
Скрипт 1
Как видите, я по привычке продолжаю пользовать sysprocesses несмотря на наличие таких замечательных DMV, как dm_exec_sessions, которое, по идее, было призвано заменить собой sysprocesses, как dm_exec_requests и т.д. Дело здесь не в моей заскорузлости, а вот в этой заметке - https://sqlblog.com/blogs/kalen_delaney/archive/2008/06/29/why-i-still-need-sysprocesses.aspx.
Далее нужно заенейблить базу для CDC. На системных базах CDC не работает, в том числе на tempdb; создадим свою для экспериментов, а в ней – табличку, изменения по которой будем отслеживать. Каждая запись может меняться неоднократно, соответственно, по ней могут отслеживаться все промежуточные изменения или выводиться только финальная картина, что с ней в итоге стало. Последний тип изменений называется net changes. Для его поддержки по таблице должен иметься уникальный индекс, очевидно, для того, чтобы CDC могла сделать group by всех промежуточных изменений в разрезе по записям, а для этого ей нужно однозначно идентифицировать каждую запись.
use tempdb
if exists (select 1 from sys.databases where name = 'cdc_test')
begin
alter database cdc_test set single_user with rollback immediate
drop database cdc_test
end
create database cdc_test
go
use cdc_test
create table Products (id int not null, name varchar(50), price smallmoney)
create unique index id_idx on Products(id)
Скрипт 2
И, натурально, включим на базе этот самый CDC. Из интерфейса SSMS эти операции недоступны, но в Т-SQL все делается максимально юзер-френдли:
exec sys.sp_cdc_enable_db
Скрипт 3
Над каждой таблицей, по которой мы хотим отслеживать изменения, CDC требуется включить отдельно. В особенности юзер-френдли выглядит включение CDC над таблицей:
exec sys.sp_cdc_enable_table
@source_schema = N'dbo',
@source_name = N'Products',
@role_name = N'cdc_Products',
@capture_instance = N'dbo_Products',
@supports_net_changes = 1,
@index_name = 'id_idx',
@captured_column_list = N'id, name, price',
@filegroup_name = null;
Скрипт 4
С первыми двумя параметрами процедуры все понятно – это схема и имя таблицы, изменения над которой будем ослеживать. Третий параметр – имя роли, которой дозволяется эти изменения смотреть, причем предопределенные роли типа db_owner не допускаются, приходится придумывать. Радует, что если указанная роль не существовала, она при этом создается автоматически. Хотя, конечно, можно было роль создать заранее, напихать в нее членов и подсунуть процедуре. Четвертый параметр хитрый. CDC создает схему cdc и в ней кучу служебных таблиц, функций и процедур для своих надобностей. Обычно внутренние таблицы видны в sys.internal_tables, их имена заканчиваются на _<object_id объекта, который данная внутренняя таблица обслуживает>, и доступиться до них нельзя иначе, как из DACa. Мы уже сталкивались с внутренней таблицей кандидатов в покойники в посте "Конфигурирование FILESTREAM. FILESTREAM и сборка мусора (удаление старых версий файлов)". В данном случае служебные объекты доступны с обычного соединения, и хвосты к их стандартным именам можно задавать на свое усмотрение. Например, служебная функция cdc.fn_cdc_get_all_changes, показывающая все изменения, произошедшие над таблицей, в данном случае будет называться cdc.fn_cdc_get_all_changes_dbo_Products. Это сделано потому, что над одной таблицей можно создать, вообще говоря, два capture instances. Один будет отслеживать такие-то изменения по таким-то колонкам, другой по другим. Хвосты нужны, чтобы различить, к результатам какого экземпляра отслеживания мы хотим обратиться. Параметр @supports_net_changes я уже косвенно упоминал; он означает, поддерживается ли дополнительно вывод итоговой картины, если над одной записью произошло несколько изменений. В этом случае над таблицей должен присутствовать уникальный индекс, который и указывается в следующем параметре, @index_name. По умолчанию, он имеет значение NULL, что означает, что CDC будет искать первичный ключ над таблицей в качестве индекса и очень расстроится, если не найдет. В параметре @captured_column_list перечисляются названия полей, изменения по которым хотим отслеживать. Поля, образующие первичный ключ таблицы (или unique index), должны присутствовать обязательно. По умолчанию, параметр имеет значение NULL, что означает, что включаются все поля. Наконец, параметр @filegroup_name задает файл-группу, на которую будут складываться отслеженные изменения в исходной таблице. По умолчанию, он имеет значение NULL, что означает дефолтную файл-группу.
После выполнения Скрипта 4 в SQL Agente создалась пара джобов:
select j.name, j.enabled, j.description, c.name from msdb.dbo.sysjobs j join msdb.dbo.syscategories c on j.category_id = c.category_id
рис.1
из которых понятно, по какому принципу действует CDC. Это просто читалка лога, как и в случае репликации. Только репликация потом доставляет транзакции на подписчиков, а CDC Log Scan Job складывает в таблицу изменений, которую со временем чистит CDC Cleanup Job. В принципе, их можно создать руками при помощи хранимой процедуры sp_cdc_add_job. В ее параметрах можно указать, с какой периодичностью Log Scan читает лог (по умолчанию 5 сек., диапазон 0 - сутки) и сколько собранные транзакции хранятся в таблице изменений прежде, чем их выкинет оттуда Cleanup (по умолчанию 3 суток, максимально 100 лет). Посмотреть существующие CDCшные джобы можно в таблице msdb.dbo.cdc_jobs. Что какая колонка в ней означает, можно прочитать в документации. В мае перевод слегка поправили, так как раньше он напоминал типовую китайскую инструкцию. Поскольку минута смеха продлевает жизнь на сколько-то там, русский BOL просто рекомендуется читать для долголетия. Изменить параметры существующих джобов по многочисленным пожеланиям трудящихся позволяет процедура sys.sp_cdc_change_job. Вручную почистить таблицы изменений можно с помощью sys.sp_cdc_cleanup_change_table. Дальше посмотрим.
Имеется также куча новых системных таблиц в 2008 и колонок в существовавших, которые позволяют получить метаданные о процессе CDC. Например,
select name from sys.databases where is_cdc_enabled = 1 --Над какими базами включено
select * from cdc.change_tables --Над какими таблицами в базе
select name, is_tracked_by_cdc from sys.tables --аналогично
select * from cdc.captured_columns --Над какими колонками отслеживаются изменения
select * from cdc.index_columns --Какие поля входят в РК / unique index по таблицам
exec sys.sp_cdc_help_change_data_capture @source_schema = 'dbo', @source_name = 'Products' --показывает общую конфигурацию экземпляров отслеживания, ассоциированных с таблицей @source_name
Скрипт 5
Чтобы посмотреть, как это работает на практике, поизгаляемся немного над несчастной таблицей
insert Products values (1, 'aaa', 10)
update Products set name = 'bbb', price = 20 where id = 1
update Products set name = 'ссс', price = 30 where id = 1
delete Products where id = 1
Скрипт 6
и попробуем теперь получить сделанные изменения. Кусок лога, относящийся к данному экземпляру отслеживания (capture instance – см. одноименный параметр процедуры sp_cdc_enable_table, Скрипт 4) лежит в таблице по имени cdc.<экземпляр отслеживания>_СТ (последние, видимо, от change table – таблица изменений). Ее можно спокойно просматривать:
select * from cdc.dbo_Products_CT
__$start_lsn |
__$end_lsn |
__$seqval |
__$operation |
__$update_mask |
id |
name |
price |
0x00000035000001110006 |
NULL |
0x00000035000001110002 |
2 |
0x07 |
1 |
aaa |
10 |
0x00000035000001130006 |
NULL |
0x00000035000001130002 |
3 |
0x06 |
1 |
aaa |
10 |
0x00000035000001130006 |
NULL |
0x00000035000001130002 |
4 |
0x06 |
1 |
bbb |
20 |
0x00000035000001150006 |
NULL |
0x00000035000001150002 |
3 |
0x06 |
1 |
bbb |
20 |
0x00000035000001150006 |
NULL |
0x00000035000001150002 |
4 |
0x06 |
1 |
ссс |
30 |
0x00000035000001170006 |
NULL |
0x00000035000001170004 |
1 |
0x07 |
1 |
ссс |
30 |
Скрипт 7
Ее структура описана в BOL - https://msdn.microsoft.com/ru-ru/library/bb500305.aspx. Первые три колонки сугубо логовские. LSN – это log sequence number, своего рода IDшник, которым снабжаются все транзакции в логе. Seqval – номер операции внутри транзакции. Оperation – код операции: 1 = delete, 2 = insert, 3 = update (before image), 4 = update (after, т.е. redo). В конце таблицы идут колонки, соответствующие параметру @captured_column_list процедуры sp_cdc_enable_table (см. Скрипт 4). В них указываются значения соответствующих колонок исходной таблицы на момент операции. Update_mask – это битовая маска, в которой единички стоят в тех позициях, какие колонки обновились. Для простоты позиции колонок соответствуют не тому порядку, в котором они перечислены, а наоборот, т.е. справа налево. Например, в Скрипте 6 у нас апдейтились поля Name и Price. Читаем колонки в порядке, обратном тому, как они были заданы в @captured_column_list и как они показываются в Скрипте 7, то есть Price, Name, ID. Ставим единички проапдейченым полям. Получаем 110, или 0х06, что и стоит в колонке update_mask. Понятно, что для операций insert, delete все колонки считаются обновившимися, то есть единички стоят во всех разрядах.
Вместо непосредственного чтения таблицы изменения можно воспользоваться функцией fn_cdc_get_all_changes, на конце которой, естественно, должен стоять соответствующий экземпляр отслеживания. Она принимает LSNы, в диапазоне которых мы хотим получить изменения, и какие, собственно, изменения мы хотим получить. 'Аll update old' в качестве третьего параметра означает, что для каждой операции update будут выводиться две строки – со значениями, которые были до обновления, и которые стали в результате. Просто 'All' означает, что будут выводиться только новые значения.
declare @from_lsn binary(10), @to_lsn binary(10)
select @from_lsn = min(__$start_lsn) from cdc.dbo_Products_CT
select @to_lsn = max(__$start_lsn) from cdc.dbo_Products_CT
select * from cdc.fn_cdc_get_all_changes_dbo_products(@from_lsn, @to_lsn, 'all update old')
Скрипт 8
Выдача, которая будет получена в результате этого скрипта, совпадает со Скриптом 8.
Для ленивых, которым в лом написать select min/max() ... существуют функции
declare @from_lsn binary(10) = sys.fn_cdc_get_min_lsn('dbo_Products')
declare @to_lsn binary(10) = sys.fn_cdc_get_max_lsn()
Скрипт 9
Если в качестве параметра указать имя экземпляра отслеживания, минимальный/максимальный LSN будет получен для него, если в sys.fn_cdc_get_max_lsn ничего не указывать, будет максимальный для всех экземпляров отслеживания.
Поскольку в Скрипте 4 мы включили (@supports_net_changes) поддержку итоговых изменений, вместо fn_cdc_get_all_changes можно применить функцию
select * from cdc.fn_cdc_get_net_changes_dbo_products(sys.fn_cdc_get_min_lsn('dbo_Products'), sys.fn_cdc_get_max_lsn(), 'all')
Скрипт 10
В данном случае итоговые изменения пусты, так как (см. Скрипт 6), мы добавили запись, пару раз ее обновили и удалили, т.е. по существу в таблице в итоге ничего не поменялось.
Для совсем ленивых существуют функции
select sys.fn_cdc_get_column_ordinal ('dbo_Products' , 'price') -- порядковый номер колонки в captured_columns
select sys.fn_cdc_has_column_changed ('dbo_Products' , 'price' , 0x03) -- попадает ли колонка price в заданную маску
select sys.fn_cdc_is_bit_set(3 , 0x03) -- установлен ли бит для колонки на позиции 3 в заданной маске
Скрипт 11
Еще можно искать изменения в диапазоне не по LSN, а по времени. Для этого существует таблица соответствий LSN <-> момент времени под названием cdc.lsn_time_mapping:
select * from cdc.lsn_time_mapping
start_lsn |
tran_begin_time |
tran_end_time |
tran_id |
tran_begin_lsn |
0x00000035000000BB0001 |
07/08/09 17:15:38.213 |
07/08/09 17:15:38.213 |
0x00 |
0x00000000000000000000 |
0x00000035000001110006 |
07/08/09 17:19:18.033 |
07/08/09 17:19:18.033 |
0x000000000E1B |
0x00000035000001110001 |
0x00000035000001130006 |
07/08/09 17:19:18.033 |
07/08/09 17:19:18.033 |
0x000000000E1C |
0x00000035000001130001 |
0x00000035000001150006 |
07/08/09 17:19:18.050 |
07/08/09 17:19:18.050 |
0x000000000E1D |
0x00000035000001150001 |
0x00000035000001170006 |
07/08/09 17:19:18.050 |
07/08/09 17:19:18.050 |
0x000000000E1E |
0x00000035000001170001 |
0x000000350000015B0001 |
07/08/09 17:24:23.410 |
07/08/09 17:24:23.410 |
0x00 |
0x00000000000000000000 |
0x00000035000001A50001 |
07/08/09 17:29:25.340 |
07/08/09 17:29:25.340 |
0x00 |
0x00000000000000000000 |
0x00000035000001E00001 |
07/08/09 17:34:26.940 |
07/08/09 17:34:26.940 |
0x00 |
0x00000000000000000000 |
0x00000035000002540001 |
07/08/09 17:39:28.540 |
07/08/09 17:39:28.540 |
0x00 |
0x00000000000000000000 |
0x000000360000001F0001 |
07/08/09 17:44:30.327 |
07/08/09 17:44:30.327 |
0x00 |
0x00000000000000000000 |
0x000000360000005A0001 |
07/08/09 17:49:32.420 |
07/08/09 17:49:32.420 |
0x00 |
0x00000000000000000000 |
Скрипт 12
так что получить LSN на заданный момент времени или позже по ней не составляет труда: select min(start_lsn) from cdc.lsn_time_mapping where tran_begin_time >= '2009-08-07 17:19:18.050'. Однако и тут это уже все сделано за нас в виде функции
declare @from_lsn varbinary(10) = sys.fn_cdc_map_time_to_lsn('smallest greater than or equal', @start_time)
declare @to_lsn varbinary(10)= sys.fn_cdc_map_time_to_lsn('largest less than or equal', @end_time)
Скрипт 13
где первый параметр может принимать значения largest less than, largest less than or equal, smallest greater than, smallest greater than or equal, что вообще избавляет от необходимости думать. Предусмотрена и обратная функция sys.fn_cdc_map_lsn_to_time. У нее, понятно, первый параметр отсутствует, т.к. каждый LSN случился в какой-нибудь момент времени, но обратное, вообще говоря, не верно: не факт, что в каждый момент времени случился какой-нибудь LSN. Наши 4 операции из Скрипта 6 – это, очевидно, те, что произошли в 17:19:18. Если мы хотим, допустим, отсечь по времени из диапазона изменений последнее удаление, это можно изобразить приблизительно так: sys.fn_cdc_map_time_to_lsn('smallest greater than or equal', '2009-08-07 17:19:18.050'). В момент 07/08/09 17:19:18.050 (см. Скрипт 7) произошли две операции: второе обновление и удаление. Функция вернет минимальный LSN за этот момент, т.е. 0x00000035000001150006. Получаем
select * from cdc.fn_cdc_get_net_changes_dbo_products(sys.fn_cdc_get_min_lsn('dbo_Products'), sys.fn_cdc_map_time_to_lsn('smallest greater than or equal', '2009-08-07 17:19:18.050'), 'all')
__$start_lsn |
__$operation |
__$update_mask |
id |
name |
price |
0x00000035000001150006 |
2 |
NULL |
1 |
ссс |
30 |
Скрипт 14
Обратите внимание, что она правильно подытожила результаты: значения полей записи указаны по итогам последнего update, но __$operation = 2, т.е. вставка. Раньше этой записи не было, следовательно, ее полагается с этими значениями полей вставить.
Ручная очистка старых изменений выполняется при помощи процедуры sys.sp_cdc_cleanup_change_table. Первый параметр указывает на экземпляр отслеживания, таблицу изменений которого чистим, второй – на LSN, до которого из прошлого чистим. Например, если мы хотим очистить первые три изменения из таблицы Скрипт 7, это можно сделать так:
declare @lsn varbinary(10)
select top 5 @lsn = __$start_lsn from cdc.dbo_Products_CT order by __$start_lsn
select @lsn
exec sys.sp_cdc_cleanup_change_table @capture_instance = 'dbo_Products', @low_water_mark = @lsn
select * from cdc.dbo_Products_CT
__$start_lsn |
__$end_lsn |
__$seqval |
__$operation |
__$update_mask |
id |
name |
price |
0x00000035000001150006 |
NULL |
0x00000035000001150002 |
3 |
0x06 |
1 |
bbb |
20 |
0x00000035000001150006 |
NULL |
0x00000035000001150002 |
4 |
0x06 |
1 |
ссс |
30 |
0x00000035000001170006 |
NULL |
0x00000035000001170004 |
1 |
0x07 |
1 |
ссс |
30 |
Скрипт 15
select top 5 возвращает 0x00000035000001150006. Следовательно, все строки с LSNом меньше будут удалены, что мы и наблюдаем. Если бы мы захотели очистить все изменения для данного экземпляра отслеживания, надо было присвоить declare @lsn varbinary(10) = sys.fn_cdc_get_max_lsn().
Отмена Change Data Capture над таблицей:
exec sys.sp_cdc_disable_table @source_schema = 'dbo', @source_name = 'Products', @capture_instance = 'dbo_Products'
Скрипт 16
И над всей базой:
exec sys.sp_cdc_disable_db
Скрипт 17
Удаляются все экземпляры отслеживания, схема cdc, объекты в ней и джобы. Роль cdc_Products, даже если она была создана процедурой sys.sp_cdc_enable_table, остается.