Оконная функция PostgreSQL для суммы за интервал

Таблица представлена ​​следующими скриптами:

CREATE TABLE sales (
  id SERIAL PRIMARY KEY,
  product_id INTEGER,
  sales_date DATE,
  quantity INTEGER,
  price NUMERIC
);

INSERT INTO sales (product_id, sales_date, quantity, price) VALUES
   (1, '2023-01-01', 10, 10.00),
   (1, '2023-01-02', 12, 12.00),
   (1, '2023-01-03', 15, 15.00),
   (2, '2023-01-01', 8, 8.00),
   (2, '2023-01-02', 10, 10.00),
   (2, '2023-01-03', 12, 12.00);

Задача — просуммировать объем продаж за последние 3 дня по каждому product_id. Период должен отсчитываться в обратном направлении от максимальной (последней) даты каждого Product_id. Таким образом, для 1 максимум — 03.01.2023, то же самое для 2. Но для product_id 2 последний день может отличаться от 1 — скажем, 05.01.2023.

Применяя этот запрос с оконной функцией в подзапросе:

select product_id, max(increasing_sum) as quantity_last_3_days
   from 
        (SELECT product_id,
         SUM(quantity) OVER (PARTITION BY product_id ORDER BY sales_date RANGE BETWEEN INTERVAL '2 days'
                PRECEDING AND CURRENT ROW) AS increasing_sum
         FROM sales) as s
   group by product_id;

Я получаю ожидаемый результат:

  | product_id | quantity_last_3_days |
  |____________|______________________|            
  |_____1______|___________37_________|
  |_____2______|___________30_________|     
 

Но является ли это решение оптимальным? Есть ли способ решить проблему, используя оконную функцию без подзапроса?

Показанный вами запрос не соответствует описанию того, что вы хотите. Он показывает самое высокое трехдневное окно, а не самое последнее трехдневное окно. Это одно и то же, учитывая ваши демонстрационные данные, но в целом это не одно и то же.

jjanes 05.05.2024 01:27
ReactJs | Supabase | Добавление данных в базу данных
ReactJs | Supabase | Добавление данных в базу данных
Это и есть ваш редактор таблиц в supabase.👇
Понимание Python и переход к SQL
Понимание Python и переход к SQL
Перед нами лабораторная работа по BloodOath:
1
1
95
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

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

WITH latest_sales AS (
  SELECT product_id, max(sales_date) AS time 
  FROM sales 
  GROUP BY product_id
)
SELECT sales.product_id, sum(sales.quantity) as quantity_last_3_days
FROM sales INNER JOIN latest_sales ON 
    sales.product_id = latest_sales.product_id 
    AND sales.sales_date >= latest_sales.time - interval '2 days'
GROUP BY sales.product_id;

Я попробовал ваше решение, и оно ничего не возвращает. Может быть, дело в пункте Where?

Jelly 04.05.2024 19:44

Ну а если вы используете предоставленные вами примерные данные, то, насколько я понимаю, результатов быть не должно (все продажи были совершены в 2023 году, поэтому за последние 3 дня продаж не было). Если вы обновите данные, вы должны получить правильные результаты. Или, возможно, я неправильно понял предполагаемый результат?

grundb 04.05.2024 19:50

Период должен отсчитываться в обратном направлении от максимальной (последней) даты каждого Product_id. Таким образом, для 1 максимум — 03.01.2023, то же самое для 2. Но для product_id 2 последний день может отличаться от 1 — скажем, 05.01.2023. Я обновил свой вопрос, чтобы сделать его более понятным

Jelly 04.05.2024 20:17

В этом есть смысл. Я обновлю ответ!

grundb 04.05.2024 21:09

Я изменил свой ответ на пример, в котором не используется оконный запрос.

grundb 04.05.2024 21:19
Ответ принят как подходящий

Но является ли это решение оптимальным?

Нет, если вам просто нужно «суммировать продажи quantity за последние 3 дня по каждому product_id».

Есть ли способ решить проблему, используя оконную функцию без подзапроса?

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

Вместо этого для каждой строки просматриваются строки с тем же product_id за 2 дня до этого. Позже вы выберете трехдневный период с наибольшей суммой quantity, который не обязательно будет тремя самыми последними датами.

На 400 тысячах выборок ваш запрос принимает целое 1.0s без индекса, 0.7s с индексом покрытия , и вы можете перейти от этого к 0.4s без индекса покрытия или 0.1s с индексом покрытия. Вам просто нужно спросить только сумму за 3 последних даты для каждой product_id: демо на db<>fiddle

select product_id,sum(quantity) as quantity_last_3_days
from(select *,row_number()over(PARTITION BY product_id ORDER BY sales_date DESC)
     from sales)_
where 3>=row_number
group by product_id;

Хитрость здесь в том, что оконная функция будет выполняться с Run Condition: (3 >= row_number() OVER (?)), что означает, что она просто возьмет 3 самых последних и завершит работу. Он даже может вывести их прямо из верхней части индекса покрытия, даже не посещая таблицу.
Ваш исходный запрос должен сканировать все целиком (либо всю таблицу, либо весь индекс, если он доступен), а затем отсортировать его, чтобы получить max().


Как вы отметили, при этом учитываются любые последние 3 даты для каждого product_id, которые не обязательно должны быть последовательными, а самая последняя не обязательно должна быть сегодня или вчера.
Не последние 3 дня вообще, если считать от текущей даты.
Не последние 3 даты подряд для каждого product_id.

Если вам это нужно, последние три из сегодняшних реализуются тривиально:

select product_id,sum(quantity) as quantity_last_3_days
from sales
where sales_date>='2023-01-03'::date--this would be current_date or now()
                  -'2d'::interval
group by product_id;

и 3 последних последовательных product_id одинаковы

with cte as (
  select distinct on (product_id) *
  from sales s1
  order by product_id,sales_date desc)
select *,(select sum(quantity)
          from sales s2
          where s1.product_id=s2.product_id
          and s1.sales_date-'2 days'::interval >= s2.sales_date) 
          as quantity_last_3_days
from cte s1;

Еще один момент. Моя выборка представляет ситуацию, когда каждая распродажа происходит каждую дату и происходит строго раз в день. Но если, например, иметь последовательность дат: '2023-01-07', '2023-01-05', '2023-01-01' и применить предложение 'where 3>=row_number', мы получим Записи '2023-01-07', '2023-01-05', '2023-01-01', хотя ожидается '2023-01-07', '2023-01-05', согласно основному условию" последние 3 дня"

Jelly 06.05.2024 18:54

@Jelly Это совершенно верно, и я предположил, что это было твое намерение. Это то, что я понял из ваших «последних 3 дней для каждого product_id». Я подумал, что если вам нужна последняя дата для каждой из этих дат плюс два предшествующих дня подряд, вы бы сказали «последние 3 дня подряд», или если вам нужны последние 3 дня с текущей даты, независимо от того, что такое данное product_id. фактически самая последняя запись sales_date, вы бы это тоже отметили. Обе эти альтернативные интерпретации логики гораздо проще реализовать.

Zegarek 06.05.2024 19:02

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

Jelly 06.05.2024 19:35

Мой ответ. В частности, он изучает Зегарека и ответы grundb:

select product_id, quantity_last_3_days
  FROM
     (SELECT product_id,
        CASE 
          WHEN
             ROW_NUMBER() OVER (PARTITION BY product_id ORDER BY sales_date DESC) < 2
          THEN
             SUM(quantity) OVER (PARTITION BY product_id ORDER BY sales_date RANGE BETWEEN INTERVAL '2 days'
            PRECEDING AND CURRENT ROW) 
          ELSE
             NULL
        END quantity_last_3_days
       FROM sales) as s
 WHERE  quantity_last_3_days IS NOT NULL;

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

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