Видаліть продуктивність для даних LOB у SQL Server


16

Це питання пов'язане з поточною темою форуму .

Запуск версії для розробників SQL Server 2008 на моїй робочій станції та двовузловий кластер віртуальних машин Enterprise Edition, де я посилаюся на "альфа-кластер".

Час, необхідний для видалення рядків з варбінарного (макс.) Стовпця, безпосередньо пов'язане з довжиною даних у цьому стовпчику. Спочатку це може здатися інтуїтивно зрозумілим, але після розслідування це суперечить моєму розумінню того, як SQL Server насправді видаляє рядки взагалі та обробляє такі дані.

Проблема випливає з проблеми очікування на видалення (> 30 секунд), яке ми бачимо у нашому веб-додатку .NET, але я спростив його заради цього обговорення.

Коли запис видаляється, SQL Server відзначає його як привид, який слід очистити за допомогою завдання очищення Ghost пізніше після здійснення транзакції (див . Блог Пола Рандала ). У тесті видалення трьох рядків з 16 КБ, 4 МБ та 50 МБ даних у варбінарному (макс.) Стовпчику, відповідно, я бачу, що це відбувається на сторінці із рядковою частиною даних, а також у транзакції журнал.

Що мені здається дивним, це те, що блокування X розміщуються на всіх сторінках даних LOB під час видалення, а сторінки розміщуються в 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). Це просто розсилає їх.

Щоб додатково довести, що пошуки сторінок викликають проблему, я спробував той самий тест, використовуючи стовпець файлового потоку замість ванільного варбінату (макс.). Видалення було постійним часом, незалежно від розміру LOB.

Отже, спочатку мої академічні питання:

  1. Чому SQL Server повинен шукати всі сторінки даних LOB, щоб 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).

Звідси, коли ви видаляєте запис, ви можете просто видалити його з батьківського запису, а потім мати час від часу процес збору сміття, щоб зайти та очистити записи в таблиці LOB. Під час цього процесу вивезення сміття може бути додаткова активність на жорсткому диску, але вона принаймні буде відокремлена від переднього полотна і може виконуватися в не пікові часи.


Спасибі. Це саме один із наших варіантів на дошці. Таблиця - це файлова система, і ми зараз перебуваємо в процесі відокремлення бінарних даних на цілком окрему базу даних від мета ієрархії. Ми можемо зробити так, як ви сказали, і видалити рядок ієрархії, а також GC-процес очистити осиротілі рядки LOB. Або мати часову позначку видалення з даними, щоб досягти тієї ж мети. Це шлях, який ми можемо пройти, якщо немає задоволеної відповіді на проблему.
Джеремі Розенберг

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