Короткая версия
Я знаю, какая из них лучше - но почему?
--Clustered index scan of 4.5M rows / 2.1 GB
SELECT MIN(RowNumber) FROM Transactions WHERE TransactionDate >= '20191002 04:00:00.000' OPTION(RECOMPILE)
--Covering index seek; 3 logical reads
SELECT TOP(1) RowNumber FROM Transactions WHERE TransactionDate >= '20191002 04:00:00.000' ORDER BY TransactionDate OPTION(RECOMPILE)
Длинная версия
У меня есть таблица:
Транзакции
RowNumber TransactionDate
(int, clustered) (datetime)
----------------- -----------------------
4592515 2019-10-07 11:12:13.690
4592516 2019-10-07 11:12:13.690
4592517 2019-10-07 11:12:18.660
4592518 2019-10-07 11:12:22.960
4592519 2019-10-07 11:13:16.587
4592520 2019-10-07 11:13:22.310
4592521 2019-10-07 11:14:50.060
4592522 2019-10-07 11:15:15.073
4592523 2019-10-07 11:15:32.860
4592524 2019-10-07 11:16:12.360
Я хочу получить первый RowNumber или после определенного времени, например:
SELECT MIN(RowNumber) FROM Transactions WHERE TransactionDate >= '20191007 11:13:00' OPTION(RECOMPILE);
Как и ожидалось,при этом выполняется сканирование кластерного индекса:
Когда сканирование кластерного индекса занимает 4,5 млн строк и 2,1 ГБ, вы должны избегать этого каждые несколькосекунд. На практике этот запрос занимает так много времени, что он никогда не возвращается.
Индекс покрытия для TransactionDate
Меня интересуют TransactionDate и RowNumber , поэтому я создаю для него индекс
CREATE INDEX IX_Transactions_TransactionDate ON Transactions
(
TransactionDate
)
(поскольку RowNumber - это уникальный ключ кластера, он неявно будет частью индекса).
Повторно выполнить запрос
И теперь я выполню логически идентичный запрос:
SELECT TOP(1) RowNumber FROM Transactions ORDER BY TransactionDate OPTION (RECOMPILE)
И это, как и ожидалось, просматривает новый индекс покрытия, возвращаясь сразу после3 логических чтения:
Исходный эквивалентный запрос не использует индекс покрытия:
SELECT MIN(RowNumber) FROM Transactions WHERE TransactionDate >= '20191002 04:00:00.000' OPTION(RECOMPILE)
На самом деле он никогда не выполняетсявозвращает, так что я могу получить только оценочный план выполнения:
Почему TOP(1)
ведет себя так сильно отличается, и лучше чем MIN
?
Другие вопросы указывают на то, что:
И люди будут пытаться уговорить:
Хорошо, попробуйте оба, и посмотрите, что лучше.
Да, я сделал это. Теперь я пытаюсь выяснить , почему SQL Server не выполняет запрос так, как должен.
Это должна быть статистика
Единственное оправдание, что сервер долженВыполнить два логически эквивалентных запроса так ужасно по-разному, будет статистика.
Статистика кластеризованного индекса
Name Updated Rows Rows Sampled Steps Density Average key
------------------------- ----------------- ------- ------------ ------ ------- -----------
IX_Transactions_RowNumber Oct 7 2019 2:32AM 4561899 4561899 5 1 4
All density Average Length Columns
------------- -------------- ---------
2.19207E-07 4 RowNumber
RANGE_HI_KEY RANGE_ROWS EQ_ROWS DISTINCT_RANGE_ROWS AVG_RANGE_ROWS
------------ ------------- ------------- -------------------- --------------
9171 0 1 0 1
1700836 1687750 1 1687750 1
2755345 1048575 1 1048575 1
4592121 1825569 1 1825569 1
4592122 0 1 0 1
Статистика покрытия индекса
Name Updated Rows Rows Sampled Steps Density Average key length
-------------------------------------- ----------------- ------- ------------ ----- --------- ------------------
IX_Transactions_TransactionDate Oct 7 2019 2:33AM 4561899 4561899 120 0.8462854 12
All density Average Length Columns
------------- -------------- --------------------------
2.590376E-07 8 TransactionDate
2.19207E-07 12 TransactionDate, RowNumber
RANGE_HI_KEY RANGE_ROWS EQ_ROWS DISTINCT_RANGE_ROWS AVG_RANGE_ROWS
----------------------- ------------- ------------- -------------------- --------------
...snip...
2019-09-22 03:06:09.883 32767 1 27923 1.173477
2019-10-02 19:10:18.007 32767 1 27714 1.182327
2019-10-07 02:30:21.680 14599 2 12430 1.174497
2019-10-07 02:31:56.807 0 1 0 1
Итак, я должен спросить:
- , были ли вы оптимизатором
- и у вас были эти два логически эквивалентных запроса
- и эта статистика
- почему вы решили сканировать 4.5M строк таблицы
- , а не искать точный ответ?
tl; dr:
--Clustered index scan of 4.5M rows / 2.1 GB
SELECT MIN(RowNumber) FROM Transactions WHERE TransactionDate >= '20191002 04:00:00.000' OPTION(RECOMPILE)
--Covering index seek; 3 logical reads
SELECT TOP(1) RowNumber FROM Transactions WHERE TransactionDate >= '20191002 04:00:00.000' ORDER BY TransactionDate OPTION(RECOMPILE)
почему?
Бонусная болтовня
Я пытаюсь подвергнуть цензуре некоторые имена таблиц. Правительства обидчивы на подобные вещи. Я включил OPTION(RECOMPILE)
на тот случай, если кто-нибудь попытается махнуть рукой о кэшированных планах выполнения. Из курса у вас нет этого в производстве. duh-doy
Примеры сценариев
Эти примеры сценариев включают несвязанные столбцы и индексы:
CREATE TABLE Transactions (
Column1 varchar(50) NOT NULL,
RowNumber int NOT NULL,
Column3 varchar(50) NULL,
Column4 varchar(50) NULL,
Column5 varchar(50) NULL,
Column6 varchar(50) NULL,
Column7 varchar(50) NULL,
Column8 varchar(50) NULL,
Column9 varchar(50) NULL,
Column10 varchar(50) NULL,
Column11 varchar(50) NULL,
Column12 varchar(50) NULL,
Column13 varchar(50) NULL,
Column14 varchar(50) NULL,
TransactionDate datetime NOT NULL,
Column16 varchar(50) NULL,
Column17 varchar(50) NULL,
Column18 varchar(50) NULL,
Column19 varchar(50) NULL,
Column20 varchar(50) NULL,
Column21 varchar(50) NULL,
Column22 varchar(50) NULL,
Column23 varchar(50) NULL,
Column24 varchar(50) NULL,
Column25 varchar(50) NULL,
Column26 varchar(50) NULL,
Column27 varchar(50) NULL
)
CREATE NONCLUSTERED INDEX [IX_Tranasctions_Index1] ON [dbo].[Transactions] (Column7 ASC) INCLUDE (Column12)
CREATE NONCLUSTERED INDEX [IX_Transactions_Index2] ON [dbo].[Transactions] (Column13 ASC)
CREATE NONCLUSTERED INDEX [IX_Transactions_Index3] ON [dbo].[Transactions] (Column5 ASC, TransactionDate ASC) INCLUDE (Column1, Column7, Column11, Column16, Column17, Column18) WHERE (Column5='1')
CREATE NONCLUSTERED INDEX [IX_Transactions_Index4] ON [dbo].[Transactions] (Column11 ASC) INCLUDE (Column7)
CREATE UNIQUE CLUSTERED INDEX [IX_Transactions_RowNumber] ON [dbo].[Transactions] ([RowNumber] ASC)
CREATE NONCLUSTERED INDEX [IX_Transactions_TransactionDate] ON [dbo].[Transactions] (TransactionDate)
CREATE NONCLUSTERED INDEX [IX_Transactions_Index7] ON [dbo].[Transactions] (Column9)
CREATE NONCLUSTERED INDEX [IX_Transactions_Index8] ON [dbo].[Transactions] (Column7, Column8) WHERE (Column7 IS NOT NULL)
CREATE NONCLUSTERED INDEX [IX_Transactions_Index9] ON [dbo].[Transactions] (Column13) INCLUDE (Column7)
ALTER TABLE [dbo].[Transactions] ADD CONSTRAINT [PK_Transactions] PRIMARY KEY NONCLUSTERED (Column1)
Индексы содержат ключ кластера
Похоже, что некоторые люди не понимают, как работает индекс.
Эта таблица уникальна - кластеризована по RowNumber . Это означает, что RowNumber однозначно идентифицирует строку.
Позволяет создать гипотетическую таблицу Customers, сгруппированную по CustomerID:
| CustomerID | FirstName | LastName |
|------------|-----------|-------------|
| 1 | Ian | Boyd |
| 2 | Tim | Biegeleisen |
| 3 | Gordon | Linoff |
Когда приходит время создавать некластеризованный покрывающий индекс, вы указываете данные, которые хотите индексировать. Например, для гипотетического индекса покрытия по имени + фамилии:
| FirstName | LastName |
|-----------|----------|
Это означает, что буквально база данных будет хранить:
| FirstName | LastName |
|------------|-------------|
| Gordon | Linoff |
| Ian | Boyd |
| Tim | Biegeleisen |
Но это еще не все, что она будет хранить. Он также должен хранить значение ключа кластера.
Каждая запись в индексе должна указывать на исходную строку данных, на которую указывает запись в индексе.
Таким образом, внутренне индекс содержит еще один столбец - значение кластера:
| FirstName | LastName || Cluster key |
|-----------|----------||-------------|
Что в нашем случае в CustomerID :
| | || Cluster key |
| FirstName | LastName || (i.e. CustomerID) |
|------------|-------------||-------------------|
| Gordon | Linoff || 3 |
| Ian | Boyd || 1 |
| Tim | Biegeleisen || 2 |
Вот классная вещь: если у вас был запрос, который использовал индекс, этот запрос может вернуть значение ключа кластера , не возвращаясь к исходной полной таблице - потому что CustomerID уже существует в индексе!
SELECT CustomerID FROM Customers WHERE FirstName = 'Ian'
База данных может использовать ваш охватывающий индекс для возврата CustomerID- , даже если вы не указали CustomerID в своем индексе . Довольно круто, да?
Вы даже можете проверить это сами.
И вы даже можете увидеть это на моих оригинальных скриншотах выше (поскольку SQL Server сделал это по запросу). Вы также можете проверить это, посмотрев на showplan:
|--Top(TOP EXPRESSION:((1)))
|--Index Seek(OBJECT:(Transactions.IX_Transactions_TransactionDate]),
SEEK:(Transactions.[TransactionDate] >= '2019-10-02 04:00:00.000') ORDERED FORWARD)
Вы можете также увидеть это в статистике, которую я включил выше:
All density Average Length Columns
------------- -------------- --------------------------
2.590376E-07 8 TransactionDate
2.19207E-07 12 TransactionDate, RowNumber
Вы также можетепосмотрите это в плане выполнения: верните столбец из индекса, когда "индекс даже не содержит этот столбец - как вы можете даже назвать это покрывающим idnex!?" .
Потому что он охватывает то, что нам нужно:
Отлично работает на таблице строк 4.2M
Я вернулся к старой копиибаза данных (4,2 млн строк против 4,5 млн сегодня) и:
- правильно использует индекс покрытия
CREATE INDEX ... (TransactionDate)
- для возврата
RowNumber
- in оба случая
Так что должно быть чем-то связанным со статистикой и оптимизатором.
Но в обоих случаях статистика работаетна сегодняшний день с FULLSCAN
.
Именно здесь мне нужен кто-то более осведомленный об оптимизаторе, чем я. Но так как это всего лишь один безответный запрос среди миллионов,и когда люди уже проголосовали против него из-под контроля, я никогда не получу объяснения:
- почему для двух запросов, которые логически эквивалентны
- и имеют все необходимоев уже отсортированном индексе
- один выбирает использовать индекс
- , а другой выполняет полное сканирование таблицы
Дамп статистики содержит все, что оптимизаторзнает о данных - ответ должен быть там.