Самый быстрый способ узнать, какие часы имеют 0 строк в таблице с данными за месяц?

У меня есть очень большая/медленная таблица (данные 35 ГБ, индексы 70 ГБ) в Azure SQL MI, примерно такая:

create table a (id int identity, mydate datetime, fielda varchar(10), fieldb money, fieldc char(50), fieldd datetime)
create index ncidx__a__mydate on a (mydate)

Я хочу получить список часов, в которых есть 0 строк, хотя в часах есть строки достаточно близко (прикоснитесь к таблице чисел, и вы получите список). Я пытался придумать необходимый минимум.

Я мог бы сделать что-то подобное, но мне понадобится 30*24 штуки на каждый месяц. Похоже, что-то может сделать функция с перекрестным применением.

SELECT * FROM (SELECT '20240703 1am test' res) a WHERE EXISTS (SELECT * FROM dbo.mytable WHERE mydate >='20240701 01:00:00' AND mydate <'20240701 02:00:00')

Я придумал это (на основе запроса Ицика Бен-Гана), которое... работает и на данный момент меня достаточно хорошо. (3 минуты на месяц, 200 млн строк, 34 ГБ данных, 70 ГБ индексов, включая тот, который вы видите. Он выполняет 715992 логических чтения).

SELECT orderday, orderhour, MIN(mydate) AS mindate
FROM mytable
CROSS APPLY (VALUES(day(mydate), datepart(HOUR,mydate)))
               AS D(orderday, orderhour)
WHERE mydate >='20240501' AND mydate <'20240601'
GROUP BY orderday, orderhour

Но есть ли более быстрый способ? Мне не нужна минимальная дата, мне просто нужно знать, есть ли там ЛЮБЫЕ строки. Кажется, что это могло бы произойти почти мгновенно, но логика этого мне непонятна. Спасибо.

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

Thom A 13.08.2024 23:27

Надеюсь, вы имеете в виду 35 ТБ данных, поскольку агрегирование 35 ГБ не займет много времени.

siggemannen 13.08.2024 23:32

Для вопросов, связанных с производительностью, необходимы 1) определения таблиц, 2) определения индексов и 3) план выполнения с использованием «Вставить план».

Dale K 14.08.2024 00:31

Какую версию SQL Server вы используете? SQL Server 2022 имеет несколько полезных функций, таких как GENERATE_SERIES() и DATETRUNC(), которые могут оказаться полезными в решении.

T N 14.08.2024 01:40

@ThomA это более простая версия. У реального индекса есть еще пара, и он используется для некоторых очень конкретных вещей, но мне хотелось, чтобы он был простым.

mbourgon 14.08.2024 16:30

@siggemannen да, именно этого я и ожидал: «облако» и все такое. Но даже с довольно узким индексом (всего 7 ГБ) возвращение с группировкой все равно занимало более 10 минут... вот почему я погрузился в кроличью нору существования, минут и тому подобного. И теперь, когда это у меня есть, я буду использовать его для других целей. То, что было «ага, интересно, а?», вероятно, с годами будет массово использоваться повторно. Ваше здоровье! :)

mbourgon 14.08.2024 16:34
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
6
57
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

С индексом mydate вы на правильном пути. Большинство решений, скорее всего, будут включать в себя либо соединение цикла и поиск по индексу (зондирование) в этом индексе, либо сканирование диапазона соответствующей части индекса (лучше, чем сканирование таблицы), которое передается логике, которая ищет пробелы.

Самым очевидным и, возможно, наиболее эффективным решением было бы генерировать диапазон значений данных/времени, который затем фильтруется с помощью предложения where not exists(...). Что-то вроде:

declare @FromDate datetime = '2024-07-01'
declare @ToDate datetime = dateadd(month, 1, @FromDate) -- Exclusive
declare @DiffHours int = datediff(hour, @FromDate, @ToDate)

-- Using not exists
-- Loop join and index seek (probe)
select h.Hour
from generate_series(0, @DiffHours - 1) s
cross apply (select dateadd(hour, s.value, @FromDate) as Hour) h
where not exists (
    select *
    from a
    where a.mydate >= h.Hour
    and a.mydate < dateadd(hour, 1, h.Hour)
)
order by h.Hour

В течение одного месяца индекс будет проверяться по пункту exists примерно 750 раз.

Другой подход — сканировать диапазон дат, сводить его к набору отдельных значений часов, а затем вычитать эти значения из сгенерированного списка всех часов. Операцию вычитания можно выполнить с помощью except или конструкции left join ... where right-side is null.

declare @FromDate datetime = '2024-07-01'
declare @ToDate datetime = dateadd(month, 1, @FromDate) -- Exclusive
declare @DiffHours int = datediff(hour, @FromDate, @ToDate)

-- Using except
-- Index seek (range scan) and merge join (or spool + loop join)
select h.Hour
from generate_series(0, @DiffHours - 1) s
cross apply (select dateadd(hour, s.value, @FromDate) as Hour) h
except
select distinct datetrunc(hour, a.mydate) as Hour
from a
where a.mydate >= @FromDate
and a.mydate < @ToDate
order by Hour

-- Using left join
-- Index seek (range scan) and merge join (or spool + loop join)
-- (Slighly different from the "except" plan)
select h.Hour
from generate_series(0, @DiffHours - 1) s
cross apply (select dateadd(hour, s.value, @FromDate) as Hour) h
left join (
    select distinct datetrunc(hour, a.mydate) as Hour
    from a
    where a.mydate >= @FromDate
    and a.mydate < @ToDate
) h2
    on h2.Hour = h.Hour
where h2.Hour is null
order by H.Hour

Наконец, можно выполнить упорядоченное сканирование диапазона дат, проверяя наличие пробелов с помощью оконной функции LAG() для сравнения текущего значения с предыдущим. Знак UNION ALL включен, чтобы гарантировать, что мы охватываем конечные случаи (первый и последний час).

Может быть небольшой выигрыш в производительности, если операции UNION переходят на следующий внешний уровень запроса, поскольку данные уже отсортированы, но цена — это еще два поиска по индексу. Обе версии следуют далее.

declare @FromDate datetime = '2024-07-01'
declare @ToDate datetime = dateadd(month, 1, @FromDate) -- Exclusive

-- Index seek (range scan), using lag() to check for gaps.
-- A union-all is used to ensure we have the end-cases covered.
select
    dd.MissingHours,
    dateadd(hour, 1, dp.PriorHour) as FromHour,
    dateadd(hour, -1, Hour) as ThruHour
from (
    select
        d.Hour,
        lag(d.Hour) over(order by d.Hour) as PriorHour 
    from (
        select dateadd(hour, -1, @FromDate) as Hour
        union all 
        select @ToDate as Hour
        union all 
        select datetrunc(hour, a.mydate) as Hour
        from a
        where a.mydate >= @FromDate
        and a.mydate < @ToDate
    ) d
) dp
cross apply (select datediff(hour, dp.PriorHour, dp.Hour) - 1 as MissingHours) dd
where dd.MissingHours >= 1
order by dp.PriorHour

-- Index seek (range scan), using lag() to check for gaps.
-- Factoring out this union /might/ yield a slight performance gain,
-- since data is already sorted, but the cist is two more index seeks.
select
    dd.MissingHours,
    dateadd(hour, 1, dp.PriorHour) as FromHour,
    dateadd(hour, -1, Hour) as ThruHour
from (
    select
        d.Hour,
        lag(d.Hour) over(order by d.Hour) as PriorHour 
    from (
        select datetrunc(hour, a.mydate) as Hour
        from a
        where a.mydate >= @FromDate
        and a.mydate < @ToDate
    ) d
    union all
    select
        datetrunc(hour, min(a.mydate)) as Hour,
        dateadd(hour, -1, @FromDate) as PriorHour
    from a
    where a.mydate >= @FromDate
    and a.mydate < @ToDate
    union all
    select
        @ToDate as Hour,
        datetrunc(hour, max(a.mydate)) as PriorHour
    from a
    where a.mydate >= @FromDate
    and a.mydate < @ToDate
) dp
cross apply (select datediff(hour, dp.PriorHour, dp.Hour) - 1 as MissingHours) dd
where dd.MissingHours >= 1
order by dp.PriorHour

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

select dateadd(hour, s.value, dp.PriorHour) as Hour
from (
    ...
) dp
cross apply (select datediff(hour, dp.PriorHour, dp.Hour) - 1 as MissingHours) dd
cross apply generate_series(1, dd.MissingHours) s
where dd.MissingHours >= 1
order by Hour

См. эту db<>fiddle для демонстрации каждого из вышеперечисленных действий с планами выполнения с использованием некоторых сгенерированных тестовых данных с промежутками в 1, 3 и 5 часов.

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

Примеры результатов (отдельные часы и диапазоны часов):

Час 2024-07-05 04:00 2024-07-11 10:00 2024-07-11 11:00 2024-07-11 12:00 2024-07-11 13:00 2024-07-22 21:00 2024-07-22 22:00 2024-07-22 23:00 2024-07-23 00:00 2024-07-23 01:00 2024-07-23 02:00
Отсутствующие часы От часа через час 1 2024-07-05 04:00 2024-07-05 04:00 3 2024-07-11 10:00 2024-07-11 12:00 5 2024-07-22 21:00 2024-07-23 01:00

СТАРЫЕ ВЕРСИИ SQL-СЕРВЕРА

Вышеупомянутые решения SQL используют функции DATE_TRUNC() и/или GENERATE_SERIES(), доступные в SQL Server 2022 и более поздних версиях.

В более ранних версиях SQL Server функцию DATE_TRUNC(hour, xxx) можно заменить вычислением dateadd(hour, datediff(hour, 0, xxx), 0). Функцию GENERATE_SERIES() можно заменить генератором чисел, завернутым в CTE или cross apply. Существует множество методов, но для этого приложения вы можете использовать следующие варианты:

cross apply (
    select top (n) -- 1..n
        row_number() over(order by (select null)) as value
    from (values (0),(1),(2),(3),(4),(5),(6),(7),(8),(9)) n1(n) -- Up to 10
    cross join (values (0),(1),(2),(3),(4),(5),(6),(7),(8),(9)) n2(n) -- Up to 100
    cross join (values (0),(1),(2),(3),(4),(5),(6),(7),(8),(9)) n3(n) -- Up to 1000
) s

Функция row_number() выдаёт числа, начинающиеся с 1. Вычитание 1 и корректировка значения top (n) необходимы для диапазонов, отсчитываемых от нуля или других оснований. Добавьте больше перекрестных соединений, если требуется более 1000 значений.

См. эту db<>fiddle для демонстрации, которая работает для более ранних версий SQL Server, по крайней мере, до 2014 года.

Вау, это превосходный ответ, чувак! Спасибо за столько подробностей в этом ответе! К счастью (?) Я использую Azure SQL MI, поэтому я получаю новые навороты, но производительность в некоторых вещах просто головокружительная (и нет, это не новый CE) .. ОБНОВЛЕНИЕ: ВАУ. 9 тысяч прочтений за месяц по сравнению с 98 тысячами прочтений для версии «min(mydate)». Ты волшебник, Гарри!

mbourgon 14.08.2024 16:28

Другие вопросы по теме