Це питання пов'язане з поточною темою форуму .
Запуск версії для розробників SQL Server 2008 на моїй робочій станції та двовузловий кластер віртуальних машин Enterprise Edition, де я посилаюся на "альфа-кластер".
Час, необхідний для видалення рядків з варбінарного (макс.) Стовпця, безпосередньо пов'язане з довжиною даних у цьому стовпчику. Спочатку це може здатися інтуїтивно зрозумілим, але після розслідування це суперечить моєму розумінню того, як SQL Server насправді видаляє рядки взагалі та обробляє такі дані.
Проблема випливає з проблеми очікування на видалення (> 30 секунд), яке ми бачимо у нашому веб-додатку .NET, але я спростив його заради цього обговорення.
Коли запис видаляється, SQL Server відзначає його як привид, який слід очистити за допомогою завдання очищення Ghost пізніше після здійснення транзакції (див . Блог Пола Рандала ). У тесті видалення трьох рядків з 16 КБ, 4 МБ та 50 МБ даних у варбінарному (макс.) Стовпчику, відповідно, я бачу, що це відбувається на сторінці із рядковою частиною даних, а також у транзакції журнал.
Що мені здається дивним, це те, що блокування X розміщуються на всіх сторінках даних LOB під час видалення, а сторінки розміщуються в 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). Це просто розсилає їх.
Щоб додатково довести, що пошуки сторінок викликають проблему, я спробував той самий тест, використовуючи стовпець файлового потоку замість ванільного варбінату (макс.). Видалення було постійним часом, незалежно від розміру LOB.
Отже, спочатку мої академічні питання:
- Чому SQL Server повинен шукати всі сторінки даних LOB, щоб 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? Чи всі інші там вирішують якусь форму роботи зі збору сміття?