Почему это быстрее и безопаснее в использовании? (ГДЕ первая буква в алфавите)

10

Короче говоря, мы обновляем маленькие таблицы людей со значениями из очень большой таблицы людей. В недавнем тесте это обновление заняло около 5 минут.

Мы наткнулись на то, что кажется самой глупой из возможных оптимизаций, которая, похоже, работает отлично! Тот же запрос теперь выполняется менее чем за 2 минуты и дает те же результаты, совершенно.

Вот запрос. Последняя строка добавляется как «оптимизация». Почему интенсивное сокращение времени запроса? Мы что-то упустили? Может ли это привести к проблемам в будущем?

UPDATE smallTbl
SET smallTbl.importantValue = largeTbl.importantValue
FROM smallTableOfPeople smallTbl
JOIN largeTableOfPeople largeTbl
    ON largeTbl.birth_date = smallTbl.birthDate
    AND DIFFERENCE(TRIM(smallTbl.last_name),TRIM(largeTbl.last_name)) = 4
    AND DIFFERENCE(TRIM(smallTbl.first_name),TRIM(largeTbl.first_name)) = 4
WHERE smallTbl.importantValue IS NULL
-- The following line is "the optimization"
AND LEFT(TRIM(largeTbl.last_name), 1) IN ('a','à','á','b','c','d','e','è','é','f','g','h','i','j','k','l','m','n','o','ô','ö','p','q','r','s','t','u','ü','v','w','x','y','z','æ','ä','ø','å')

Технические примечания. Нам известно, что для проверки списка букв может потребоваться еще несколько букв. Мы также знаем об очевидном поле для ошибки при использовании «РАЗНИЦА».

План запроса (обычный): https://www.brentozar.com/pastetheplan/?id=rypV84y7V
План запроса (с «оптимизацией»): https://www.brentozar.com/pastetheplan/?id=r1aC2my7E

JohnF
источник
4
Небольшой ответ на вашу техническую заметку: AND LEFT(TRIM(largeTbl.last_name), 1) BETWEEN 'a' AND 'z' COLLATE LATIN1_GENERAL_CI_AIделать то, что вы хотите, не требуя перечисления всех символов и иметь код, который трудно прочитать
Эрик
У вас есть строки, где последнее условие в WHEREложно? В частности, обратите внимание, что сравнение может быть чувствительным к регистру.
jpmc26
@ErikvonAsmuth делает отличное замечание. Но только небольшое техническое замечание: для SQL Server 2008 и 2008 R2 лучше использовать параметры сортировки «100» (если они доступны для используемой культуры / локали). Так бы и было Latin1_General_100_CI_AI. А для SQL Server 2012 и новее (по крайней мере, через SQL Server 2019) лучше использовать сопоставления с поддержкой дополнительных символов в самой высокой версии для используемой локали. Так было бы Latin1_General_100_CI_AI_SCв этом случае. Версии> 100 (пока только японские) не имеют (или не нужны) _SC(например Japanese_XJIS_140_CI_AI).
Соломон Руцкий

Ответы:

9

Это зависит от данных в ваших таблицах, ваших индексов ... Трудно сказать, не имея возможности сравнить планы выполнения / статистику времени io +.

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

План выполнения с «оптимизацией» введите описание изображения здесь

План выполнения

Вы четко видите, что происходит операция фильтрации, в моих тестовых данных нет записей, где отфильтровываются, и, как следствие, нет улучшений, где сделаны

План выполнения, без «оптимизации» введите описание изображения здесь

План выполнения

Фильтр пропал, а это значит, что нам придется полагаться на объединение, чтобы отфильтровать ненужные записи.

Другая (-ые) причина (-ы) Другая причина / следствие изменения запроса может заключаться в том, что при изменении запроса был создан новый план выполнения, который оказывается быстрее. Примером этого является движок, выбирающий другой оператор Join, но на данный момент это только предположение.

РЕДАКТИРОВАТЬ:

Уточнение после получения двух планов запроса:

Запрос читает 550M строк из большой таблицы и отфильтровывает их. введите описание изображения здесь

Это означает, что именно предикат выполняет большую часть фильтрации, а не предикат поиска. В результате данные читаются, но возвращаются меньше.

Заставить SQL Server использовать другой индекс (план запроса) / добавление индекса может решить эту проблему.

Так почему же запрос на оптимизацию не имеет такой же проблемы?

Потому что используется другой план запроса, с поиском вместо поиска.

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

Без каких-либо поисков, но только возвращая 4M строк для работы.

Следующая разница

Не обращая внимания на разницу в обновлении (в оптимизированном запросе ничего не обновляется), в оптимизированном запросе используется совпадение хеша:

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

Вместо неоптимизированного соединения с вложенным циклом:

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

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

обзор

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

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

введите описание изображения здесь Неоптимизированный запрос План неоптимизированного запроса не имеет параллелизма, использует соединение с вложенным циклом и должен выполнять остаточную фильтрацию ввода-вывода для записей 550M. (Также происходит обновление)

Что вы можете сделать, чтобы улучшить неоптимизированный запрос?

  • Изменение индекса, чтобы в списке ключевых столбцов были указаны имя и фамилия:

    CREATE INDEX IX_largeTableOfPeople_birth_date_first_name_last_name для dbo.largeTableOfPeople (дата рождения, имя_первого, фамилия) включают (id)

Но из-за использования функций и большой таблицы это может быть не оптимальным решением.

  • Обновление статистики с использованием перекомпиляции, чтобы попытаться получить лучший план.
  • Добавление OPTION (HASH JOIN, MERGE JOIN)к запросу
  • ...

Тестовые данные + использованные запросы

CREATE TABLE #smallTableOfPeople(importantValue int, birthDate datetime2, first_name varchar(50),last_name varchar(50));
CREATE TABLE #largeTableOfPeople(importantValue int, birth_date datetime2, first_name varchar(50),last_name varchar(50));


set nocount on;
DECLARE @i int = 1
WHILE @i <= 1000
BEGIN
insert into #smallTableOfPeople (importantValue,birthDate,first_name,last_name)
VALUES(NULL, dateadd(mi,@i,'2018-01-18 11:05:29.067'),'Frodo','Baggins');

set @i += 1;
END


set nocount on;
DECLARE @j int = 1
WHILE @j <= 20000
BEGIN
insert into #largeTableOfPeople (importantValue,birth_Date,first_name,last_name)
VALUES(@j, dateadd(mi,@j,'2018-01-18 11:05:29.067'),'Frodo','Baggins');

set @j += 1;
END


SET STATISTICS IO, TIME ON;

SELECT  smallTbl.importantValue , largeTbl.importantValue
FROM #smallTableOfPeople smallTbl
JOIN #largeTableOfPeople largeTbl
    ON largeTbl.birth_date = smallTbl.birthDate
    AND DIFFERENCE(RTRIM(LTRIM(smallTbl.last_name)),RTRIM(LTRIM(largeTbl.last_name))) = 4
    AND DIFFERENCE(RTRIM(LTRIM(smallTbl.first_name)),RTRIM(LTRIM(largeTbl.first_name))) = 4
WHERE smallTbl.importantValue IS NULL
-- The following line is "the optimization"
AND LEFT(RTRIM(LTRIM(largeTbl.last_name)), 1) IN ('a','à','á','b','c','d','e','è','é','f','g','h','i','j','k','l','m','n','o','ô','ö','p','q','r','s','t','u','ü','v','w','x','y','z','æ','ä','ø','å');

SELECT  smallTbl.importantValue , largeTbl.importantValue
FROM #smallTableOfPeople smallTbl
JOIN #largeTableOfPeople largeTbl
    ON largeTbl.birth_date = smallTbl.birthDate
    AND DIFFERENCE(RTRIM(LTRIM(smallTbl.last_name)),RTRIM(LTRIM(largeTbl.last_name))) = 4
    AND DIFFERENCE(RTRIM(LTRIM(smallTbl.first_name)),RTRIM(LTRIM(largeTbl.first_name))) = 4
WHERE smallTbl.importantValue IS NULL
-- The following line is "the optimization"
--AND LEFT(RTRIM(LTRIM(largeTbl.last_name)), 1) IN ('a','à','á','b','c','d','e','è','é','f','g','h','i','j','k','l','m','n','o','ô','ö','p','q','r','s','t','u','ü','v','w','x','y','z','æ','ä','ø','å')




drop table #largeTableOfPeople;
drop table #smallTableOfPeople;
Рэнди Вертонген
источник
8

Не ясно, что второй запрос на самом деле является улучшением.

Планы выполнения содержат QueryTimeStats, которые показывают гораздо меньшую разницу, чем указано в вопросе.

У медленного плана прошло время 257,556 ms(4 минуты 17 секунд). У быстрого плана прошло время 190,992 ms(3 минуты 11 секунд), несмотря на то, что он выполнялся со степенью параллелизма 3.

Более того, второй план выполнялся в базе данных, где не было никакой работы после объединения.

Первый План

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

Второй План

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

Так что дополнительное время вполне может быть объяснено работой, необходимой для обновления 3,5 миллионов строк (работа, требуемая в операторе обновления, чтобы найти эти строки, заблокировать страницу, записать обновление на страницу и журнал транзакций, не является незначительной)

Если это на самом деле воспроизводимо при сравнении как с подобным, то объяснение состоит в том, что вам просто повезло в этом случае.

Фильтр с 37 INусловиями исключил только 51 строку из 4008,334 в таблице, но оптимизатор посчитал, что это устранит гораздо больше

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

   LEFT(TRIM(largeTbl.last_name), 1) IN ( 'a', 'à', 'á', 'b',
                                          'c', 'd', 'e', 'è',
                                          'é', 'f', 'g', 'h',
                                          'i', 'j', 'k', 'l',
                                          'm', 'n', 'o', 'ô',
                                          'ö', 'p', 'q', 'r',
                                          's', 't', 'u', 'ü',
                                          'v', 'w', 'x', 'y',
                                          'z', 'æ', 'ä', 'ø', 'å' ) 

Такие неправильные оценки мощности обычно плохие вещи. В этом случае он создал план (и параллельный) другой формы, который, по-видимому, (?) Работал лучше для вас, несмотря на разлив хеша, вызванный массивной недооценкой.

Без TRIMSQL Server можно преобразовать это в интервал диапазона в гистограмме базового столбца и дать гораздо более точные оценки, но при этом TRIMон просто прибегает к догадкам.

Природа предположения может варьироваться, но оценка для одного предиката LEFT(TRIM(largeTbl.last_name), 1)в некоторых случаях * просто оценивается как table_cardinality/estimated_number_of_distinct_column_values.

Я не уверен точно, какие обстоятельства - размер данных, кажется, играет роль. Я смог воспроизвести это с широкими типами данных фиксированной длины, как здесь, но получил другое, более высокое, предположение с varchar(которое только использовало 10% -ое предположение и приблизительно 100 000 строк). @Solomon Rutzky указывает на то, что, если они дополнены концевымиvarchar(100) пробелами, как это происходит для charнижней оценки, используется

INСписок расширен, чтобы ORи SQL Server использует экспоненциальную отсрочку максимум 4 предикатов рассмотренных. Таким образом, 219.707оценка получается следующим образом.

DECLARE @TableCardinality FLOAT = 4008334, 
        @DistinctColumnValueEstimate FLOAT = 34207

DECLARE @NotSelectivity float = 1 - (1/@DistinctColumnValueEstimate)

SELECT @TableCardinality * ( 1 - (
@NotSelectivity * 
SQRT(@NotSelectivity) * 
SQRT(SQRT(@NotSelectivity)) * 
SQRT(SQRT(SQRT(@NotSelectivity)))
))
Мартин Смит
источник