GROUP BY с МАКСОМ и просто МАКСИМАЛЬНЫМ

8

Я программист, занимаюсь большой таблицей по следующей схеме:

UpdateTime, PK, datetime, notnull
Name, PK, char(14), notnull
TheData, float

Есть кластерный индекс на Name, UpdateTime

Мне было интересно, что должно быть быстрее:

SELECT MAX(UpdateTime)
FROM [MyTable]

или

SELECT MAX([UpdateTime]) AS value
from
   (
    SELECT [UpdateTime]
    FROM [MyTable]
    group by [UpdateTime]
   ) as t

Вставки в эту таблицу представлены фрагментами по 50000 строк с одинаковой датой . Поэтому я подумал, что группировка по может облегчить MAXрасчет.

Вместо того, чтобы пытаться найти максимум 150000 строк, сгруппировать по 3 строкам, и тогда вычисление MAXбудет быстрее? Правильно ли мое предположение, или группировка также является дорогостоящей?

Ofiris
источник

Ответы:

12

Я создал таблицу big_table по вашей схеме

create table big_table
(
    updatetime datetime not null,
    name char(14) not null,
    TheData float,
    primary key(Name,updatetime)
)

Затем я заполнил таблицу 50000 строк с этим кодом:

DECLARE @ROWNUM as bigint = 1
WHILE(1=1)
BEGIN
    set @rownum  = @ROWNUM + 1
    insert into big_table values(getdate(),'name' + cast(@rownum as CHAR), cast(@rownum as float))
    if @ROWNUM > 50000
        BREAK;  
END

Используя SSMS, я протестировал оба запроса и понял, что в первом запросе вы ищете MAX для TheData, а во втором - MAX для времени обновления

Таким образом, я изменил первый запрос, чтобы получить также максимум обновления

set statistics time on -- execution time
set statistics io on -- io stats (how many pages read, temp tables)

-- query 1
SELECT MAX([UpdateTime])
FROM big_table

-- query 2
SELECT MAX([UpdateTime]) AS value
from
   (
    SELECT [UpdateTime]
    FROM big_table
    group by [UpdateTime]
   ) as t


set statistics time off
set statistics io off

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

Используя статистику ввода / вывода, я получаю информацию о дисковой активности

STATISTICS TIME и STATISTICS IO предоставляют полезную информацию. Например, были использованы временные таблицы (указаны рабочим столом). Также, сколько прочитанных логических страниц было прочитано, что указывает на количество страниц базы данных, прочитанных из кэша.

Затем я активирую план выполнения с помощью CTRL + M (активирует показ фактического плана выполнения), а затем выполняю с помощью F5.

Это обеспечит сравнение обоих запросов.

Вот вывод вкладки « Сообщения»

- Запрос 1

Стол "большой_таблицы". Сканирование 1, логическое чтение 543 , физическое чтение 0, чтение с опережением 0, логическое чтение с 0, физическое чтение с 0, чтение с опережением 0.

Время выполнения SQL Server: время ЦП = 16 мс, прошедшее время = 6 мс .

- Запрос 2

Стол « Рабочий стол ». Сканирование счетчик 0, логическое чтение 0, физическое чтение 0, чтение с опережением 0, логическое чтение с бита 0, физическое чтение с бита 0, чтение с опережением чтения 0.

Стол "большой_таблицы". Сканирование 1, логическое чтение 543 , физическое чтение 0, чтение с опережением 0, логическое чтение с 0, физическое чтение с 0, чтение с опережением 0.

Время выполнения SQL Server: время ЦП = 0 мс, прошедшее время = 35 мс .

Оба запроса приводят к 543 логическим чтениям, но у второго запроса истекшее время 35 мс, тогда как у первого - только 6 мс. Вы также заметите, что второй запрос приводит к использованию временных таблиц в базе данных tempdb, обозначенных словом worktable . Несмотря на то, что все значения для рабочего стола равны 0, работа все еще выполнялась в базе данных tempdb.

Затем выводится вкладка фактического плана выполнения рядом с вкладкой «Сообщения».

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

Согласно плану выполнения, предоставленному MSSQL, второй предоставленный вами запрос имеет общую стоимость пакета 64%, тогда как первый запрос стоит всего 36% от общего пакета, поэтому первый запрос требует меньше работы.

Используя SSMS, вы можете тестировать и сравнивать свои запросы и точно выяснять, как MSSQL анализирует ваши запросы и какие объекты: таблицы, индексы и / или статистические данные, если таковые используются, для удовлетворения этих запросов.

Еще одно дополнительное замечание, которое следует учитывать при тестировании, - это очистить кеш перед тестированием, если это возможно. Это помогает гарантировать, что сравнения точны, и это важно, когда вы думаете об активности диска. Я начинаю с DBCC DROPCLEANBUFFERS и DBCC FREEPROCCACHE, чтобы очистить весь кэш. Однако будьте осторожны, чтобы не использовать эти команды на рабочем сервере, который фактически используется, поскольку вы фактически заставите сервер читать все данные с диска в память.

Вот соответствующая документация.

  1. Очистить кэш плана с помощью DBCC FREEPROCCACHE
  2. Очистить все из пула буферов с помощью DBCC DROPCLEANBUFFERS

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

Обновлено 28.10 12:46

Внесены исправления в изображение плана выполнения и вывод статистики.

Крейг Эфрейн
источник
Спасибо за глубокий ответ, обратите внимание на мою лысую строку в коде, каждая группа из 50000 строк имеет одинаковую дату, которая отличается от других фрагментов. Так что я должен выйти getdate()из цикла
Ofiris
1
Привет @Ofiris. Ответ, который я дал, на самом деле просто, чтобы помочь вам сделать сравнение самостоятельно. Я создал случайные ненужные данные, чтобы проиллюстрировать использование различных команд и инструментов, которые вы можете использовать, чтобы прийти к своим собственным выводам.
Крейг Эфрейн
1
В tempdb работа не выполнялась. Рабочий стол предназначен для управления разделами в случае, если агрегат хеша должен перетекать в базу данных tempdb, поскольку для него было зарезервировано недостаточно памяти. Пожалуйста, подчеркните, что затраты всегда являются оценочными даже в «фактическом» плане. Это оценки оптимизатора, которые могут не иметь большого отношения к реальной производительности. Не используйте% партии в качестве основного показателя настройки. Очистка буферов важна, только если вы хотите проверить производительность холодного кэша.
Пол Уайт 9
1
Привет @PaulWhite. Спасибо за дополнительную информацию, я искренне благодарен за любые предложения о том, как быть более точным. Хотя, когда вы произносите свои предложения: «Не используйте», не может ли это быть неверно истолковано как предоставление приказа, а не профессиональная консультация? С наилучшими пожеланиями.
Крейг Эфрейн
@CraigEfrein Вероятно. Я был кратким, чтобы вписаться в поле для комментариев разрешено.
Пол Уайт 9
6

Вставки в эту таблицу представлены фрагментами по 50000 строк с одинаковой датой. Поэтому я подумал, что группировка по может облегчить расчет MAX.

Перезапись могла бы помочь, если в SQL Server реализовано сканирование с пропуском индекса, но это не так.

Пропуск сканирования индекса позволяет ядру базы данных искать следующее другое значение индекса вместо сканирования всех дубликатов (или нерелевантных подключей) между ними. В вашем случае, skip-scan позволит движку найти MAX(UpdateTime)первый Name, перейти ко MAX(UpdateTime)второму Name... и так далее. Последним шагом будет поиск MAX(UpdateTime)кандидатов по одному имени.

Вы можете смоделировать это в некоторой степени с помощью рекурсивного CTE, но это немного грязно и не так эффективно, как встроенное skip-scan:

WITH RecursiveCTE
AS
(
    -- Anchor: MAX UpdateTime for
    -- highest-sorting Name
    SELECT TOP (1)
        BT.Name,
        BT.UpdateTime
    FROM dbo.BigTable AS BT
    ORDER BY
        BT.Name DESC,
        BT.UpdateTime DESC

    UNION ALL

    -- Recursive part
    -- MAX UpdateTime for Name
    -- that sorts immediately lower
    SELECT
        SubQuery.Name,
        SubQuery.UpdateTime
    FROM 
    (
        SELECT
            BT.Name,
            BT.UpdateTime,
            rn = ROW_NUMBER() OVER (
                ORDER BY BT.Name DESC, BT.UpdateTime DESC)
        FROM RecursiveCTE AS R
        JOIN dbo.BigTable AS BT
            ON BT.Name < R.Name
    ) AS SubQuery
    WHERE
        SubQuery.rn = 1
)
-- Final MAX aggregate over
-- MAX(UpdateTime) per Name
SELECT MAX(UpdateTime) 
FROM RecursiveCTE
OPTION (MAXRECURSION 0);

План рекурсивного CTE

Этот план выполняет поиск по одному для каждого отдельного Name, а затем находит наибольшее UpdateTimeиз кандидатов. Его производительность по сравнению с простым полным сканированием таблицы зависит от того, сколько дубликатов существует Name, и от того, есть ли в памяти страницы, к которым обращается поиск по одиночке, или нет.

Альтернативные решения

Если вы можете создать новый индекс для этой таблицы, хорошим выбором для этого запроса будет индекс UpdateTimeтолько для одного:

CREATE INDEX IX__BigTable_UpdateTime 
ON dbo.BigTable (UpdateTime);

Этот индекс позволит механизму выполнения найти самое высокое UpdateTimeпри одиночном поиске до конца b-дерева индекса:

Новый индексный план

Этот план потребляет всего несколько логических операций ввода-вывода (для навигации по уровням B-дерева) и завершается немедленно. Обратите внимание, что индексное сканирование в плане не является полным сканированием нового индекса - оно просто возвращает одну строку из одного «конца» индекса.

Если вы не хотите создавать новый полный индекс для таблицы, вы можете рассмотреть индексированное представление, содержащее только уникальные UpdateTimeзначения:

CREATE VIEW dbo.BigTableUpdateTimes
WITH SCHEMABINDING AS
SELECT 
    UpdateTime, 
    NumRows = COUNT_BIG(*)
FROM dbo.BigTable AS BT
GROUP BY
    UpdateTime;
GO
CREATE UNIQUE CLUSTERED INDEX cuq
ON dbo.BigTableUpdateTimes (UpdateTime);

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

SELECT MAX(BTUT.UpdateTime)
FROM dbo.BigTableUpdateTimes AS BTUT
    WITH (NOEXPAND);

Индексированный план просмотра

Пол Уайт 9
источник