Как я могу получить итоги последних строк быстрее?

8

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

CREATE TABLE [dbo].[Table_1](
    [seq] [int] IDENTITY(1,1) NOT NULL,
    [value] [bigint] NOT NULL,
 CONSTRAINT [PK_Table_1] PRIMARY KEY CLUSTERED 
(
    [seq] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO

И я попытался получить 10 последних строк и их промежуточные итоги, но это заняло около 10 секунд.

--1st attempt
SELECT TOP 10 seq
    ,value
    ,sum(value) OVER (ORDER BY seq) total
FROM Table_1
ORDER BY seq DESC

--(10 rows affected)
--Table 'Worktable'. Scan count 1000001, logical reads 8461526, physical reads 2, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
--Table 'Table_1'. Scan count 1, logical reads 2608, physical reads 516, read-ahead reads 2617, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
--Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
--
--(1 row affected)
--
-- SQL Server Execution Times:
--   CPU time = 8483 ms,  elapsed time = 9786 ms.

План выполнения 1-й попытки

Я подозревал, TOPчто это связано с низкой производительностью плана, поэтому я изменил запрос следующим образом, и это заняло около 1-2 секунд. Но я думаю, что это все еще медленно для производства и интересно, можно ли это еще улучшить.

--2nd attempt
SELECT *
    ,(
        SELECT SUM(value)
        FROM Table_1
        WHERE seq <= t.seq
        ) total
FROM (
    SELECT TOP 10 seq
        ,value
    FROM Table_1
    ORDER BY seq DESC
    ) t
ORDER BY seq DESC

--(10 rows affected)
--Table 'Table_1'. Scan count 11, logical reads 26083, physical reads 1, read-ahead reads 443, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
--
--(1 row affected)
--
-- SQL Server Execution Times:
--   CPU time = 1422 ms,  elapsed time = 1621 ms.

План выполнения 2-й попытки

Мои вопросы:

  • Почему запрос с 1-й попытки медленнее, чем 2-й?
  • Как я могу улучшить производительность дальше? Я также могу изменить схемы.

Просто чтобы быть понятным, оба запроса возвращают тот же результат, что и ниже.

Результаты

user2652379
источник
1
Я обычно не использую оконные функции, но я помню, что прочитал некоторые полезные статьи о них. Взгляните на одно Введение в оконные функции T-SQL , особенно в части « Совокупные улучшения окна» в 2012 году . Возможно, это даст вам несколько ответов. ... и еще одна статья того же превосходного автора Функции и производительность окна T-SQL
Денис Рубашкин
Вы пытались поставить индекс value?
Джейкоб Х,

Ответы:

5

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

Следующий подход занимает 19 секунд на моей машине:

SELECT TOP (10) seq
    ,value
    ,sum(value) OVER (ORDER BY seq ROWS UNBOUNDED PRECEDING) total
FROM dbo.[Table_1_BIG]
ORDER BY seq DESC;

Актуальный план здесь . Большую часть времени тратят на подсчет суммы и выполнение сортировки. Тревожно, план запроса выполняет почти всю работу для всего набора результатов и фильтрует до 10 строк, которые вы запросили в самом конце. Время выполнения этого запроса масштабируется с размером таблицы, а не с размером набора результатов.

Эта опция занимает 23 секунды на моей машине:

SELECT *
    ,(
        SELECT SUM(value)
        FROM dbo.[Table_1_BIG]
        WHERE seq <= t.seq
        ) total
FROM (
    SELECT TOP (10) seq
        ,value
    FROM dbo.[Table_1_BIG]
    ORDER BY seq DESC
    ) t
ORDER BY seq DESC;

Актуальный план здесь . Этот подход масштабируется как с количеством запрошенных строк, так и с размером таблицы. Из таблицы прочитано почти 160 миллионов строк:

Привет

Чтобы получить правильные результаты, вы должны суммировать строки для всей таблицы. В идеале вы должны выполнить это суммирование только один раз. Это можно сделать, если вы измените подход к проблеме. Вы можете вычислить сумму для всей таблицы, а затем вычесть промежуточную сумму из строк в наборе результатов. Это позволяет найти сумму для N-го ряда. Один из способов сделать это:

SELECT TOP (10) seq
,value
, [value]
    - SUM([value]) OVER (ORDER BY seq DESC ROWS UNBOUNDED PRECEDING)
    + (SELECT SUM([value]) FROM dbo.[Table_1_BIG]) AS total
FROM dbo.[Table_1_BIG]
ORDER BY seq DESC;

Актуальный план здесь . Новый запрос выполняется на моей машине за 644 мс. Таблица сканируется один раз для получения полного итога, затем для каждой строки в наборе результатов читается дополнительная строка. Там нет сортировки и почти все время тратится на подсчет суммы в параллельной части плана:

довольно хорошо

Если вы хотите, чтобы этот запрос выполнялся еще быстрее, вам просто нужно оптимизировать часть, которая рассчитывает полную сумму. Приведенный выше запрос выполняет сканирование кластерного индекса. Кластерный индекс включает в себя все столбцы, но вам нужен только [value]столбец. Один из вариантов - создать некластеризованный индекс для этого столбца. Другим вариантом является создание некластеризованного индекса columnstore для этого столбца. Оба улучшат производительность. Если вы находитесь на предприятии, отличный вариант - создать индексированное представление, как показано ниже:

CREATE OR ALTER VIEW dbo.Table_1_BIG__SUM
WITH SCHEMABINDING
AS
SELECT SUM([value]) SUM_VALUE
, COUNT_BIG(*) FOR_U
FROM dbo.[Table_1_BIG];

GO

CREATE UNIQUE CLUSTERED INDEX CI ON dbo.Table_1_BIG__SUM (SUM_VALUE);

Это представление возвращает одну строку, поэтому оно почти не занимает места. Будет штраф за выполнение DML, но он не должен сильно отличаться от обслуживания индекса. При включенном индексированном представлении запрос теперь занимает 0 мс:

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

Актуальный план здесь . Лучшая часть этого подхода заключается в том, что время выполнения не изменяется в зависимости от размера таблицы. Единственное, что имеет значение, это то, сколько строк возвращено. Например, если вы получили первые 10000 строк, выполнение запроса теперь занимает 18 мс.

Код для заполнения таблицы:

DROP TABLE IF EXISTS dbo.[Table_1_BIG];

CREATE TABLE dbo.[Table_1_BIG] (
    [seq] [int] NOT NULL,
    [value] [bigint] NOT NULL
);

DROP TABLE IF EXISTS #t;
CREATE TABLE #t (ID BIGINT);

INSERT INTO #t WITH (TABLOCK)
SELECT TOP (4000) -1 + ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
OPTION (MAXDOP 1);

INSERT INTO dbo.[Table_1_BIG] WITH (TABLOCK)
SELECT t1.ID * 4000 + t2.ID, 8 * t2.ID + t1.ID
FROM (SELECT TOP (4000) ID FROM #t) t1
CROSS JOIN #t t2;

ALTER TABLE dbo.[Table_1_BIG]
ADD CONSTRAINT [PK_Table_1] PRIMARY KEY ([seq]);
Джо Оббиш
источник
4

Разница в первых двух подходах

Первый план тратит около 7 из 10 секунд в операторе окна золотника, так что это главная причина , это очень медленно. Он выполняет много операций ввода-вывода в базе данных tempdb. Моя статистика ввода / вывода и время выглядят так:

Table 'Worktable'. Scan count 1000001, logical reads 8461526
Table 'Table_1'. Scan count 1, logical reads 2609
Table 'Worktable'. Scan count 0, logical reads 0

 SQL Server Execution Times:
   CPU time = 8641 ms,  elapsed time = 8537 ms.

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

Table 'Table_1'. Scan count 11, logical reads 26093
 SQL Server Execution Times:
   CPU time = 1563 ms,  elapsed time = 1671 ms.

Улучшение производительности

Columnstore

Если вам действительно нужен подход «онлайн-отчетности», вероятно, вам лучше подходит columnstore.

ALTER TABLE [dbo].[Table_1] DROP CONSTRAINT [PK_Table_1];

CREATE CLUSTERED COLUMNSTORE INDEX [PK_Table_1] ON dbo.Table_1;

Тогда этот запрос смехотворно быстр:

SELECT TOP 10
    seq, 
    value, 
    SUM(value) OVER (ORDER BY seq ROWS UNBOUNDED PRECEDING)
FROM dbo.Table_1
ORDER BY seq DESC;

Вот статистика с моей машины:

Table 'Table_1'. Scan count 4, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 3319
Table 'Table_1'. Segment reads 1, segment skipped 0.
Table 'Worktable'. Scan count 0, logical reads 0

 SQL Server Execution Times:
   CPU time = 375 ms,  elapsed time = 205 ms.

Вы, вероятно, не собираетесь победить это (если вы не очень умный - хороший, Джо). Columnstore отлично умеет сканировать и собирать большие объемы данных.

Использование ROWвместо RANGEоконной функции

Вы можете получить очень похожую производительность со вторым запросом с помощью этого подхода, который был упомянут в другом ответе и который я использовал в приведенном выше примере columnstore ( план выполнения ):

SELECT TOP 10
    seq, 
    value, 
    SUM(value) OVER (ORDER BY seq ROWS UNBOUNDED PRECEDING)
FROM dbo.Table_1
ORDER BY seq DESC;

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

... RANGE использует катушку на диске, в то время как ROWS использует катушку в памяти

К сожалению, время выполнения примерно такое же, как ваш второй подход.

Table 'Worktable'. Scan count 0, logical reads 0
Table 'Table_1'. Scan count 1, logical reads 2609
Table 'Worktable'. Scan count 0, logical reads 0

 SQL Server Execution Times:
   CPU time = 1984 ms,  elapsed time = 1474 ms.

Основанное на схеме решение: асинхронные промежуточные итоги

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

CREATE TABLE [dbo].[Table_1_Totals]
(
    [seq] [int] NOT NULL,
    [running_total] [bigint] NOT NULL,
    CONSTRAINT [PK_Table_1_Totals] PRIMARY KEY CLUSTERED ([seq])
);

Загружайте его каждый день / час / что угодно (это заняло около 2 секунд на моей машине с рядами 1 мм и могло быть оптимизировано):

INSERT INTO dbo.Table_1_Totals
SELECT
    seq, 
    SUM(value) OVER (ORDER BY seq ROWS UNBOUNDED PRECEDING) as total
FROM dbo.Table_1 t
WHERE NOT EXISTS (
            SELECT NULL 
            FROM dbo.Table_1_Totals t2
            WHERE t.seq = t2.seq)
ORDER BY seq DESC;

Тогда ваш отчетный запрос очень эффективен:

SELECT TOP 10
    t.seq, 
    t.value, 
    t2.running_total
FROM dbo.Table_1 t
    INNER JOIN dbo.Table_1_Totals t2
        ON t.seq = t2.seq
ORDER BY seq DESC;

Вот прочитанная статистика:

Table 'Table_1'. Scan count 0, logical reads 35
Table 'Table_1_Totals'. Scan count 1, logical reads 3

Основанное на схеме решение: итоговые суммы с ограничениями

Действительно интересное решение этой проблемы подробно рассмотрено в этом ответе на вопрос: написание простой банковской схемы: как я должен синхронизировать свои балансы с их историей транзакций?

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

Благодарим Пола Уайта за предоставление примера реализации схемы в этом разделе вопросов и ответов:

CREATE TABLE dbo.Table_1
(
    seq integer IDENTITY(1,1) NOT NULL,
    val bigint NOT NULL,
    total bigint NOT NULL,

    prev_seq integer NULL,
    prev_total bigint NULL,

    CONSTRAINT [PK_Table_1] 
        PRIMARY KEY CLUSTERED (seq ASC),

    CONSTRAINT [UQ dbo.Table_1 seq, total]
        UNIQUE (seq, total),

    CONSTRAINT [UQ dbo.Table_1 prev_seq]
        UNIQUE (prev_seq),

    CONSTRAINT [FK dbo.Table_1 previous seq and total]
        FOREIGN KEY (prev_seq, prev_total) 
        REFERENCES dbo.Table_1 (seq, total),

    CONSTRAINT [CK dbo.Table_1 total = prev_total + val]
        CHECK (total = ISNULL(prev_total, 0) + val),

    CONSTRAINT [CK dbo.Table_1 denormalized columns all null or all not null]
        CHECK 
        (
            (prev_seq IS NOT NULL AND prev_total IS NOT NULL)
            OR
            (prev_seq IS NULL AND prev_total IS NULL)
        )
);
Джош Дарнелл
источник
2

При работе с таким небольшим подмножеством возвращаемых строк хорошим вариантом является треугольное соединение. Однако при использовании оконных функций у вас есть больше опций, которые могут повысить их производительность. Опция по умолчанию для опции окна - ДИАПАЗОН, но оптимальная опция - СТРОКИ. Имейте в виду, что разница заключается не только в производительности, но и в результатах, когда участвуют связи.

Следующий код немного быстрее, чем те, которые вы представили.

SELECT TOP 10 seq
    ,value
    ,sum(value) OVER (ORDER BY seq ROWS UNBOUNDED PRECEDING) total
FROM Table_1
ORDER BY seq DESC
Луис Казарес
источник
Спасибо, что рассказали ROWS. Я попробовал, но не могу сказать, что это быстрее, чем мой второй запрос. Результат былCPU time = 1438 ms, elapsed time = 1537 ms.
user2652379
Но это только по этому варианту. Ваш второй запрос плохо масштабируется. Попробуйте вернуть больше строк, и разница станет довольно очевидной.
Луис Казарес
Может быть, за пределами т-sql? Я могу изменить схему.
user2652379