Этот вопрос связан с этой веткой форума .
Запуск SQL Server 2008 Developer Edition на моей рабочей станции и двухузловой кластер виртуальных машин Enterprise Edition, где я называю «альфа-кластер».
Время, необходимое для удаления строк с столбцом varbinary (max), напрямую связано с длиной данных в этом столбце. Поначалу это может показаться интуитивно понятным, но после исследования это вступает в противоречие с моим пониманием того, как SQL Server фактически удаляет строки в целом и обрабатывает данные такого рода.
Проблема связана с проблемой истечения времени ожидания (> 30 секунд), которую мы наблюдаем в нашем веб-приложении .NET, но я упростил ее для обсуждения.
Когда запись удаляется, SQL Server помечает ее как призрак, подлежащий очистке с помощью задачи очистки призрака, через некоторое время после фиксации транзакции (см . Блог Пола Рэндала ). В тесте, удаляющем три строки с данными 16 КБ, 4 МБ и 50 МБ в столбце varbinary (max) соответственно, я вижу, что это происходит на странице со строчной частью данных, а также в транзакции журнал.
Что мне кажется странным, так это то, что блокировки X помещаются на все страницы данных больших объектов во время удаления, и эти страницы освобождаются в PFS. Я вижу это в журнале транзакций, а также с sp_lock
и результаты dm_db_index_operational_stats
DMV ( page_lock_count
).
Это создает узкое место ввода-вывода на моей рабочей станции и нашем альфа-кластере, если эти страницы еще не находятся в буферном кеше. На самом деле, page_io_latch_wait_in_ms
из одной и той же DMV происходит практически вся продолжительность удаления, и это page_io_latch_wait_count
соответствует количеству заблокированных страниц. Для файла размером 50 МБ на моей рабочей станции это составляет более 3 секунд при запуске с пустым буферным кешем ( checkpoint
/ dbcc dropcleanbuffers
), и я не сомневаюсь, что он будет дольше для интенсивной фрагментации и под нагрузкой.
Я попытался убедиться, что это не просто выделение места в кеше, занимающее это время. Я прочитал в 2 ГБ данных из других строк перед выполнением удаления вместо checkpoint
метода, который больше, чем выделено процессу SQL Server. Не уверен, является ли это действительным тестом или нет, так как я не знаю, как SQL Server перемешивает данные. Я предполагал, что это всегда вытеснит старое в пользу нового.
Кроме того, он даже не изменяет страницы. Это я могу видеть dm_os_buffer_descriptors
. После удаления страницы очищаются, а количество измененных страниц меньше 20 для всех трех небольших, средних и больших удалений. Я также сравнил вывод DBCC PAGE
для выборки просмотренных страниц, и изменений не было ( ALLOCATED
из PFS был удален только бит). Это просто освобождает их.
Чтобы доказать, что поиск / освобождение страниц являются причиной проблемы, я попробовал тот же тест, используя столбец filestream вместо vanilla varbinary (max). Количество удалений было постоянным, независимо от размера LOB.
Итак, сначала мои академические вопросы:
- Почему SQL Server должен искать все страницы данных больших объектов, чтобы X заблокировал их? Это просто деталь того, как блокировки представлены в памяти (как-то хранятся вместе со страницей)? Это делает влияние ввода-вывода сильно зависит от размера данных, если не полностью кэшируется.
- Почему X блокируется вообще, просто чтобы освободить их? Разве этого недостаточно, чтобы заблокировать только лист индекса с частью строки, так как освобождение не должно изменять сами страницы? Есть ли другой способ получить данные LOB, от которых защищает блокировка?
- Зачем вообще освобождать страницы, учитывая, что уже есть фоновая задача, посвященная этому типу работы?
И, может быть, более важный мой практический вопрос:
- Есть ли способ заставить удаления работать по-другому? Моя цель состоит в постоянном удалении независимо от размера, аналогично файловому потоку, где любая очистка происходит в фоновом режиме после факта. Это вещь конфигурации? Я храню вещи странно?
Вот как воспроизвести описанный тест (выполняется через окно запроса SSMS):
CREATE TABLE [T] (
[ID] [uniqueidentifier] NOT NULL PRIMARY KEY,
[Data] [varbinary](max) NULL
)
DECLARE @SmallID uniqueidentifier
DECLARE @MediumID uniqueidentifier
DECLARE @LargeID uniqueidentifier
SELECT @SmallID = NEWID(), @MediumID = NEWID(), @LargeID = NEWID()
-- May want to keep these IDs somewhere so you can use them in the deletes without var declaration
INSERT INTO [T] VALUES (@SmallID, CAST(REPLICATE(CAST('a' AS varchar(max)), 16 * 1024) AS varbinary(max)))
INSERT INTO [T] VALUES (@MediumID, CAST(REPLICATE(CAST('a' AS varchar(max)), 4 * 1024 * 1024) AS varbinary(max)))
INSERT INTO [T] VALUES (@LargeID, CAST(REPLICATE(CAST('a' AS varchar(max)), 50 * 1024 * 1024) AS varbinary(max)))
-- Do this before test
CHECKPOINT
DBCC DROPCLEANBUFFERS
BEGIN TRAN
-- Do one of these deletes to measure results or profile
DELETE FROM [T] WHERE ID = @SmallID
DELETE FROM [T] WHERE ID = @MediumID
DELETE FROM [T] WHERE ID = @LargeID
-- Do this after test
ROLLBACK
Вот некоторые результаты профилирования удалений на моей рабочей станции:
| Тип столбца | Удалить размер | Продолжительность (мс) | Читает | Пишет | Процессор | -------------------------------------------------- ------------------ | VarBinary | 16 кб | 40 | 13 | 2 | 0 | | VarBinary | 4 МБ | 952 | 2318 | 2 | 0 | | VarBinary | 50 МБ | 2976 | 28594 | 1 | 62 | -------------------------------------------------- ------------------ | FileStream | 16 кб | 1 | 12 | 1 | 0 | | FileStream | 4 МБ | 0 | 9 | 0 | 0 | | FileStream | 50 МБ | 1 | 9 | 0 | 0 |
Мы не можем просто использовать вместо этого файловый поток, потому что:
- Наше распределение данных по размеру не гарантирует этого.
- На практике мы добавляем данные во множество блоков, а файловый поток не поддерживает частичные обновления. Нам нужно разработать вокруг этого.
Обновление 1
Протестировал теорию, согласно которой данные записываются в журнал транзакций как часть удаления, и, похоже, это не так. Я проверяю это неправильно? См. ниже.
SELECT MAX([Current LSN]) FROM fn_dblog(NULL, NULL)
--0000002f:000001d9:0001
BEGIN TRAN
DELETE FROM [T] WHERE ID = @ID
SELECT
SUM(
DATALENGTH([RowLog Contents 0]) +
DATALENGTH([RowLog Contents 1]) +
DATALENGTH([RowLog Contents 3]) +
DATALENGTH([RowLog Contents 4])
) [RowLog Contents Total],
SUM(
DATALENGTH([Log Record])
) [Log Record Total]
FROM fn_dblog(NULL, NULL)
WHERE [Current LSN] > '0000002f:000001d9:0001'
Для файла размером более 5 МБ возвращается 1651 | 171860
.
Кроме того, я ожидаю, что сами страницы будут грязными, если данные будут записаны в журнал. Кажется, что регистрируются только освобождения, что соответствует тому, что является грязным после удаления.
Обновление 2
Я получил ответ от Пола Рэндала. Он подтвердил тот факт, что он должен прочитать все страницы, чтобы пройти по дереву и найти, какие страницы нужно освободить, и заявил, что нет другого способа посмотреть, какие страницы. Это наполовину ответ на 1 и 2 (хотя и не объясняет необходимость блокировок для данных вне строки, но это маленькая картошка).
Вопрос 3 все еще открыт: зачем освобождать страницы заранее, если уже есть фоновая задача для очистки от удалений?
И, конечно же, главный вопрос: существует ли способ напрямую смягчить (то есть не обойти) это поведение удаления, зависящее от размера? Я думаю, что это будет более распространенной проблемой, если только мы не единственные, кто хранит и удаляет 50 МБ строк в SQL Server? Все ли вокруг работают с какой-то формой работы по сбору мусора?