Вимушення розрізнення потоку


19

У мене така таблиця:

CREATE TABLE Updates
(
    UpdateId INT NOT NULL IDENTITY(1,1) PRIMARY KEY,
    ObjectId INT NOT NULL
)

По суті, відстеження оновлень для об’єктів зі збільшенням ідентифікатора.

Споживач цієї таблиці вибере фрагмент із 100 різних ідентифікаторів об'єкта, упорядкований UpdateIdта починаючи з конкретного UpdateId. По суті, слідкуйте за тим, де він зупинився, а потім запитуйте про оновлення.

Я вважав це цікавою проблемою оптимізації, тому що мені вдалося створити максимально оптимальний план запитів, записуючи запити, які трапляються робити те, що я хочу завдяки індексам, але не гарантую, що я хочу:

SELECT DISTINCT TOP 100 ObjectId
FROM Updates
WHERE UpdateId > @fromUpdateId

Де @fromUpdateIdпараметр збереженої процедури.

З планом:

SELECT <- TOP <- Hash match (flow distinct, 100 rows touched) <- Index seek

Завдяки пошуку UpdateIdіндексу, який використовується, результати вже приємні та упорядковані від найнижчого до найвищого ідентифікатора оновлення, як я хочу. І це генерує чіткий план потоку , якого я хочу. Але впорядкування очевидно не є гарантованим поведінкою, тому я не хочу його використовувати.

Цей трюк також призводить до того ж плану запитів (правда, із надлишковим TOP):

WITH ids AS
(
    SELECT ObjectId
    FROM Updates
    WHERE UpdateId > @fromUpdateId
    ORDER BY UpdateId OFFSET 0 ROWS
)
SELECT DISTINCT TOP 100 ObjectId FROM ids

Хоча я не впевнений (і не підозрюю), чи справді це гарантує замовлення.

Один запит, на який я сподівався, що SQL Server буде досить розумним для спрощення, це такий, але в результаті він створює дуже поганий план запитів:

SELECT TOP 100 ObjectId
FROM Updates
WHERE UpdateId > @fromUpdateId
GROUP BY ObjectId
ORDER BY MIN(UpdateId)

З планом:

SELECT <- Top N Sort <- Hash Match aggregate (50,000+ rows touched) <- Index Seek

Я намагаюсь знайти спосіб створення оптимального плану з індексом пошуку UpdateIdта потоком, відмінним для видалення дублікатів ObjectIds. Будь-які ідеї?

Зразок даних, якщо ви хочете. Об'єкти рідко матимуть більше одного оновлення, і майже ніколи не повинні мати більше одного в наборі з 100 рядків, саме тому я після потоку виразний , якщо немає чогось кращого, про що я не знаю? Однак немає гарантії, що в одного ObjectIdне буде більше 100 рядків у таблиці. Таблиця налічує понад 1 000 000 рядків і очікується швидке зростання.

Припустимо, що користувач цього має інший спосіб знайти відповідний наступний @fromUpdateId. Не потрібно повертати його в цьому запиті.

Відповіді:


15

Оптимізатор SQL Server не може скласти план виконання, який ви виконуєте, з необхідною гарантією, оскільки оператор Hash Match Flow Distinct не зберігає замовлення.

Хоча я не впевнений (і не підозрюю), чи справді це гарантує замовлення.

Ви можете спостерігати за збереженням порядку в багатьох випадках, але це детальна інформація про виконання; гарантії немає, тому ви не можете покластися на неї. Як завжди, порядок презентації може бути гарантований лише ORDER BYпунктом верхнього рівня .

Приклад

Сценарій нижче показує, що Hash Match Flow Distinct не зберігає порядок. Він встановлює відповідну таблицю з відповідними числами 1-50 000 в обох стовпцях:

IF OBJECT_ID(N'dbo.Updates', N'U') IS NOT NULL
    DROP TABLE dbo.Updates;
GO
CREATE TABLE Updates
(
    UpdateId INT NOT NULL IDENTITY(1,1),
    ObjectId INT NOT NULL,

    CONSTRAINT PK_Updates_UpdateId PRIMARY KEY (UpdateId)
);
GO
INSERT dbo.Updates (ObjectId)
SELECT TOP (50000)
    ObjectId =
        ROW_NUMBER() OVER (
            ORDER BY C1.[object_id]) 
FROM sys.columns AS C1
CROSS JOIN sys.columns AS C2
ORDER BY
    ObjectId;

Тестовий запит:

DECLARE @Rows bigint = 50000;

-- Optimized for 1 row, but will be 50,000 when executed
SELECT DISTINCT TOP (@Rows)
    U.ObjectId 
FROM dbo.Updates AS U
WHERE 
    U.UpdateId > 0
OPTION (OPTIMIZE FOR (@Rows = 1));

Орієнтовний план показує різницю пошуку та потоку індексу:

Розрахунковий план

Вихід, безумовно, здається впорядкованим починати з:

Початок результатів

... але подальші нижчі значення починають «пропадати»:

Розбивка візерунка

... і врешті:

Вибухнув хаос

Пояснення в цьому конкретному випадку полягає в тому, що хеш-оператор розливається:

План після виконання

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


Існує багато способів написати ефективний запит для отримання потрібного впорядкованого результату, наприклад, рекурсія або використання курсору. Однак це неможливо зробити за допомогою Hash Match Flow Distinct .


11

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

Я підійшов до цієї проблеми, намагаючись придумати комбінацію стовпців, яку я міг би, ORDER BYі отримати правильні результати після застосування DISTINCTдо них. Мінімальне значення UpdateIdper ObjectIdразом із ObjectId- це одна така комбінація. Однак, безпосередньо запит на мінімум, як UpdateIdвидається, призводить до читання всіх рядків із таблиці. Натомість ми можемо опосередковано просити мінімальне значення UpdateIdз іншим приєднанням до таблиці. Ідея полягає в тому, щоб сканувати Updatesтаблицю для того, щоб викинути будь-які рядки, для яких UpdateIdце не мінімальне значення ObjectId, і зберегти перші 100 рядків. Виходячи з вашого опису розподілу даних, нам не потрібно викидати дуже багато рядків.

Для підготовки даних я помістив 1 мільйон рядків у таблицю з 2 рядками для кожного окремого ObjectId:

INSERT INTO Updates WITH (TABLOCK)
SELECT t.RN / 2
FROM 
(
    SELECT TOP 1000000 -1 + ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) RN
    FROM master..spt_values t1
    CROSS JOIN master..spt_values t2
) t;

CREATE INDEX IX On Updates (Objectid, UpdateId);

Некластеризований індекс на Objectidі UpdateIdє важливим. Це дозволяє нам ефективно викинути ті рядки , які не мають мінімум UpdateIdза Objectid. Існує багато способів написати запит, який відповідає описаному вище. Ось один із таких способів використання NOT EXISTS:

DECLARE @fromUpdateId INT = 9999;
SELECT ObjectId
FROM (
    SELECT DISTINCT TOP 100 u1.UpdateId, u1.ObjectId
    FROM Updates u1
    WHERE UpdateId > @fromUpdateId
    AND NOT EXISTS (
        SELECT 1
        FROM Updates u2
        WHERE u2.UpdateId > @fromUpdateId
        AND u1.ObjectId = u2.ObjectId
        AND u2.UpdateId < u1.UpdateId
    )
    ORDER BY u1.UpdateId, u1.ObjectId
) t;

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

план запитів

У кращому випадку SQL Server зробить лише 100 запитів на індекс проти некластеризованого індексу. Щоб імітувати дуже невдало, я змінив запит, щоб повернути клієнту перші 5000 рядків. Це призвело до прагнення до індексу 9999, тож це як отримання в середньому 100 рядків за окремі ObjectId. Ось вихід із SET STATISTICS IO, TIME ON:

Таблиця "Оновлення". Кількість сканів 10000, логічне зчитування 31900, фізичне зчитування 0

Часи виконання SQL Server: час процесора = 31 мс, минулий час = 42 мс.


9

Мені подобається питання - Flow Distinct - один з моїх улюблених операторів.

Тепер гарантія - проблема. Якщо ви думаєте про оператора FD, який впорядковує витягування рядків з оператора Seek, створюючи кожен рядок, оскільки він визначає його унікальність, це дасть вам рядки в правильному порядку. Але важко знати, чи можуть бути якісь сценарії, коли FD не обробляє один рядок одночасно.

Теоретично FD може вимагати 100 рядків від Seek і виробляти їх у будь-якому порядку, в якому вони потребують.

Підказки запитів OPTION (FAST 1, MAXDOP 1)можуть допомогти, оскільки це дозволить уникнути отримання більше рядків, ніж потрібно оператору Seek. Хоча це гарантія ? Не зовсім. Він все ще може вирішити за один раз тягнути сторінку рядків, або щось подібне.

Думаю OPTION (FAST 1, MAXDOP 1), ваша OFFSETверсія надала б вам велику впевненість у виконанні замовлення, але це не гарантія.


Як я зрозумів, проблема полягає в тому, що оператор Flow Distinct використовує хеш-таблицю, яка може розпливатися на диск. Коли відбувається розлив, рядки, які можна обробити за допомогою частини, що все ще знаходиться в оперативній пам'яті, обробляються негайно, але інші рядки не обробляються, поки дані не будуть прочитані з диска. Як я можу сказати, будь-який оператор, що використовує хеш-таблицю (наприклад, Hash Join), не гарантує збереження порядку через свою розлиту поведінку.
sam.bishop

Правильно. Дивіться відповідь Пола Вайт.
Роб Фарлі
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.