Как присоединиться к ближайшей дате в Postgresql - PullRequest
0 голосов
/ 29 апреля 2019

Предположим, у меня есть следующие таблицы

product_prices

product|price|date
-------+-----+----------
apple  |10   |2014-03-01
-------+-----+----------
apple  |20   |2014-05-02
-------+-----+----------
egg    |2    |2014-03-03
-------+-----+----------
egg    |4    |2015-10-12

покупки:

user|product|date
----+-------+----------
John|apple  |2014-03-02
----+-------+----------
John|apple  |2014-06-03
----+-------+----------
John|egg    |2014-08-13
----+-------+----------
John|egg    |2016-08-13

Мне нужна таблица, подобная этой:

name|product|purchase date |price date|price
----+-------+--------------+----------+-----
John|apple  |2014-03-02    |2014-03-01|10
----+-------+--------------+----------+-----
John|apple  |2014-06-03    |2014-05-02|20
----+-------+--------------+----------+-----
John|egg    |2014-08-13    |2014-08-13|2
----+-------+--------------+----------+-----
John|egg    |2016-08-13    |2015-10-12|4

Или «какова цена товара в этот день». Где цена рассчитывается на основе даты из таблицы products. На реальных БД я пытался использовать что-то похожее на:

SELECT name, product, pu.date, pp.date, pp.price
FROM purchases AS pu
LEFT JOIN product_prices AS pp
ON pu.date = (
              SELECT date
              FROM product_prices
              ORDER BY date DESC LIMIT 1);

Но я продолжаю получать только левую часть таблицы (с (нулевым) вместо даты и цены продукта) или много строк со всеми комбинациями цен и дат.

Ответы [ 4 ]

1 голос
/ 30 апреля 2019

Я бы предложил изменить таблицу product_prices, чтобы вместо нее использовать столбец daterange (или, по крайней мере, start_date и end_date).

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

A daterange может быть эффективно проиндексирован, и с этим на месте запрос становится таким простым:

SELECT name, product, pu.date, pp.valid_during, pp.price
FROM purchases AS pu
  LEFT JOIN product_prices AS pp ON pu.date <@ pp.valid_during

(при условии, что столбец диапазона назван valid_during)


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

Новые определения таблиц могут выглядеть примерно так:

create table purchase_prices
(
   product_id    integer       not null references products,
   price         numeric(16,4) not null,
   valid_during  daterange not null
);

И ограничение, которое предотвращает перекрывающиеся диапазоны:

alter table purchase_prices
  add constraint check_price_range
  exclude using gist (product_id with =, valid_during with &&);

Ограничение требует btree_gist расширение.

Как всегда, повышение скорости запросов сопровождается ценой, и в этом случае это более высокие затраты на обслуживание индекса GiST.Вам нужно будет выполнить некоторые тесты, чтобы увидеть, перевешивает ли более простой (и, скорее всего, гораздо более быстрый) запрос медленную производительность вставки на purchase_prices.

0 голосов
/ 30 апреля 2019

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

SELECT date
FROM product_prices
ORDER BY date DESC LIMIT 1

Есть две проблемы:

  1. Он вернет 2015-10-12 для каждой строки в соединении, и, в конечном счете, в эту дату ничего не было куплено, следовательно, ноль.
  2. Ваше приближение ближе всего к тому, что даты равны. Если у вас нет строки product_prices для каждого продукта на каждую дату, вы всегда будете пропускать. «Ближайший» подразумевает расстояние и рейтинг.
WITH close_prices_by_purchase AS (
    SELECT
      p.user,
      p.product,
      p.date pp.date,
      pp.price,
      row_number() over (partition by pp.product, order by pp.date desc) as distance -- calculate distance between purchase date and price date
    FROM purchases AS p
    INNER JOIN product_prices AS pp on pp.product = p.product
    WHERE pp.date < p.date
)
SELECT user as name, product, pu.date as purchase_date, pp.date as price_date, price
FROM close_prices_by_purchase AS cpbp
WHERE distance = 1; -- shortest distance
0 голосов
/ 29 апреля 2019

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

with price_ranges as 
    (select product, 
            price, 
            date as price_date, 
            daterange(date, lead(date, 1) 
               OVER (partition by product order by date), '[)'
            ) as valid_price_range from product_prices
     )
select "user" as name, 
       purchases.product, 
       purchases.date, 
       price_date, 
       price
from purchases
join price_ranges on purchases.product = price_ranges.product
and purchases.date <@ price_ranges.valid_price_range
order by purchases.date;
0 голосов
/ 29 апреля 2019

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

with diffs as (
  select
      a.*,
      b."date" as bdate,
      b.price,
      b."date" - a."date" as diffdays,
      row_number() over (
        partition by "user", a."product", a."date"
        order by "user", a."product", a."date", b."date" - a."date" desc
      ) as sr
  from purchases a
  inner join product_prices b on a.product = b.product
  where b."date" - a."date" < 1
)
select
    "user" as "name",
    product,
    "date" as "purchase date",
    bdate as "price date",
    price
from diffs
where sr = 1

Пример: https://www.db -fiddle.com / f / dwQ9EXmp1SdpNpxyV1wc6M / 0

Объяснение

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...