Включение ORDER BY в запрос, который не возвращает строк, существенно влияет на производительность

15

При простом соединении из трех таблиц производительность запросов резко меняется, если включить ORDER BY, даже если строки не возвращены. Реальный сценарий проблемы занимает 30 секунд, чтобы вернуть ноль строк, но он мгновенный, когда ORDER BY не включен. Почему?

SELECT * 
FROM tinytable t                          /* one narrow row */
JOIN smalltable s on t.id=s.tinyId        /* one narrow row */
JOIN bigtable b on b.smallGuidId=s.GuidId /* a million narrow rows */
WHERE t.foreignId=3                       /* doesn't match */
ORDER BY b.CreatedUtc          /* try with and without this ORDER BY */

Я понимаю, что у меня мог бы быть индекс на bigtable.smallGuidId, но, я думаю, это действительно ухудшило бы ситуацию в этом случае.

Вот скрипт для создания / заполнения таблиц для тестирования. Любопытно, что кажется, что smalltable имеет поле nvarchar (max). Также, кажется, имеет значение, что я присоединяюсь к bigtable с помощью guid (который, я думаю, заставляет использовать хеш-сопоставление).

CREATE TABLE tinytable
  (
     id        INT PRIMARY KEY IDENTITY(1, 1),
     foreignId INT NOT NULL
  )

CREATE TABLE smalltable
  (
     id     INT PRIMARY KEY IDENTITY(1, 1),
     GuidId UNIQUEIDENTIFIER NOT NULL DEFAULT NEWID(),
     tinyId INT NOT NULL,
     Magic  NVARCHAR(max) NOT NULL DEFAULT ''
  )

CREATE TABLE bigtable
  (
     id          INT PRIMARY KEY IDENTITY(1, 1),
     CreatedUtc  DATETIME NOT NULL DEFAULT GETUTCDATE(),
     smallGuidId UNIQUEIDENTIFIER NOT NULL
  )

INSERT tinytable
       (foreignId)
VALUES(7)

INSERT smalltable
       (tinyId)
VALUES(1)

-- make a million rows 
DECLARE @i INT;

SET @i=20;

INSERT bigtable
       (smallGuidId)
SELECT GuidId
FROM   smalltable;

WHILE @i > 0
  BEGIN
      INSERT bigtable
             (smallGuidId)
      SELECT smallGuidId
      FROM   bigtable;

      SET @i=@i - 1;
  END 

Я тестировал на SQL 2005, 2008 и 2008R2 с теми же результатами.

Hafthor
источник

Ответы:

32

Я согласен с ответом Мартина Смита, но проблема не просто в статистике. Статистика для столбца foreignId (при условии, что автоматическая статистика включена) точно показывает, что не существует строк для значения 3 (есть только одна, со значением 7):

DBCC SHOW_STATISTICS (tinytable, foreignId) WITH HISTOGRAM

вывод статистики

SQL Server знает , что все может измениться , так как статистические данные были взяты в плен, так что может быть строка для значения 3 , когда план выполняется . Кроме того, между компиляцией и выполнением плана может пройти любое время (в конце концов, планы кэшируются для повторного использования). Как говорит Мартин, SQL Server содержит логику, позволяющую обнаруживать, когда было внесено достаточно изменений, чтобы оправдать перекомпиляцию любого кэшированного плана по соображениям оптимальности.

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

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

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

По сути, у вас есть запрос, который не соответствует модели оптимизатора. Мы ничего не можем сделать, чтобы «улучшить» оценки с помощью нескольких столбцов или отфильтрованных индексов; здесь нет способа получить оценку ниже 1 строки. Реальная база данных может иметь внешние ключи, чтобы гарантировать, что эта ситуация не может возникнуть, но при условии, что это здесь не применимо, мы оставляем за собой использование подсказок для исправления условия вне модели. Любое количество различных подходов подсказок будет работать с этим запросом. OPTION (FORCE ORDER)это тот, который работает хорошо с запросом, как написано.

Пол Уайт восстановил Монику
источник
21

Основная проблема здесь одна из статистики.

Для обоих запросов предполагаемое количество строк показывает, что он полагает, что финал SELECTвернет 1 048 580 строк (то же самое число строк, в которых, как предполагается, существует bigtable), а не 0, который фактически следует.

Оба ваших JOINусловия совпадают и сохранят все строки. В итоге они исключаются, потому что одна строка tinytableне соответствует t.foreignId=3предикату.

Если вы бежите

SELECT * 
FROM tinytable t  
WHERE t.foreignId=3  AND id=1 

и посмотрите на предполагаемое количество строк, 1а не на 0это, и эта ошибка распространяется по всему плану. tinytableВ настоящее время содержит 1 строку. Статистика не будет перекомпилирована для этой таблицы до тех пор, пока не произойдет 500 модификаций строк , чтобы можно было добавить соответствующую строку, и она не вызовет перекомпиляцию.

Причина, по которой порядок соединения изменяется при добавлении ORDER BYпредложения и наличии varchar(max)столбца, smalltableзаключается в том, что, по оценкам, varchar(max)столбцы увеличивают размер строки в среднем на 4000 байтов. Умножьте это на 1048580 строк, и это означает, что для операции сортировки потребуется приблизительно 4 ГБ, поэтому она разумно решит выполнить эту SORTоперацию до JOIN.

Вы можете заставить ORDER BYзапрос принять ORDER BYстратегию без объединения, используя подсказки, как показано ниже.

SELECT *
FROM   tinytable t /* one narrow row */
       INNER MERGE JOIN smalltable s /* one narrow row */
                        INNER LOOP JOIN bigtable b
                          ON b.smallGuidId = s.GuidId /* a million narrow rows */
         ON t.id = s.tinyId
WHERE  t.foreignId = 3 /* doesn't match */
ORDER  BY b.CreatedUtc
OPTION (MAXDOP 1) 

В плане показан оператор сортировки с приблизительной стоимостью поддерева, состоящей из почти 12,000ошибочных расчетных чисел строк и предполагаемого размера данных.

Строить планы

Кстати, я не нашел замены UNIQUEIDENTIFIERстолбцов на целые, которые изменили вещи в моем тесте.

Мартин Смит
источник
2

Включите кнопку Показать план выполнения, и вы увидите, что происходит. Вот план для «медленного» запроса: введите описание изображения здесь

И вот «быстрый» запрос: введите описание изображения здесь

Посмотрите на это - запустите вместе, первый запрос примерно в 33 раза дороже (соотношение 97: 3). SQL оптимизирует первый запрос, чтобы упорядочить BigTable по дате и времени, затем запускает небольшой цикл «поиска» по SmallTable и TinyTable, выполняя их по 1 миллион раз каждый (вы можете навести курсор на значок «Поиск по кластерному индексу», чтобы получить больше статистики). Таким образом, сортировка (27%) и 2 x 1 миллион «поисков» в небольших таблицах (23% и 46%) составляют основную массу дорогостоящих запросов. Для сравнения, не- ORDER BYзапрос выполняет всего 3 сканирования.

По сути, вы нашли дыру в логике оптимизатора SQL для вашего конкретного сценария. Но, как заявляет TysHTTP, если вы добавите индекс (который немного замедляет вставку / обновление), ваше сканирование быстро сходит с ума.

jklemmack
источник
2

Происходит то, что SQL решает выполнить заказ до ограничения.

Попробуй это:

SELECT *
(
SELECT * 
FROM tinytable t
    INNER JOIN smalltable s on t.id=s.tinyId
    INNER JOIN bigtable b on b.smallGuidId=s.GuidId
WHERE t.foreignId=3
) X
ORDER BY b.CreatedUtc

Это дает вам улучшенную производительность (в этом случае, когда количество возвращаемых результатов очень мало), фактически без снижения производительности от добавления другого индекса. Хотя это странно, когда оптимизатор SQL решает выполнить порядок перед объединением, скорее всего, потому что, если у вас действительно есть возвращаемые данные, тогда сортировка после объединений займет больше времени, чем без сортировки.

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

EXEC [sp_MSforeachtable] @command1="RAISERROR('UPDATE STATISTICS(''?'') ...',10,1) WITH NOWAIT UPDATE STATISTICS ? "

EXEC [sp_MSforeachtable] @command1="RAISERROR('DBCC DBREINDEX(''?'') ...',10,1) WITH NOWAIT DBCC DBREINDEX('?')"

EXEC [sp_MSforeachtable] @command1="RAISERROR('UPDATE STATISTICS(''?'') ...',10,1) WITH NOWAIT UPDATE STATISTICS ? "
Seph
источник