Проблема TOP (1) и MIN. Почему SQL Server использует кластеризованное сканирование, а не ищет индекс покрытия? - PullRequest
1 голос
/ 07 октября 2019

Короткая версия

Я знаю, какая из них лучше - но почему?

--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);

Как и ожидалось,при этом выполняется сканирование кластерного индекса:

enter image description here

Когда сканирование кластерного индекса занимает 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 логических чтения:

enter image description here

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

SELECT MIN(RowNumber) FROM Transactions WHERE TransactionDate >= '20191002 04:00:00.000' OPTION(RECOMPILE)

На самом деле он никогда не выполняетсявозвращает, так что я могу получить только оценочный план выполнения:

enter image description here

Почему 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!?" .

Потому что он охватывает то, что нам нужно:

enter image description here

Отлично работает на таблице строк 4.2M

Я вернулся к старой копиибаза данных (4,2 млн строк против 4,5 млн сегодня) и:

  • правильно использует индекс покрытия CREATE INDEX ... (TransactionDate)
  • для возврата RowNumber
  • in оба случая

Так что должно быть чем-то связанным со статистикой и оптимизатором.

Но в обоих случаях статистика работаетна сегодняшний день с FULLSCAN.

Именно здесь мне нужен кто-то более осведомленный об оптимизаторе, чем я. Но так как это всего лишь один безответный запрос среди миллионов,и когда люди уже проголосовали против него из-под контроля, я никогда не получу объяснения:

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

Дамп статистики содержит все, что оптимизаторзнает о данных - ответ должен быть там.

1 Ответ

4 голосов
/ 07 октября 2019

Несмотря на то, что RowNumber, ключ кластеризованного индекса, является значением ключа в IX_Transactions_TransactionDate, ключи индекса сначала упорядочиваются по TransactionDate, а затем по RowNumber. MIN(RowNumber) не может быть в первой строке с TransactionDate >= '20191002 04:00:00.000'.

. Рассмотрим, содержит ли IX_Transactions_TransactionDate ключевые значения:

(20191002 04:00:00.000,10),
(20191002 05:00:00.000,11),
(20191002 06:00:00.000,1)

Результат

SELECT MIN(RowNumber) FROM FintracTransactions WHERE TransactionDate >= '20191002 04:00:00.000' OPTION(RECOMPILE)

равно 1. В то время как результат:

SELECT TOP(1) RowNumber FROM FintracTransactions WHERE TransactionDate >= '20191002 04:00:00.000' ORDER BY TransactionDate OPTION(RECOMPILE)

равен 10.

Таким образом, реальный выбор оптимизатора - сканировать каждое значение из IX_Transactions_TransactionDateпосле целевой даты или для сканирования кластеризованного индекса с начала, пока он не найдет первую строку с соответствующей TransactionDate.

Вы должны увидеть, что план выполнения для:

SELECT MIN(RowNumber) FROM [Transactions] with (index=[IX_Transactions_TransactionDate]) WHERE TransactionDate >= '20191002 04:00:00.000' OPTION(RECOMPILE)

имеет более высокую оценочную стоимость, чем план выполнения для:

SELECT MIN(RowNumber) FROM [Transactions]  WHERE TransactionDate >= '20191002 04:00:00.000' OPTION(RECOMPILE)
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...