Принудительный поток


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 .

пример

Приведенный ниже сценарий показывает, что Flow Hash Match 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));

Расчетный план показывает индекс поиска и потока различны:

Ориентировочный план

Вывод, конечно, выглядит упорядоченным для начала:

Начало результатов

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

Разбивка паттернов

... и в конце концов:

Хаос вспыхивает

Объяснение в данном конкретном случае заключается в том, что оператор хеширования разливает:

План после исполнения

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


Есть много способов написать эффективный запрос для получения желаемого упорядоченного результата, например, рекурсии или использования курсора. Однако это невозможно сделать с помощью функции Flow Hash Match 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важен. Это позволяет нам эффективно выбрасывать строки, у которых нет минимального значения UpdateIdper 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.