Удалить производительность для данных больших объектов в SQL Server


16

Этот вопрос связан с этой веткой форума .

Запуск 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_statsDMV ( 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.

Итак, сначала мои академические вопросы:

  1. Почему SQL Server должен искать все страницы данных больших объектов, чтобы X заблокировал их? Это просто деталь того, как блокировки представлены в памяти (как-то хранятся вместе со страницей)? Это делает влияние ввода-вывода сильно зависит от размера данных, если не полностью кэшируется.
  2. Почему X блокируется вообще, просто чтобы освободить их? Разве этого недостаточно, чтобы заблокировать только лист индекса с частью строки, так как освобождение не должно изменять сами страницы? Есть ли другой способ получить данные LOB, от которых защищает блокировка?
  3. Зачем вообще освобождать страницы, учитывая, что уже есть фоновая задача, посвященная этому типу работы?

И, может быть, более важный мой практический вопрос:

  • Есть ли способ заставить удаления работать по-другому? Моя цель состоит в постоянном удалении независимо от размера, аналогично файловому потоку, где любая очистка происходит в фоновом режиме после факта. Это вещь конфигурации? Я храню вещи странно?

Вот как воспроизвести описанный тест (выполняется через окно запроса 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. Наше распределение данных по размеру не гарантирует этого.
  2. На практике мы добавляем данные во множество блоков, а файловый поток не поддерживает частичные обновления. Нам нужно разработать вокруг этого.

Обновление 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? Все ли вокруг работают с какой-то формой работы по сбору мусора?


Я хотел бы найти лучшее решение, но не нашел его. У меня есть ситуация регистрации больших объемов строк различного размера, до 1 МБ +, и у меня есть процесс «очистки» для удаления старых записей. Поскольку удаления были очень медленными, мне пришлось разбить его на два этапа - сначала удалить ссылки между таблицами (что очень быстро), затем удалить потерянные строки. Задание на удаление в среднем составляло ~ 2,2 секунды / МБ для удаления данных. Поэтому, конечно, мне пришлось уменьшить количество конфликтов, поэтому у меня есть хранимая процедура с «DELETE TOP (250)» внутри цикла, пока строки больше не удаляются.
Абак

Ответы:


5

Я не могу сказать, почему именно было бы гораздо более неэффективно удалять VARBINARY (MAX) по сравнению с файловым потоком, но одну идею вы могли бы рассмотреть, если вы просто пытаетесь избежать тайм-аутов из вашего веб-приложения при удалении этих LOBS. Вы можете хранить значения VARBINARY (MAX) в отдельной таблице (назовем ее tblLOB), на которую ссылается исходная таблица (назовем это tblParent).

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


Благодарю. Это как раз один из наших вариантов на доске. Таблица представляет собой файловую систему, и в настоящее время мы находимся в процессе разделения двоичных данных в совершенно отдельной базе данных из мета-иерархии. Мы могли бы либо сделать, как вы сказали, и удалить строку иерархии, и позволить процессу GC очистить потерянные строки больших объектов. Или удалите временную метку с данными для достижения той же цели. Это путь, по которому мы можем пойти, если нет удовлетворительного ответа на проблему.
Джереми Розенберг

1
Я был бы осторожен с отметкой времени, указывающей, что она удалена. Это будет работать, но тогда у вас будет много занятого пространства в активных строках. В какой-то момент вам понадобится какой-то процесс gc, в зависимости от того, сколько удалено, и будет меньше влиять на меньшее удаление на регулярной основе, а не на случайные партии.
Ян Чемберленд
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.