Можно ли с помощью SQL подсчитать все диапазоны дат в году?

Я выбираю несколько диапазонов дат с помощью tsrange, который включает определенный год.

Я получаю такой вывод:

  { time_range: '("2024-12-27 00:00:00","2025-02-01 00:00:00")' },
  { time_range: '("2025-05-01 00:00:00","2025-05-05 00:00:00")' },
  { time_range: '("2025-05-08 00:00:00","2025-05-20 00:00:00")' }

могу ли я посчитать все дни, сгруппированные по годам?

текущий код:

SELECT time_range FROM holidays WHERE time_range && tsrange($1, $2, '[)')

Каков ваш ожидаемый результат по этому подсчету? 3 потому что у вас есть 3 time_range или 1 потому что они все связаны... ?

Luuk 16.03.2024 10:23

У тебя действительно есть верёвка вокруг тупеля?

nbk 16.03.2024 10:46

@Luuk Я хочу посчитать, сколько дней в 2024 году и сколько дней в 2025 году.

serverz 16.03.2024 10:47

Хорошо, вам нужно что-то вроде 2024 | 5, потому что в 2024 году у вас есть 17 декабря 2024 года .. 31 декабря 2024 года, а в 2025 году большее число с 01 января 2025 года .. 01 февраля 2025 года (эксклюзивное) и длина из двух других диапазонов? Однако я не понимаю, какое отношение ваш текущий запрос имеет к этому.

Richard Huxton 16.03.2024 10:56

Вы можете начать здесь: Количество дней в диапазоне дат

Luuk 16.03.2024 11:02

@RichardHuxton да, я попробовал несколько других запросов, но все не сработало, поэтому я не писал здесь

serverz 16.03.2024 11:37

@RichardHuxton «что-то вроде 2024 | 5, потому что в 2024 году у вас есть 17 декабря 2024 года .. 31 декабря 2024 года» - а, разве там не 15 дней (включительно)?

Bagus Tesa 16.03.2024 11:56

опечатка в моем комментарии - 27 декабря в вопросе

Richard Huxton 16.03.2024 12:05
ReactJs | Supabase | Добавление данных в базу данных
ReactJs | Supabase | Добавление данных в базу данных
Это и есть ваш редактор таблиц в supabase.👇
Понимание Python и переход к SQL
Понимание Python и переход к SQL
Перед нами лабораторная работа по BloodOath:
0
8
96
4
Перейти к ответу Данный вопрос помечен как решенный

Ответы 4

используя эти данные:

create table example (
  x int,
  ts tsrange);
INSERT INTO example VALUES
  (1, '[2024-12-27 00:00:00,2025-02-01 00:00:00)'),
  (2, '[2025-05-01 00:00:00,2025-05-05 00:00:00)'),
  (3, '[2025-05-08 00:00:00,2025-05-20 00:00:00)');

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

WITH RECURSIVE days as (
  SELECT 
    x, 
    LOWER(ts) as t 
  FROM example 
  
  UNION ALL
  
  SELECT 
    x, 
    t+'1 day' 
  FROM days 
  where t < (SELECT UPPER(ts) FROM example where x=days.x)
  )
SELECT 
  extract(year from t), count(*)
FROM days
GROUP BY extract(year from t)
ORDER BY extract(year from t)
;

см.: DBFIDDLE

выход:

год считать 2024 год 5 2025 год 50

или, если вы хотите получить их для каждого диапазона дат: DBFIDDLE

выход:

Икс извлекать считать 1 2024 год 5 1 2025 год 32 2 2025 год 5 3 2025 год 13
Ответ принят как подходящий

Кажется, вам нужен не tsrange, а скорее диапазон дат. В противном случае я сохранил ваши образцы данных. Ряд положений CTE предназначен только для облегчения чтения — возможно, вы захотите объединить некоторые шаги.

CREATE TEMP TABLE src (
    id            int PRIMARY KEY
    , event_range daterange NOT NULL
);

INSERT INTO src VALUES
    ( 1, '(2024-12-27,2025-02-01)' )
  , ( 2, '(2025-05-01,2025-05-05)' )
  , ( 3, '(2025-05-08,2025-05-20)' )
;

WITH target_years (y) AS (
    VALUES (2024), (2025), (2026)
)
, year_ranges (y, y_r) AS (
    SELECT y, daterange( make_date(y, 1, 1), make_date(y+1, 1, 1), '[)' )
    FROM target_years
)
, overlapping_ranges (y, o_r) AS (
    SELECT
        year_ranges.y
      , year_ranges.y_r * src.event_range AS o_r
    FROM
        year_ranges
        JOIN src    ON year_ranges.y_r && src.event_range
)
, range_day_counts AS (
    SELECT
        y
        , upper(o_r) - lower(o_r) AS days
    FROM
        overlapping_ranges
)
SELECT y, sum(days) AS tot_days
FROM range_day_counts
GROUP BY y
ORDER BY y
;

дает

  y   | tot_days 
------+----------
 2024 |        4
 2025 |       45
(2 rows)

Как я могу фильтровать по user_id? Итак, где user_id = 4, я получаю, что столбец user_id не существует.

serverz 16.03.2024 14:37

Вам понадобится столбец с названием user_id, чтобы фильтровать по нему. В вопросе нет такой графы.

Richard Huxton 16.03.2024 15:09

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

Richard Huxton 16.03.2024 15:13

У вас это получилось очень хорошо, и спасибо, что не допускаете дублирования, вы лучшие! Я сделал это в другом запросе для проверки перекрытия. Мне нужно изучить больше sql, но в случае с user_id я пытался добавить user_id, но получил ошибку: ссылка на столбец "o_r" неоднозначна, я пробовал каждый cte или другие, но каждый раз получал ошибку, когда хотел фильтровать по user_id

serverz 16.03.2024 15:18

Предполагая, что user_id находится в вашей исходной таблице, добавьте предложение WHERE в overlapping_ranges CTE. В какой-то момент вам придется выучить немного больше SQL, если вы боретесь с базовыми проблемами, такими как построение предложенияwhere.

Richard Huxton 16.03.2024 17:13

См. пример.
Вопрос о том, включать или нет граничные даты в расчеты, остается открытым. Например ('2024-03-14', '2024-03-15') =0? =1? или =2?

Образец данных

create table holidays (id int, date_range tsrange);
insert into holidays values
 (1,'("2024-12-27 00:00:00","2025-02-01 00:00:00")' )
,(2,'("2025-05-01 00:00:00","2025-05-05 00:00:00")' )
,(3,'("2025-05-08 00:00:00","2025-05-20 00:00:00")' )
;

Запрос

with cte1 as(
  select *
    ,case when date_part('year',upper(date_range))<>date_part('year',lower(date_range)) 
         then 1
     else 0
     end overyear
  from holidays
)
,cte2 as(
select * 
  ,lower(date_range) l
  ,upper(date_range) u
  ,date_part('day',upper(date_range)- lower(date_range)) d
  ,case when n=0 then date_part('year',lower(date_range))
   else date_part('year',upper(date_range))
   end yy
  ,case when overyear>0 and n=0 then
           date_part('day',date_trunc('year',upper(date_range))-lower(date_range))
        when overyear>0 and n=1 then 
           date_part('day',upper(date_range)-date_trunc('year',upper(date_range)))
   else date_part('day',upper(date_range)- lower(date_range))
   end lng
from cte1
left join (values (0),(1))t(n)
  on n<=overyear
)
--select * from cte2;
select yy,sum(lng)lng
from cte2
group by yy
order by yy
;

Выход

ууу СПГ 2024 год 5 2025 год 47

Результат подзапроса

идентификатор диапазон дат годовой н л ты д ууу СПГ 1 ("2024-12-27 00:00:00","2025-02-01 00:00:00") 1 0 2024-12-27 00:00:00 2025-02-01 00:00:00 36 2024 год 5 1 ("2024-12-27 00:00:00","2025-02-01 00:00:00") 1 1 2024-12-27 00:00:00 2025-02-01 00:00:00 36 2025 год 31 2 («01.05.2025 00:00:00», «05.05.2025 00:00:00») 0 0 2025-05-01 00:00:00 2025-05-05 00:00:00 4 2025 год 4 3 («2025-05-08 00:00:00», «2025-05-20 00:00:00») 0 0 2025-05-08 00:00:00 2025-05-20 00:00:00 12 2025 год 12

Нет сомнений в том, являются ли диапазоны дат включенными или исключительными. По умолчанию PostgreSQL использует значение (включительно, эксклюзивно), но позволяет вам указать именно то, что вы хотите. postgresql.org/docs/current/…

Richard Huxton 16.03.2024 15:11

Да, в примере показано исключение границ. Однако на праздники лучше было бы включить бордюры :)

ValNik 16.03.2024 15:12
  1. Сгенерируйте даты в каждом диапазоне с помощью generate_series()
  2. Не игнорируйте связанную инклюзивность
  3. Считайте только отдельные даты, поскольку диапазоны могут перекрываться.
  4. Группировать по годам

Демо на db<>fiddle:

SELECT date_part('year',holidate) "year", count(distinct holidate)
FROM holidays CROSS JOIN
generate_series( +(not lower_inc(time_range))::int+lower(time_range)::date
                ,-(not upper_inc(time_range))::int+upper(time_range)::date
                ,'1 day')as a(holidate)
WHERE holidate <@ tstzrange('2024-12-27', '2025-05-20', '[)')
GROUP BY "year";
год считать 2024 год 4 2025 год 45

Если вы хотите предотвратить перекрытие диапазонов, добавьте ограничение исключения :

alter table holidays add constraint non_overlapping_ranges 
  exclude using gist(time_range with &&);

Подход, использующий только границы диапазона, будет работать лучше, но для получения достоверных результатов вам необходимо range_agg() затем unnest() объединить перекрывающиеся диапазоны, затем разделить их 1 января так, чтобы они полностью содержались в данном году, и только затем получить sum(upper()-lower()).

Zegarek 16.03.2024 15:07

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

serverz 16.03.2024 15:24

@serverz Если вы решите добавить ограничение, вам не нужно его добавлять: оно будет добавлено автоматически, чтобы обеспечить соблюдение ограничения. В противном случае добавьте create index on holidays using gist (time_range);, но имейте в виду, что если вы разрешите перекрытие в этих диапазонах, этот ответ будет единственным, который даст вам действительные результаты.

Zegarek 16.03.2024 15:29

Я создал индекс с exlucude, используя gist (user_id With =, time_range = &&) или что-то в этом роде. Прежде чем выполнить запрос, я также проверяю, перекрываются ли даты, отправленные пользователем, иначе я не запускаю ваш запрос. Поэтому я проверяю перекрытие, но в другом запросе

serverz 16.03.2024 15:33

@serverz Звучит хорошо. Если наложения предотвращаются ограничением, а также останавливаются отдельной проверкой, то upper()-lower() (без фактического создания дат только для их подсчета) будет быстрее.

Zegarek 16.03.2024 15:41

Хорошо, у вас есть пример, как я могу использовать только верхний/нижний?

serverz 16.03.2024 15:53

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