Чому це швидше і чи безпечно його використовувати? (ДЕ перша буква в алфавіті)


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


4
Невелика відповідь на вашу технічну записку: AND LEFT(TRIM(largeTbl.last_name), 1) BETWEEN 'a' AND 'z' COLLATE LATIN1_GENERAL_CI_AIслід робити те, що ви хочете там, не вимагаючи, щоб ви перераховували всі символи та не мали коду, який важко читати
Ерік A

Чи є у вас рядки, де кінцева умова у 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, але це лише здогадки.

Редагувати:

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

Запит читає 550М рядків з великої таблиці та фільтрує їх. введіть тут опис зображення

Це означає, що предикат - це той, хто робить більшу частину фільтрації, а не предикат. У результаті отримуються дані, що читаються, але набагато менше повертаються.

Змушення сервера sql використовувати інший індекс (план запитів) / додавання індексу може вирішити це.

Так чому запит на оптимізацію не має цього самого питання?

Оскільки використовується інший план запитів із скануванням замість пошуку.

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

Не виконуючи жодних пошуків, а лише повертаючи 4M рядки для роботи.

Наступна різниця

Не зважаючи на різницю оновлення (нічого не оновлюється в оптимізованому запиті) для оптимізованого запиту використовується хеш-відповідність:

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

Замість вкладеного циклу приєднуйтесь до неоптимізованого:

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

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

Огляд

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

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

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

Що ви могли б зробити для покращення неоптимізованого запиту?

  • Зміна індексу на прізвище ім’я та прізвище у списку ключових стовпців:

    СТВОРИТИ ІНДЕКС 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 рядок із 4,008,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 рядків). @ Соломон Руцький зазначає, що якщо 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)))
))
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.