В версии SQL Server 2016 появился оператор пакетного режима Window Aggregate, который позволяет значительно повысить производительность при расчете оконных функций. Он не только гарантирует более высокое общее быстродействие, обеспечиваемое пакетной обработкой по сравнению с обработкой в построчном режиме. В новом операторе предусмотрены отдельные программные пути для каждой оконной функции. Разработчики устранили множество недостатков процедуры оптимизации исходного построчного режима. В частности, теперь мы избавлены от необходимости использования каталога на диске, что достигается за счет сохранения в памяти окна строк и применения многопроходного процесса обработки данного окна в потоковом режиме.

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

Тестовые данные

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

Напомню, что тестовые данные создаются в базе данных testwindow. Существует три таблицы, содержащие идентичные копии данных. Эти данные представляют собой 10 000 000 строк, где зафиксированы проходящие по банковским счетам транзакции (500 счетов × 20000 транзакций по каждому счету). В таблице Transactions используется только построчное представление данных с кластеризованным индексом двоичного дерева, определенным в списке ключей: (actid, tranid). В запросах к данной таблице сейчас используется только построчный метод обработки, поскольку применение пакетного режима обработки допускается лишь в том случае, если у таблицы имеется по крайней мере один индекс columnstore. Поэтому демонстрировать обработку в построчном режиме данных в формате rowstore я буду на примере этой таблицы. В таблице TransactionsCS имеется лишь кластеризованный индекс columnstore, поэтому я буду использовать ее, когда нужно будет продемонстрировать обработку в пакетном режиме данных в формате columnstore. Таблица TransactionsDCS организована так же, как таблица Transactions (кластеризованный индекс двоичного дерева), но она еще имеет фиктивный пустой отфильтрованный индекс columnstore, обеспечивающий возможность пакетной обработки. Я буду использовать эту таблицу, когда потребуется продемонстрировать пакетную обработку данных в формате rowstore.

Ранжирующие оконные функции

Язык программирования T-SQL дает возможность работать с четырьмя ранжирующими оконными функциями: ROW_NUMBER, RANK, DENSE_RANK и NTILE. Первые три я буду рассматривать в рамках единой группы, поскольку они оптимизированы аналогичным образом. Последняя функция будет рассматриваться отдельно, так как она оптимизирована особым способом. Запрос в листинге 2 (назовем его Query 1) показывает, как используются первые три функции при наличии вспомогательного индекса двоичного дерева.

С одной стороны, индекс обеспечивает охват элементов запроса, а с другой — позволяет обойтись без сортировки. Он строится по схеме POC, где P означает partitioning (распределение), O — ordering (упорядочение), а C — covering (охват). Части P и O составляют список ключей индекса, а часть C нужно просто включить в схему. Поскольку наш индекс двоичного дерева — кластеризованный, он естественным образом охватывает весь запрос. Если фильтры запроса строятся на базе тождеств, элементы фильтров помещаются в список ключей в качестве ведущих элементов, и в этом случае мы получаем схему FPOC (где F обозначает filtering, то есть фильтрацию). Ну а поскольку наш запрос не имеет фильтров на основе тождеств, часть F в индексе отсутствует, имеются лишь части POC.

План выполнения рассматриваемого запроса показан на рисунке 1. Он предусматривает обработку представленных в формате rowstore данных в построчном режиме.

 

План выполнения запроса (ранжирующие функции, обработка в построчном режиме данных в формате rowstore)
Рисунок 1. План выполнения запроса (ранжирующие функции, обработка в построчном режиме данных в формате rowstore)

 

Согласно этому плану, проверка индекса POC осуществляется в порядке очередности, следовательно, применять оператор сортировки необходимости нет. Один оператор Segment используется для установки флага оператора Sequence Project в случае, когда начинается новое разделение (применяется во всех трех функциях). Еще один оператор Segment используется для установки флага оператора Sequence Project в случае изменения значения упорядочения (применяется в функциях RANK и DENSE_RANK). Назначение оператора Sequence Project — выполнение реальных вычислений и повышение явного значения на основе семантики соответствующей функции. Оператор Sequence Project вычисляет ранжирующие функции в потоке и не предполагает создания каталога. По этой причине данный оператор отличается высокой эффективностью, несмотря на то что он обрабатывает данные в построчном режиме. Если при этом удается, как в рассматриваемом случае, подготовить POC-индекс, план может предусматривать использование порядка индекса. В этой ситуации необходимость в сортировке результатов отпадает, а значит, не будет и снижения производительности, связанного с выполнением такой процедуры.

Вот статистические данные по выполнению рассматриваемого запроса на моей системе: продолжительность — 12 секунд, процессор — 12 секунд, логические операции считывания — 31К, записи — 0.

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

При отсутствии вспомогательного POC-индекса скорость выполнения запроса значительно снижается, поскольку какое-то время уходит на сортировку результатов. Даже если оптимизатор выбирает режим параллельного сканирования и сортировки данных, системе приходится преобразовывать параллельные потоки в один последовательный поток с помощью оператора обмена Gather Streams. Чтобы продемонстрировать такого рода оптимизацию, нужно изменить порядок ранжирующих функций. Пусть он будет базироваться не на столбце tranid, а на столбце val (назовем этот запрос Query 2; см. листинг 3).

План выполнения этого запроса представлен на рисунке 2. Вспомогательный POC-индекс для данного запроса отсутствует, поэтому в плане предусмотрено использование оператора Sort (действие которого в данном примере распространяется и на tempdb). На этом примере удобно демонстрировать изменение показателя быстродействия при включении такой дополнительной процедуры, как сортировка. Не забывайте, что на практике запросы часто включают в себя дополнительные элементы, например фильтры диапазонов, объединения и т. д., которые могут исключать возможность создания вспомогательного POC-индекса. В таких условиях сортировка может стать неизбежной. При выполнении данного запроса я получил следующие статистические данные: продолжительность — 28 секунд, процессор — 53 секунды, логические операции считывания — 53К, записи — 13. Обратите внимание: на выполнение этого запроса ушло вдвое больше времени, чем на обработку запроса Query 1.

 

План выполнения запроса Query 2 (ранжирующие функции, построчный режим обработки данных в формате rowstore с сортировкой)
Рисунок 2. План выполнения запроса Query 2 (ранжирующие функции, построчный режим обработки данных в формате rowstore с сортировкой)

 

Для проверки быстродействия при обработке в пакетном режиме данных в формате columnstore протестируйте любой из приведенных выше запросов на содержимом таблицы TransactionsCS, как показано в листинге 4 (назовем этот запрос Query 3).

План выполнения данного запроса представлен на рисунке 3.

 

План выполнения запроса Query 3 (ранжирующие функции, обработка в пакетном режиме данных в формате columnstore)
Рисунок 3. План выполнения запроса Query 3 (ранжирующие функции, обработка в пакетном режиме данных в формате columnstore)

 

Помните, что в ходе выполнения запроса к данным в формате columnstore сортировка для расчета оконных функций необходима, поскольку данные в этом формате не являются упорядоченными. Однако при этом мы получаем ряд преимуществ: сокращаются издержки на операции ввода-вывода, данные обрабатываются в пакетном режиме и применяется параллелизм, то есть по сравнению с обработкой в построчном режиме потенциал для масштабирования с использованием большего числа процессоров значительно возрастает. По итогам выполнения данного запроса я получил следующие статистические данные: продолжительность — 10 секунд, процессор — 23 секунды, логические операции считывания — 6К, записи — 0. Имейте в виду: мой тестовый ноутбук оснащен четырьмя логическими процессорами (два ядра, реализующие гиперпотоковую технологию). Естественно, на более мощной системе с большим числом процессоров производительность была бы значительно выше.

Аналогичный план и такие же статистические показатели мы получим, если изменим порядок окон так, что он будет базироваться на столбце val, а не на столбце tranid. Проверить это утверждение вы можете с помощью запроса, приведенного в листинге 5 (назовем его Query 4).

Для проверки быстродействия при выполнении данного запроса в пакетном режиме с оптимизаций rowstore протестируйте его на содержимом таблицы TransactionsDCS, как показано в листинге 6 (назовем этот запрос Query 5).

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

Если мы направим запрос к таблице TransactionsDCS, не располагая POC-индексом для применяемых оконных функций, то в условиях, когда в любом случае необходимо задействовать явную процедуру сортировки, оптимизатор делает выбор в пользу пакетного режима в рамках стратегии хранилища строк. Это видно на примере следующего запроса, показанного в листинге 7 (назовем его Query 6).

План выполнения данного запроса показан на рисунке 4.

 

План выполнения запроса Query 6 (ранжирующие функции, пакетный режим применительно к данным в формате rowstore)
Рисунок 4. План выполнения запроса Query 6 (ранжирующие функции, пакетный режим применительно к данным в формате rowstore)

 

Это параллельный план. Отметим, что индекс двоичного дерева проверяется в построчном режиме в форме Ordered: False, поскольку его порядок не используется при работе с оконными функциями. Оставшаяся часть работы выполняется с помощью операторов пакетного режима. При выполнении данного запроса я получил следующие статистические данные: продолжительность — 12 секунд, процессор — 30 секунд, логические операции считывания — 31К, записи — 0.

На рисунке 5 представлены сводные показатели времени выполнения для различных запросов.

 

Графическая сводка времени выполнения трех ранжирующих функций
Рисунок 5. Графическая сводка времени выполнения трех ранжирующих функций

 

Как видите, обработка данных хранилища строк в построчном режиме дает сравнительно неплохие результаты лишь тогда, когда имеется вспомогательный POC-индекс, позволяющий обойтись без процедуры сортировки. Но как бы то ни было, этот план — последовательный, и мы не сможем улучшить результат, воспользовавшись мощной машиной с большим количеством процессоров. Применение пакетного режима дает хорошие результаты даже в тех случаях, когда необходимо осуществлять сортировку. Кроме того, этот режим прекрасно обеспечивает масштабирование с использованием нескольких процессоров.

Оконная функция NTILE вычисляет количество блоков для результирующих строк; при этом размер блока представляет собой частное от деления общего числа строк на требуемое число блоков. Рассмотрим для примера запрос, приведенный в листинге 8 (назовем его Query 7). Этот запрос к таблице Transactions (построчный режим применительно к данным в формате rowstore) обеспечивает расчет чисел на базе необходимого количества блоков, равного 10.

План выполнения данного запроса представлен на рисунке 6.

 

План выполнения запроса Query 7 (NTILE, построчный режим применительно к данным в формате rowstore)
Рисунок 6. План выполнения запроса Query 7 (NTILE, построчный режим применительно к данным в формате rowstore)

 

Как вы можете убедиться, план тщательно проработан и в чем-то напоминает продемонстрированные мною в предыдущей статье планы для агрегатных оконных функций без фрейма. Перед тем как функция NTILE сможет вычислить размер плитки, она должна будет вычислить число строк в разделе. Расчет числа строк оптимизируется подобно тому, как будет оптимизироваться функция COUNT (*) OVER (PARTITION BY actid). План считывает упорядоченные данные из индекса rowstore двоичного дерева, сегментирует их в соответствии с содержимым столбца actid и сохраняет эти данные в дисковом каталоге. Для каждого сегмента средняя и нижняя ветви плана считывают данные из каталога один раз для того, чтобы рассчитать число строк, и второй раз — чтобы получить детали. Последняя часть плана (расположенная в левой верхней части) обеспечивает повторное сегментирование данных в соответствии с содержимым столбца actid, после чего оператор Sequence Project вычисляет число блоков. Этот план нельзя назвать особенно эффективным главным образом вследствие использования дискового каталога.

При выполнении данного запроса я получил следующие статистические данные: продолжительность — 47 секунд, процессор — 47 секунд, логические операции считывания — 20М, записи — 40К.

Если бы у меня не было вспомогательного POC-индекса, ситуация лишь усугубилась бы, поскольку в план пришлось бы включать процедуру сортировки. Например, когда в последнем запросе я заменил упорядочивающий элемент tranid на val, время выполнения этого запроса увеличилось до 57 секунд.

Ту часть расчета, которая связана с определением количества, можно выполнить методом параллельной пакетной обработки (оператор Window Aggregate), если присутствует индекс columnstore. При выполнении своих тестов я всегда произвожу расчет числа блоков в последовательной зоне. Если для расчета числа блоков оптимизатор выбирает метод обработки в построчном режиме, он использует комбинацию операторов Segment и Sequence project. Если же выбирается обработка в пакетном режиме, используется оператор Window Aggregate. Поскольку при выполнении последнего запроса наиболее значительная потеря в быстродействии приходилась на этап расчета количества и была связана с использованием дискового каталога, применение пакетной обработки в данном случае может дать значительное повышение производительности, даже если потребуется выполнить процедуру сортировки. Чтобы проиллюстрировать эту мысль, обратимся к таблице TransactionsCS, как показано в листинге 9 (назовем данный запрос Query 8).

План выполнения данного запроса показан на рисунке 7.

 

Рисунок 7. План выполнения запроса Query 8 (NTILE, пакетный режим применительно к данным в формате columnstore)
Рисунок 7. План выполнения запроса Query 8 (NTILE, пакетный режим применительно к данным в формате columnstore)

 

Обратите внимание на то, что функция count aggregate вычисляется с помощью оператора пакетного режима Window Aggregate в параллельной зоне плана, тогда как число блоков определяется в режиме построчной обработки с помощью операторов Segment и Sequence Project в последовательной зоне плана. При выполнении данного запроса я получил следующие статистические показатели: продолжительность — 14 секунд, процессор — 23 секунды, логические операции считывания — 6К, записи — 0. По сравнению с показателем около минуты этот результат можно считать немалым достижением.

Чтобы протестировать процедуру пакетной обработки данных в формате хранилища строк, не прибегая при этом к сортировке, воспользуйтесь запросом к таблице TransactionsDCS, приведенным в листинге 10 (назовем его Query 9).

План выполнения этого запроса показан на рисунке 8.

 

План выполнения запроса Query 9 (NTILE, пакетный режим применительно к данным в формате rowstore, без сортировки)
Рисунок 8. План выполнения запроса Query 9 (NTILE, пакетный режим применительно к данным в формате rowstore, без сортировки)

 

Это последовательный план, но применять процедуру сортировки не требуется, поскольку данные извлекаются из индекса хранилища строк двоичного дерева в упорядоченном виде. Как вычисление count aggregate, так и расчет числа блоков выполняются с помощью операторов пакетного режима Window Aggregate. При выполнении данного запроса я получил следующие статистические показатели: продолжительность — 8 секунд, процессор — 8 секунд, логические операции считывания — 31К, записи — 0. Время выполнения запроса сократилось до 8 секунд!

Если бы у меня не было вспомогательного POC-индекса, тогда в процессе вычисления оконных функций мне пришлось бы иметь дело с дополнительными издержками на сортировку. Чтобы продемонстрировать такой вариант, измените порядок функции NTILE. Пусть она будет базироваться не на столбце tranid, а на столбце val (назовем этот запрос Query 10; см. листинг 11).

План выполнения этого запроса показан на рисунке 9.

 

Рисунок 9. План выполнения запроса Query 10 (NTILE, пакетный режим применительно к данным в формате rowstore, с сортировкой)
Рисунок 9. План выполнения запроса Query 10 (NTILE, пакетный режим применительно к данным в формате rowstore, с сортировкой)

 

В соответствии с этим планом индекс двоичного дерева rowstore проверяется в построчном режиме в форме Ordered: False. Далее план предусматривает для расчета count использование операторов пакетного режима Sort и Window Aggregate. Затем план переходит в последовательную зону и предусматривает расчет числа блоков в построчном режиме с использованием операторов Segment и Sequence Project. Я получил следующие статистические показатели по длительности выполнения этого запроса: продолжительность — 16 секунд, процессор — 30 секунд, логические операции считывания — 31К, записи — 0.

Диаграмма, представленная на рисунке 10, показывает время выполнения различных запросов, вычисляющих функцию NTILE.

 

Диаграмма, иллюстрирующая выполнение функции NTILE
Рисунок 10. Диаграмма, иллюстрирующая выполнение функции NTILE

 

Как вы можете убедиться, производительность запросов, осуществляющих обработку в пакетном режиме, значительно выше, чем у запросов, использующих построчный режим обработки

Статистические оконные функции

Язык T-SQL допускает использование двух пар оконных функций распределения, выполняющих статистические вычисления. Обратные функции распределения PERCENTILE_CONT и PERCENTILE_DISC вычисляют процентили с использованием недискретных и дискретных моделей распределения, соответственно. Функции распределения рангов PERCENT_RANK и CUME_DIST вычисляют процентили и кумулятивное распределение соответственно. Подробное описание значения этих функций можно найти в разделе Window Distribution Functions статьи Microsoft SQL Server 2012: How to Write T-SQL Window Functions, Part 2 (http://sqlmag.com/sql-server-2012/microsoft-sql-server-2012-how-write-t-sql-window-functions-part-2).

Традиционный вариант выполнения этих функций — обработка в построчном режиме данных в формате rowstore — отличается весьма низкой эффективностью. Объясняется это главным образом фактом использования дискового каталога, в ряде случаев — по несколько раз в ходе выполнения плана запроса. Для тестирования скорости выполнения этих функций я использовал запросы в листинге 12 (показатели быстродействия включены в текст в виде комментариев).

На рисунке 11 представлена диаграмма с сопоставлением времени выполнения различных запросов, вычисляющих функцию PERCENTILE_CONT. Рабочие характеристики для функции PERCENTILE_DISC аналогичны.

 

Рисунок 11. Сводная диаграмма, иллюстрирующая производительность функций PERCENTILE_CONT и PERCENTILE_DISC
Рисунок 11. Сводная диаграмма, иллюстрирующая производительность функций PERCENTILE_CONT и PERCENTILE_DISC

 

На рисунке 12 представлена диаграмма с сопоставлением времени выполнения различных запросов, вычисляющих функции PERCENT_RANK и CUME_DIST.

 

Сводная диаграмма, иллюстрирующая производительность функций PERCENT_RANK и CUME_DIST
Рисунок 12. Сводная диаграмма, иллюстрирующая производительность функций PERCENT_RANK и CUME_DIST

 

Итак, в этой статье мы рассмотрели оптимизацию ранжирующих и статистических оконных функций с использованием оператора пакетного режима Window Aggregate. Результаты, достигаемые с помощью нового оператора, впечатляют. Они особенно значительны при оптимизации функций, которые ранее оптимизировались с помощью дискового каталога, таких как NTILE, PERCENTILE_CONT, PERCENTILE_DISC, PERCENT_RANK и CUME_DIST. Одно из важных достоинств оптимизированного оператора состоит в том, что при его использовании нет необходимости вносить в код какие-либо изменения. Требуется только наличие индекса columnstore, пусть даже речь идет о фиктивном пустом индексе, который сделает возможным применение операторов пакетного режима, причем вероятно, что это требование в дальнейшем будет отменено.

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

Листинг 1. Создание тестовых данных
-- База данных testwindow
SET NOCOUNT ON;
IF DB_ID(N'testwindow') IS NULL CREATE DATABASE testwindow;
GO
USE testwindow;
GO
-- GetNums helper function
DROP FUNCTION IF EXISTS dbo.GetNums;
GO
CREATE FUNCTION dbo.GetNums(@low AS BIGINT, @high AS BIGINT) RETURNS TABLE
AS
RETURN
  WITH
    L0   AS (SELECT c FROM (SELECT 1 UNION ALL SELECT 1) AS D(c)),
    L1   AS (SELECT 1 AS c FROM L0 AS A CROSS JOIN L0 AS B),
    L2   AS (SELECT 1 AS c FROM L1 AS A CROSS JOIN L1 AS B),
    L3   AS (SELECT 1 AS c FROM L2 AS A CROSS JOIN L2 AS B),
    L4   AS (SELECT 1 AS c FROM L3 AS A CROSS JOIN L3 AS B),
    L5   AS (SELECT 1 AS c FROM L4 AS A CROSS JOIN L4 AS B),
    Nums AS (SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS rownum
             FROM L5)
  SELECT TOP(@high - @low + 1) @low + rownum - 1 AS n
  FROM Nums
  ORDER BY rownum;
GO
-- Таблица транзакций с 10 миллионами строк (200 счетов х 50000 транзакций на каждом счету)
-- Традиционный индекс двоичного дерева для формата rowstore
DROP TABLE IF EXISTS dbo.Transactions;
CREATE TABLE dbo.Transactions
(
  actid  INT   NOT NULL,
  tranid INT   NOT NULL,
  val    MONEY NOT NULL,
  CONSTRAINT PK_Transactions PRIMARY KEY(actid, tranid)
);
GO
DECLARE
  @num_partitions     AS INT = 200,
  @rows_per_partition AS INT = 50000;
INSERT INTO dbo.Transactions WITH (TABLOCK) (actid, tranid, val)
  SELECT NP.n, RPP.n,
    (ABS(CHECKSUM(NEWID())%2)*2-1) * (1 + ABS(CHECKSUM(NEWID())%5))
  FROM dbo.GetNums(1, @num_partitions) AS NP
    CROSS JOIN dbo.GetNums(1, @rows_per_partition) AS RPP;
GO
Листинг 2. Запрос Query 1
SELECT actid, tranid, val,
  ROW_NUMBER() OVER(PARTITION BY actid ORDER BY tranid) AS rownum,
  RANK() OVER(PARTITION BY actid ORDER BY tranid) AS rnk,
  DENSE_RANK() OVER(PARTITION BY actid ORDER BY tranid) AS drnk
FROM dbo.Transactions;
Листинг 3. Запрос Query 2
SELECT actid, tranid, val,
  ROW_NUMBER() OVER(PARTITION BY actid ORDER BY val) AS rownum,
  RANK() OVER(PARTITION BY actid ORDER BY val) AS rnk,
  DENSE_RANK() OVER(PARTITION BY actid ORDER BY val) AS drnk
FROM dbo.Transactions;
Листинг 4. Запрос Query 3
SELECT actid, tranid, val,
  ROW_NUMBER() OVER(PARTITION BY actid ORDER BY tranid) AS rownum,
  RANK() OVER(PARTITION BY actid ORDER BY tranid) AS rnk,
  DENSE_RANK() OVER(PARTITION BY actid ORDER BY tranid) AS drnk
FROM dbo.TransactionsCS;
Листинг 5. Запрос Query 4 
SELECT actid, tranid, val,
  ROW_NUMBER() OVER(PARTITION BY actid ORDER BY val) AS rownum,
  RANK() OVER(PARTITION BY actid ORDER BY val) AS rnk,
  DENSE_RANK() OVER(PARTITION BY actid ORDER BY val) AS drnk
FROM dbo.TransactionsCS;
Листинг 6. Запрос Query 5
SELECT actid, tranid, val,
  ROW_NUMBER() OVER(PARTITION BY actid ORDER BY tranid) AS rownum,
  RANK() OVER(PARTITION BY actid ORDER BY tranid) AS rnk,
  DENSE_RANK() OVER(PARTITION BY actid ORDER BY tranid) AS drnk
FROM dbo.TransactionsDCS;
Листинг 7. Запрос Query 6
SELECT actid, tranid, val,
  ROW_NUMBER() OVER(PARTITION BY actid ORDER BY val) AS rownum,
  RANK() OVER(PARTITION BY actid ORDER BY val) AS rnk,
  DENSE_RANK() OVER(PARTITION BY actid ORDER BY val) AS drnk
FROM dbo.TransactionsDCS;
Листинг 8. Запрос Query 7
SELECT actid, tranid, val,
  NTILE(10) OVER(PARTITION BY actid ORDER BY tranid) AS ntile10
FROM dbo.Transactions;
Листинг 9. Запрос Query 8
SELECT actid, tranid, val,
  NTILE(10) OVER(PARTITION BY actid ORDER BY tranid) AS ntile10
FROM dbo.TransactionsCS;
Листинг 10. Запрос Query 9
SELECT actid, tranid, val,
  NTILE(10) OVER(PARTITION BY actid ORDER BY tranid) AS ntile10
FROM dbo.TransactionsDCS;
Листинг 11. Запрос Query 10
SELECT actid, tranid, val,
  NTILE(10) OVER(PARTITION BY actid ORDER BY val) AS ntile10
FROM dbo.TransactionsDCS;
Листинг 12. Запросы для тестирования скорости выполнения функций
---------------------------------------------------------------------
-- PERCENTILE_CONT (PERCENTILE_DISC оптимизируется аналогичным образом)
---------------------------------------------------------------------
-- Query 11, построчный режим обработки данных в формате rowstore, без сортировки
-- Два цикла записи в дисковый каталог и считывания данных из него
-- продолжительность - 110 секунд, процессор - 121 секунда, логические операции считывания - 41М, записи - 101К
-- Если заменить на ORDER BY val, потребуется выполнить сортировку
SELECT actid, tranid, val,
  PERCENTILE_CONT(0.5)
    WITHIN GROUP(ORDER BY tranid) OVER(PARTITION BY actid) AS mediantid
FROM dbo.Transactions;
-- Query 12, пакетный режим, формат данных columnstore, сортировка, использование каталога не предусмотрено
-- продолжительность - 14 секунд, процессор - 13 секунд, логические операции считывания - 5К, записи - 0
SELECT actid, tranid, val,
  PERCENTILE_CONT(0.5)
    WITHIN GROUP(ORDER BY tranid) OVER(PARTITION BY actid) AS mediantid
FROM dbo.TransactionsCS;
-- Query 13, пакетный режим, формат данных rowstore, без сортировки, без использования каталога
-- продолжительность - 11 секунд, процессор - 11 секунд, логические операции считывания - 31К, записи - 0
SELECT actid, tranid, val,
  PERCENTILE_CONT(0.5)
    WITHIN GROUP(ORDER BY tranid) OVER(PARTITION BY actid) AS mediantid
FROM dbo.TransactionsDCS;
---------------------------------------------------------------------
-- PERCENT_RANK and CUME_DIST
---------------------------------------------------------------------
-- Query 14, построчный режим, данные в формате rowstore, без сортировки, дисковый каталог
-- продолжительность - 54 секунды, процессор - 53 секунды, логические операции считывания - 20М, записи - 40К
SELECT actid, tranid, val,
  PERCENT_RANK() OVER(PARTITION BY actid ORDER BY tranid) AS pctrk
FROM dbo.Transactions;
-- Query 15, построчный режим, данные в формате rowstore, без сортировки, дисковый каталог
-- продолжительность - 239 секунд, процессор - 250 секунд, логические операции считывания - 125М, записи - 82К
SELECT actid, tranid, val,
  CUME_DIST() OVER(PARTITION BY actid ORDER BY tranid) AS cumedist
FROM dbo.Transactions;
-- Query 16, пакетный режим, данные в формате columnstore, сортировка, без использования каталога
-- продолжительность - 17 секунд, процессор - 17 секунд, логические операции считывания - 6К, записи - 0К
SELECT actid, tranid, val,
  PERCENT_RANK() OVER(PARTITION BY actid ORDER BY tranid) AS pctrk
FROM dbo.TransactionsCS;
-- Query 17, пакетный режим, формат данных columnstore, сортировка, без использования каталога
-- продолжительность - 11 секунд, процессор - 10 секунд, логические операции считывания - 6К, записи - 0К
SELECT actid, tranid, val,
  CUME_DIST() OVER(PARTITION BY actid ORDER BY tranid) AS cumedist
FROM dbo.TransactionsCS;
-- Query 18, пакетный режим, формат данных rowstore, без сортировки, без использования каталога
-- продолжительность - 8 секунд, процессор - 8 секунд, логические операции считывания - 31К, записи - 0К
SELECT actid, tranid, val,
  PERCENT_RANK() OVER(PARTITION BY actid ORDER BY tranid) AS pctrk
FROM dbo.TransactionsDCS;
-- Query 19, пакетный режим, формат данных rowstore, без сортировки, без использования каталога
-- продолжительность - 8 секунд, процессор - 8 секунд, логические операции считывания - 31К, записи - 0К
SELECT actid, tranid, val,
  CUME_DIST() OVER(PARTITION BY actid ORDER BY tranid) AS cumedist
FROM dbo.TransactionsDCS;