Отличным ресурсом для расчета промежуточных итогов в SQL Server является этот документ Ицик Бен Ган, который был представлен команде SQL Server в рамках его кампании, чтобы предложение OVER
было расширено от егоначальная реализация SQL Server 2005.В нем он показывает, как, попав в десятки тысяч строк, курсоры выполняют решения на основе множеств.SQL Server 2012 действительно расширил предложение OVER
, сделав этот тип запроса намного проще.
SELECT col1,
SUM(col1) OVER (ORDER BY ind ROWS UNBOUNDED PRECEDING)
FROM @tmp
Поскольку вы работаете на SQL Server 2005, однако он вам недоступен.
АдамМачаник показывает здесь , как можно использовать CLR для улучшения производительности стандартных курсоров TSQL.
Для этого определения таблицы
CREATE TABLE RunningTotals
(
ind int identity(1,1) primary key,
col1 int
)
Я создаю таблицы с обоими 2000и 10 000 строк в базе данных с ALLOW_SNAPSHOT_ISOLATION ON
и одной с отключенной настройкой (причина этого в том, что мои первоначальные результаты были в БД с настройкой, которая привела к удивительному аспекту результатов).
Кластерные индексы для всех таблиц имели только одну корневую страницу.Количество листовых страниц для каждого показано ниже.
+-------------------------------+-----------+------------+
| | 2,000 row | 10,000 row |
+-------------------------------+-----------+------------+
| ALLOW_SNAPSHOT_ISOLATION OFF | 5 | 22 |
| ALLOW_SNAPSHOT_ISOLATION ON | 8 | 39 |
+-------------------------------+-----------+------------+
Я проверил следующие случаи (ссылки показывают планы выполнения)
- Левое объединение и группировка по
- коррелированный подзапрос план строки 2000 , 10000 план строки
- CTE из ответа Микаэля (обновлено)
- CTE ниже
Причина включения дополнительной опции CTE была в том, чтобы предоставить решение CTE, которое все еще работало бы, если столбец ind
не был гарантированпоследовательный.
SET STATISTICS IO ON;
SET STATISTICS TIME ON;
DECLARE @col1 int, @sumcol1 bigint;
WITH RecursiveCTE
AS (
SELECT TOP 1 ind, col1, CAST(col1 AS BIGINT) AS Total
FROM RunningTotals
ORDER BY ind
UNION ALL
SELECT R.ind, R.col1, R.Total
FROM (
SELECT T.*,
T.col1 + Total AS Total,
rn = ROW_NUMBER() OVER (ORDER BY T.ind)
FROM RunningTotals T
JOIN RecursiveCTE R
ON R.ind < T.ind
) R
WHERE R.rn = 1
)
SELECT @col1 =col1, @sumcol1=Total
FROM RecursiveCTE
OPTION (MAXRECURSION 0);
Во все запросы добавлено CAST(col1 AS BIGINT)
, чтобы избежать ошибок переполнения во время выполнения.Кроме того, для всех них я назначил результаты переменным, как указано выше, чтобы исключить время, затрачиваемое на отправку результатов из рассмотрения.
Результаты
+------------------+----------+--------+------------+---------------+------------+---------------+-------+---------+
| | | | Base Table | Work Table | Time |
+------------------+----------+--------+------------+---------------+------------+---------------+-------+---------+
| | Snapshot | Rows | Scan count | logical reads | Scan count | logical reads | cpu | elapsed |
| Group By | On | 2,000 | 2001 | 12709 | | | 1469 | 1250 |
| | On | 10,000 | 10001 | 216678 | | | 30906 | 30963 |
| | Off | 2,000 | 2001 | 9251 | | | 1140 | 1160 |
| | Off | 10,000 | 10001 | 130089 | | | 29906 | 28306 |
+------------------+----------+--------+------------+---------------+------------+---------------+-------+---------+
| Sub Query | On | 2,000 | 2001 | 12709 | | | 844 | 823 |
| | On | 10,000 | 2 | 82 | 10000 | 165025 | 24672 | 24535 |
| | Off | 2,000 | 2001 | 9251 | | | 766 | 999 |
| | Off | 10,000 | 2 | 48 | 10000 | 165025 | 25188 | 23880 |
+------------------+----------+--------+------------+---------------+------------+---------------+-------+---------+
| CTE No Gaps | On | 2,000 | 0 | 4002 | 2 | 12001 | 78 | 101 |
| | On | 10,000 | 0 | 20002 | 2 | 60001 | 344 | 342 |
| | Off | 2,000 | 0 | 4002 | 2 | 12001 | 62 | 253 |
| | Off | 10,000 | 0 | 20002 | 2 | 60001 | 281 | 326 |
+------------------+----------+--------+------------+---------------+------------+---------------+-------+---------+
| CTE Alllows Gaps | On | 2,000 | 2001 | 4009 | 2 | 12001 | 47 | 75 |
| | On | 10,000 | 10001 | 20040 | 2 | 60001 | 312 | 413 |
| | Off | 2,000 | 2001 | 4006 | 2 | 12001 | 94 | 90 |
| | Off | 10,000 | 10001 | 20023 | 2 | 60001 | 313 | 349 |
+------------------+----------+--------+------------+---------------+------------+---------------+-------+---------+
И коррелированный подзапрос, и GROUP BY
версия использует «треугольные» объединения вложенных циклов, управляемые сканированием кластеризованного индекса в таблице RunningTotals
(T1
), и для каждой строки, возвращаемой этим сканированием, поиск обратно в таблицу (T2
), самосоединение в T2.ind<=T1.ind
.
Это означает, что одни и те же строки обрабатываются неоднократно.Когда строка T1.ind=1000
обработана, самообъединение извлекает и суммирует все строки с ind <= 1000
, затем для следующей строки, где T1.ind=1001
те же 1000 строк извлекаются снова и суммируются вместе с одной дополнительнойстрока и т. д.
Общее количество таких операций для таблицы 2000 строк составляет 2 001 000, для строк 10 000 - 50 005 000 или более (n² + n) / 2
, что явно растет в геометрической прогрессии.
В случае с 2000 строками основное различие между GROUP BY
и версиями подзапроса состоит в том, что первый имеет агрегат потока после объединения и поэтому в него входят три столбца (T1.ind
, T2.col1
, T2.col1
) и свойство GROUP BY
, равное T1.ind
, тогда как последнее рассчитывается как скалярный агрегат с агрегатом потока перед объединением, в него подается только T2.col1
и вообще не имеет свойства GROUP BY
.Можно заметить, что это более простое расположение дает ощутимое преимущество с точки зрения сокращения процессорного времени.
Для случая с 10 000 строк есть дополнительное различие в плане подзапроса.Он добавляет нетерпеливая шпуля , которая копирует все значения ind,cast(col1 as bigint)
в tempdb
.В случае, когда используется изоляция моментальных снимков, это работает более компактно, чем структура кластерного индекса, и чистый эффект заключается в сокращении числа операций чтения примерно на 25% (поскольку базовая таблица сохраняет довольно много пустого пространства для информации о версиях),когда эта опция выключена, она работает менее компактно (предположительно из-за разницы bigint
против int
) и дает больше результатов чтения.Это уменьшает разрыв между подзапросом и группой по версиям, но подзапрос все еще выигрывает.
Однако явным победителем стал Рекурсивный CTE.Для версии «без пробелов» логические чтения из базовой таблицы теперь 2 x (n + 1)
, отражающие индекс n
поиска в двухуровневом индексе для извлечения всех строк плюс дополнительной в конце, которая ничего не возвращает и завершает рекурсию,Это по-прежнему означало 20 002 чтения для обработки таблицы из 22 страниц!
Логические чтения рабочей таблицы для рекурсивной версии CTE очень высоки.Кажется, он работает при 6 чтениях рабочих таблиц на исходную строку.Они поступают из катушки индекса, в которой хранятся выходные данные предыдущей строки, а затем снова считываются на следующей итерации (хорошее объяснение этого Умачандар Джаячандран здесь ).Несмотря на большое количество, это по-прежнему лучший исполнитель.