Данные изначально упорядочены, как если бы это был кластерный индекс

8

У меня есть следующая таблица с 7,5 миллионами записей:

CREATE TABLE [dbo].[TestTable](
    [Id] [int] IDENTITY(1,1) NOT NULL,
    [TestCol] [nvarchar](50) NOT NULL,
    [TestCol2] [nvarchar](50) NOT NULL,
    [TestCol3] [nvarchar](50) NOT NULL,
    [Anonymised] [tinyint] NOT NULL,
    [Date] [datetime] NOT NULL,
CONSTRAINT [PK_TestTable] PRIMARY KEY CLUSTERED 
(
    [Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, 
ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

Я заметил, что когда в поле даты есть некластеризованный индекс:

CREATE NONCLUSTERED INDEX IX_TestTable_Date ON [dbo].[TestTable] ([Date])

-и я запускаю следующий запрос:

UPDATE TestTable 
SET TestCol='*GDPR*', TestCol2='*GDPR*', TestCol3='*GDPR*', Anonymised=1
WHERE [Date] <= '25 August 2016'

- поступающие данные, возвращаемые операцией доступа к индексу, сортируются в соответствии с порядком клавиш PK / CX, что снижает производительность.

План запроса

Я был удивлен, обнаружив, что удаление индекса из поля даты фактически повышает производительность запроса примерно на 30%, поскольку он больше не выполняет сортировку:

План запроса

Моя теория, и это может быть очевидно для более опытных из вас, заключается в том, что она выяснила, что столбец даты неявно упорядочен точно так же, как первичный ключ / кластерный индекс.

Поэтому мой вопрос: возможно ли воспользоваться этим фактом для повышения производительности моего запроса?

AproposArmadillo
источник
1
Я не смотрел на планы, но подозреваю, что производительность (ну, продолжительность, ни один из этих бесполезных показателей% стоимости) улучшилась, потому что больше не нужно было обновлять индекс, который вы удалили, а не из-за операции сортировки.
Аарон Бертран
@AaronBertrand Возможно, я читаю их неправильно, поэтому, пожалуйста, исправьте меня, если я ошибаюсь, но в обоих планах запросов, похоже, есть операция обновления индекса. Вы имеете в виду что-то еще?
поводу Армадильо
1
Я снова сказал, что не смотрю на планы. Вы сказали, что «удаление индекса из поля даты повышает производительность запроса» ... если вы удалили индекс, он не должен отображаться в плане, поэтому, возможно, вы собрали неправильный план или на самом деле не удалили Индекс вы думали, что вы сделали. И еще раз, некоторый расчетный% для плана является индикатором, но на самом деле никак не отражает истинное измерение производительности. Это оценка, которая рассчитывается еще до запуска запроса.
Аарон Бертран
@ Аарон Бертран, ему все равно не нужно было обновлять индекс, потому что [Дата] не была среди обновленных полей.
Денис Рубашкин
1
@Shaffanhoon Вы пытались воссоздать индекс, [Date]но по DESCпорядку? Просто любопытно, так как предикат есть <=. Кроме того, если индекс on Date(по умолчанию, ACSorder) помогает другим запросам, то, возможно, вы можете попробовать добавить табличную подсказку в UPDATE, чтобы заставить его использовать PK? Или, может быть, разбить это на две части: создать временную таблицу, заполнить [Id]на основе [Date] <= '25 August 2016', а затем удалить WHEREиз ОБНОВЛЕНИЕ и добавить FROM dbo.TestTable tt INNER JOIN #tmp ids ON ids.[Id] = tt.[Id]. В конце концов, это ОБНОВЛЕНИЕ, и оно должно найти фактические строки, индекс или нет.
Соломон Руцкий

Ответы:

7

Я смоделировал тестовые данные, которые в основном воспроизводят вашу проблему:

INSERT INTO [dbo].[TestTable] WITH (TABLOCK)
SELECT TOP (7000000) N'*NOT GDPR*', N'*NOT GDPR*', N'*NOT GDPR*', 0, DATEADD(DAY, q.RN  / 16965, '20160801')
FROM
(
    SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) RN
    FROM master..spt_values t1
    CROSS JOIN master..spt_values t2
) q
ORDER BY q.RN
OPTION (MAXDOP 1);


DROP INDEX IF EXISTS [dbo].[TestTable].IX_TestTable_Date;
CREATE NONCLUSTERED INDEX IX_TestTable_Date ON [dbo].[TestTable] ([Date]);

Статистика для запроса, который использует некластеризованный индекс:

Таблица «TestTable». Сканирование 1, логическое чтение 1299838, физическое чтение 0, чтение с опережением 0, логическое чтение 1, физическое чтение 1, чтение с опережением 0.

Время выполнения SQL Server: время ЦП = 984 мс, прошедшее время = 988 мс.

Статистика для запроса, использующего кластерный индекс:

Таблица «TestTable». Сканирование 1, логическое чтение 72609, физическое чтение 0, чтение с опережением 0, логическое чтение 1, физическое чтение 1, чтение с опережением 0.

Время выполнения SQL Server: время ЦП = 781 мс, прошедшее время = 772 мс.

Как добраться до вашего вопроса:

Можно ли воспользоваться этим фактом для повышения производительности моего запроса?

Да. Вы можете использовать некластеризованный индекс, который вам уже idнужен , чтобы эффективно найти максимальное значение, которое необходимо обновить. Если вы сохраните это в переменной и отфильтруете ее, вы получите план запроса для обновления, которое выполняет сканирование кластерного индекса (без сортировки), которое останавливается рано и, следовательно, выполняет меньше операций ввода-вывода. Вот одна из реализаций:

DECLARE @Id INT;

SELECT TOP (1) @Id = Id
FROM dbo.TestTable 
WHERE [Date] <= '25 August 2016'
ORDER BY [Date] DESC, Id DESC;

UPDATE TestTable 
SET TestCol='*GDPR*', TestCol2='*GDPR*', TestCol3='*GDPR*', Anonymised=1
WHERE [Id] < @Id AND [Date] <= '25 August 2016'
AND [Anonymised] <> 1 -- optional
OPTION (MAXDOP 1);

Запустите статистику для нового запроса:

Таблица «TestTable». Сканирование 1, логическое чтение 3, физическое чтение 0, чтение с опережением 0, логическое чтение с бита 0, физическое чтение с бита 0, чтение с опережением чтения 0.

Таблица «TestTable». Сканирование 1, логическое чтение 4776, физическое чтение 0, чтение с опережением 0, логическое чтение с 0, физическое чтение с 0, чтение с опережением 0.

Время выполнения SQL Server: время ЦП = 515 мс, прошедшее время = 510 мс.

А также план запроса:

хорошо план запроса

С учетом всего вышесказанного, ваше желание ускорить запрос подсказывает мне, что вы планируете выполнять запрос более одного раза. Прямо сейчас у вашего запроса есть открытый фильтр в dateстолбце. Действительно ли необходимо анонимизировать строки более одного раза? Можете ли вы избежать обновления или сканирования уже анонимных строк? Конечно, должно быть быстрее обновить диапазон дат с датами по обе стороны от него. Вы также можете добавить Anonymisedстолбец в свой индекс, но этот индекс необходимо будет обновить во время вашего UPDATEзапроса. Таким образом, по возможности, избегайте повторной обработки одних и тех же данных.

Исходный запрос, который у вас есть с сортировкой, медленнее из-за работы, выполняемой в Clustered Index Updateоператоре. Время, затраченное на поиск и сортировку индекса, составляет всего 407 мс. Вы можете увидеть это в фактическом плане. План выполняется в режиме строки, поэтому время, затраченное на сортировку, равно времени этого оператора вместе с каждым дочерним оператором:

введите описание изображения здесь

Это оставляет оператору сортировки около 1600 мс времени. SQL Server должен прочитать страницы из кластерного индекса, чтобы выполнить обновление. Вы можете видеть, что Clustered Index Updateоператор выполняет 1205921 логическое чтение. Вы можете прочитать больше о сортировке оптимизированная для DML и оптимизированный предвыборку в этом блоге по Пол Уайт .

Другой план запроса (без сортировки) занимает 683 мс для сканирования кластерного индекса и около 550 мс для Clustered Index Updateоператора. Оператор обновления не выполняет ввода-вывода для этого запроса.

Простой ответ о том, почему план с сортировкой медленнее, заключается в том, что SQL Server выполняет больше логических операций чтения кластерного индекса для этого плана по сравнению с планом сканирования кластерного индекса. Даже если все необходимые данные находятся в памяти, затраты на выполнение этих логических чтений все равно остаются высокими. Гораздо труднее получить лучший ответ, поскольку, насколько я знаю, планы не дадут вам дальнейших подробностей. Для сравнения стеков вызовов между запросами можно использовать PerfView или другой инструмент, основанный на трассировке ETW:

введите описание изображения здесь

Слева находится запрос, который выполняет сканирование кластерного индекса, а справа - запрос, который выполняет сортировку. Я пометил стеки вызовов синим или красным цветом, которые появляются только в одном запросе. Неудивительно, что различные стеки вызовов с большим числом выбранных циклов ЦП для запроса сортировки, по-видимому, связаны с логическими чтениями, необходимыми для выполнения обновления кластерного индекса. Кроме того, существуют различия в количестве циклов выборки между запросами для одной и той же операции. Например, запрос с сортировкой тратит 31 цикл на получение защелок, тогда как запрос на сканирование тратит только 9 циклов на получение защелок.

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

Джо Оббиш
источник