В настоящее время я разрабатываю таблицу транзакций. Я понял, что потребуется подсчет промежуточных итогов для каждой строки, и это может привести к снижению производительности. Поэтому я создал таблицу с 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.
Я подозревал, 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.
Мои вопросы:
- Почему запрос с 1-й попытки медленнее, чем 2-й?
- Как я могу улучшить производительность дальше? Я также могу изменить схемы.
Просто чтобы быть понятным, оба запроса возвращают тот же результат, что и ниже.
value
?Ответы:
Я рекомендую тестировать немного больше данных, чтобы лучше понять, что происходит, и посмотреть, как работают разные подходы. Я загрузил 16 миллионов строк в таблицу с такой же структурой. Вы можете найти код для заполнения таблицы в нижней части этого ответа.
Следующий подход занимает 19 секунд на моей машине:
Актуальный план здесь . Большую часть времени тратят на подсчет суммы и выполнение сортировки. Тревожно, план запроса выполняет почти всю работу для всего набора результатов и фильтрует до 10 строк, которые вы запросили в самом конце. Время выполнения этого запроса масштабируется с размером таблицы, а не с размером набора результатов.
Эта опция занимает 23 секунды на моей машине:
Актуальный план здесь . Этот подход масштабируется как с количеством запрошенных строк, так и с размером таблицы. Из таблицы прочитано почти 160 миллионов строк:
Чтобы получить правильные результаты, вы должны суммировать строки для всей таблицы. В идеале вы должны выполнить это суммирование только один раз. Это можно сделать, если вы измените подход к проблеме. Вы можете вычислить сумму для всей таблицы, а затем вычесть промежуточную сумму из строк в наборе результатов. Это позволяет найти сумму для N-го ряда. Один из способов сделать это:
Актуальный план здесь . Новый запрос выполняется на моей машине за 644 мс. Таблица сканируется один раз для получения полного итога, затем для каждой строки в наборе результатов читается дополнительная строка. Там нет сортировки и почти все время тратится на подсчет суммы в параллельной части плана:
Если вы хотите, чтобы этот запрос выполнялся еще быстрее, вам просто нужно оптимизировать часть, которая рассчитывает полную сумму. Приведенный выше запрос выполняет сканирование кластерного индекса. Кластерный индекс включает в себя все столбцы, но вам нужен только
[value]
столбец. Один из вариантов - создать некластеризованный индекс для этого столбца. Другим вариантом является создание некластеризованного индекса columnstore для этого столбца. Оба улучшат производительность. Если вы находитесь на предприятии, отличный вариант - создать индексированное представление, как показано ниже:Это представление возвращает одну строку, поэтому оно почти не занимает места. Будет штраф за выполнение DML, но он не должен сильно отличаться от обслуживания индекса. При включенном индексированном представлении запрос теперь занимает 0 мс:
Актуальный план здесь . Лучшая часть этого подхода заключается в том, что время выполнения не изменяется в зависимости от размера таблицы. Единственное, что имеет значение, это то, сколько строк возвращено. Например, если вы получили первые 10000 строк, выполнение запроса теперь занимает 18 мс.
Код для заполнения таблицы:
источник
Разница в первых двух подходах
Первый план тратит около 7 из 10 секунд в операторе окна золотника, так что это главная причина , это очень медленно. Он выполняет много операций ввода-вывода в базе данных tempdb. Моя статистика ввода / вывода и время выглядят так:
Второй план способен избежать катушку, и , следовательно, целиком рабочий стол. Он просто захватывает первые 10 строк из кластеризованного индекса, а затем объединяет вложенные циклы с агрегацией (суммой), получаемой в результате отдельного сканирования кластерного индекса. Внутренняя сторона все еще заканчивается чтением всей таблицы, но таблица очень плотная, поэтому это достаточно эффективно с миллионами строк.
Улучшение производительности
Columnstore
Если вам действительно нужен подход «онлайн-отчетности», вероятно, вам лучше подходит columnstore.
Тогда этот запрос смехотворно быстр:
Вот статистика с моей машины:
Вы, вероятно, не собираетесь победить это (если вы не очень умный - хороший, Джо). Columnstore отлично умеет сканировать и собирать большие объемы данных.
Использование
ROW
вместоRANGE
оконной функцииВы можете получить очень похожую производительность со вторым запросом с помощью этого подхода, который был упомянут в другом ответе и который я использовал в приведенном выше примере columnstore ( план выполнения ):
Это приводит к меньшему количеству операций чтения, чем ваш второй подход, и никакая активность tempdb по сравнению с вашим первым подходом не происходит, потому что в памяти возникает спул окна :
К сожалению, время выполнения примерно такое же, как ваш второй подход.
Основанное на схеме решение: асинхронные промежуточные итоги
Поскольку вы открыты для других идей, вы можете рассмотреть возможность асинхронного обновления «промежуточного итога». Вы можете периодически получать результаты одного из этих запросов и загружать их в таблицу «итогов». Итак, вы бы сделали что-то вроде этого:
Загружайте его каждый день / час / что угодно (это заняло около 2 секунд на моей машине с рядами 1 мм и могло быть оптимизировано):
Тогда ваш отчетный запрос очень эффективен:
Вот прочитанная статистика:
Основанное на схеме решение: итоговые суммы с ограничениями
Действительно интересное решение этой проблемы подробно рассмотрено в этом ответе на вопрос: написание простой банковской схемы: как я должен синхронизировать свои балансы с их историей транзакций?
Основной подход заключается в отслеживании текущего промежуточного итога в строке вместе с предыдущим промежуточным итогом и порядковым номером. Затем вы можете использовать ограничения для проверки правильности и актуальности текущих итогов.
Благодарим Пола Уайта за предоставление примера реализации схемы в этом разделе вопросов и ответов:
источник
При работе с таким небольшим подмножеством возвращаемых строк хорошим вариантом является треугольное соединение. Однако при использовании оконных функций у вас есть больше опций, которые могут повысить их производительность. Опция по умолчанию для опции окна - ДИАПАЗОН, но оптимальная опция - СТРОКИ. Имейте в виду, что разница заключается не только в производительности, но и в результатах, когда участвуют связи.
Следующий код немного быстрее, чем те, которые вы представили.
источник
ROWS
. Я попробовал, но не могу сказать, что это быстрее, чем мой второй запрос. Результат былCPU time = 1438 ms, elapsed time = 1537 ms.