SQL почему SELECT COUNT (*), MIN (столбец), MAX (столбец) быстрее, чем SELECT MIN (столбец), MAX (столбец) - PullRequest
15 голосов
/ 20 сентября 2011

Мы видим огромную разницу между этими запросами.

Медленный запрос

SELECT MIN(col) AS Firstdate, MAX(col) AS Lastdate 
FROM table WHERE status = 'OK' AND fk = 4193

Стол «стол». Число сканирований 2, логическое чтение 2458969, физическое чтение 0, чтение с опережением 0, логическое чтение с 0, физическое чтение с 0, чтение с опережением 0.

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

Быстрый запрос

SELECT count(*), MIN(col) AS Firstdate, MAX(col) AS Lastdate 
FROM table WHERE status = 'OK' AND fk = 4193

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

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

Вопрос

В чем причина огромной разницы в производительности между запросами?

Обновление Небольшое обновление на основе вопросов, заданных в качестве комментариев:

Порядок выполнения или повторного выполнения ничего не меняет с точки зрения производительности. Не используются дополнительные параметры, и (тестовая) база данных больше ничего не делает во время выполнения.

Медленный запрос

|--Nested Loops(Inner Join)
 |--Stream Aggregate(DEFINE:([Expr1003]=MIN([DBTest].[dbo].[table].[startdate])))
   |    |--Top(TOP EXPRESSION:((1)))
   |         |--Nested Loops(Inner Join, OUTER REFERENCES:([DBTest].[dbo].[table].[id], [Expr1008]) WITH ORDERED PREFETCH)
   |              |--Index Scan(OBJECT:([DBTest].[dbo].[table].[startdate]), ORDERED FORWARD)
   |              |--Clustered Index Seek(OBJECT:([DBTest].[dbo].[table].[PK_table]), SEEK:([DBTest].[dbo].[table].[id]=[DBTest].[dbo].[table].[id]),  WHERE:([DBTest].[dbo].[table].[FK]=(5806) AND [DBTest].[dbo].[table].[status]<>'A') LOOKUP ORDERED FORWARD)
   |--Stream Aggregate(DEFINE:([Expr1004]=MAX([DBTest].[dbo].[table].[startdate])))
        |--Top(TOP EXPRESSION:((1)))
             |--Nested Loops(Inner Join, OUTER REFERENCES:([DBTest].[dbo].[table].[id], [Expr1009]) WITH ORDERED PREFETCH)
                  |--Index Scan(OBJECT:([DBTest].[dbo].[table].[startdate]), ORDERED BACKWARD)
                  |--Clustered Index Seek(OBJECT:([DBTest].[dbo].[table].[PK_table]), SEEK:([DBTest].[dbo].[table].[id]=[DBTest].[dbo].[table].[id]),  WHERE:([DBTest].[dbo].[table].[FK]=(5806) AND [DBTest].[dbo].[table].[status]<>'A') LOOKUP ORDERED FORWARD)

Быстрый запрос

 |--Compute Scalar(DEFINE:([Expr1003]=CONVERT_IMPLICIT(int,[Expr1012],0)))
   |--Stream Aggregate(DEFINE:([Expr1012]=Count(*), [Expr1004]=MIN([DBTest].[dbo].[table].[startdate]), [Expr1005]=MAX([DBTest].[dbo].[table].[startdate])))
        |--Nested Loops(Inner Join, OUTER REFERENCES:([DBTest].[dbo].[table].[id], [Expr1011]) WITH UNORDERED PREFETCH)
             |--Index Seek(OBJECT:([DBTest].[dbo].[table].[FK]), SEEK:([DBTest].[dbo].[table].[FK]=(5806)) ORDERED FORWARD)
             |--Clustered Index Seek(OBJECT:([DBTest].[dbo].[table].[PK_table]), SEEK:([DBTest].[dbo].[table].[id]=[DBTest].[dbo].[table].[id]),  WHERE:([DBTest].[dbo].[table].[status]<'A' OR [DBTest].[dbo].[table].[status]>'A') LOOKUP ORDERED FORWARD)

The execution plan from SSMS

Ответ

Ответ, данный ниже Мартином Смитом, кажется, объясняет проблему. Супер короткая версия состоит в том, что анализатор запросов MS-SQL неправильно использует план запроса в медленном запросе, который вызывает полное сканирование таблицы.

Добавление счетчика (*), подсказки запроса с помощью (FORCESCAN) или комбинированного индекса в столбцах startdate, FK и status устраняет проблему производительности.

1 Ответ

25 голосов
/ 20 сентября 2011

Оценщик мощности SQL Server делает различные предположения моделирования, такие как

  • Независимость. Распределение данных по разным столбцам является независимым, если не доступна информация о корреляции.
  • Однородность: на каждом шаге гистограммы объекта статистики равномерно распределены различные значения, и каждое значение имеет одинаковую частоту.

Источник

В таблице 810 064 строки.

У вас есть запрос

SELECT COUNT(*),
       MIN(startdate) AS Firstdate,
       MAX(startdate) AS Lastdate
FROM   table
WHERE  status <> 'A'
       AND fk = 4193 

1 893 (0,23%) строки соответствуют предикату fk = 4193, а из этих двух нет части status <> 'A', поэтому в целом 1891 совпадают и их необходимо агрегировать.

У вас также есть два индекса, ни один из которых не охватывает весь запрос.

Для вашего быстрого запроса он использует индекс на fk, чтобы непосредственно найти строки, где fk = 4193 затем необходимо выполнить 1893 поиск ключей , чтобы найти каждую строку в кластерном индексе для проверки status предикат и извлечение startdate для агрегации.

При удалении COUNT(*) из списка SELECT SQL Server больше не имеет для обработки каждой подходящей строки. В результате он рассматривает другой вариант.

У вас есть индекс на startdate, чтобы он мог начать его сканирование с самого начала, выполняя поиск по ключам обратно к базовой таблице и, как только он находит первую подходящую остановку строки, как он обнаружил MIN(startdate), аналогично MAX можно найти при другом сканировании, начиная с другого конца индекса и возвращаясь назад.

По оценкам SQL Server, при каждом из этих сканирований будет обработано 590 строк, прежде чем они попадут в одну, соответствующую предикату. Общее количество поисков составляет 1180 против 1893, поэтому он выбирает этот план.

590 цифра просто table_size / estimated_number_of_rows_that_match. то есть оценщик мощности предполагает, что совпадающие строки будут равномерно распределены по всей таблице.

К сожалению, 1891 строка, которая соответствует предикату, не случайным образом распределена относительно startdate. Фактически все они сжаты в один 8,205-строчный сегмент к концу индекса, что означает, что сканирование, чтобы добраться до MIN(startdate), завершается выполнением 801 859 поисков ключа, прежде чем он может остановиться.

Это можно воспроизвести ниже.

CREATE TABLE T
(
id int identity(1,1) primary key,
startdate datetime,
fk int,
[status] char(1),
Filler char(2000)
)

CREATE NONCLUSTERED INDEX ix ON T(startdate)

INSERT INTO T
SELECT TOP 810064 Getdate() - 1,
                  4192,
                  'B',
                  ''
FROM   sys.all_columns c1,
       sys.all_columns c2  


UPDATE T 
SET fk = 4193, startdate = GETDATE()
WHERE id BETWEEN 801859 and 803748 or id = 810064

UPDATE T 
SET  startdate = GETDATE() + 1
WHERE id > 810064


/*Both queries give the same plan. 
UPDATE STATISTICS T WITH FULLSCAN
makes no difference*/

SELECT MIN(startdate) AS Firstdate, 
       MAX(startdate) AS Lastdate 
FROM T
WHERE status <> 'A' AND fk = 4192


SELECT MIN(startdate) AS Firstdate, 
       MAX(startdate) AS Lastdate 
FROM T
WHERE status <> 'A' AND fk = 4193

Вы можете использовать подсказки запросов, чтобы план использовал индекс на fk вместо startdate, или добавить предложенный отсутствующий индекс, выделенный в плане выполнения на (fk,status) INCLUDE (startdate), чтобы избежать этой проблемы.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...