Сумма по отдельным строкам с несколькими объединениями

10

Схема :

CREATE TABLE "items" (
  "id"            SERIAL                   NOT NULL PRIMARY KEY,
  "country"       VARCHAR(2)               NOT NULL,
  "created"       TIMESTAMP WITH TIME ZONE NOT NULL,
  "price"         NUMERIC(11, 2)           NOT NULL
);
CREATE TABLE "payments" (
  "id"      SERIAL                   NOT NULL PRIMARY KEY,
  "created" TIMESTAMP WITH TIME ZONE NOT NULL,
  "amount"  NUMERIC(11, 2)           NOT NULL,
  "item_id" INTEGER                  NULL
);
CREATE TABLE "extras" (
  "id"      SERIAL                   NOT NULL PRIMARY KEY,
  "created" TIMESTAMP WITH TIME ZONE NOT NULL,
  "amount"  NUMERIC(11, 2)           NOT NULL,
  "item_id" INTEGER                  NULL
);

Данные :

INSERT INTO items VALUES
  (1, 'CZ', '2016-11-01', 100),
  (2, 'CZ', '2016-11-02', 100),
  (3, 'PL', '2016-11-03', 20),
  (4, 'CZ', '2016-11-04', 150)
;
INSERT INTO payments VALUES
  (1, '2016-11-01', 60, 1),
  (2, '2016-11-01', 60, 1),
  (3, '2016-11-02', 100, 2),
  (4, '2016-11-03', 25, 3),
  (5, '2016-11-04', 150, 4)
;
INSERT INTO extras VALUES
  (1, '2016-11-01', 5, 1),
  (2, '2016-11-02', 1, 2),
  (3, '2016-11-03', 2, 3),
  (4, '2016-11-03', 3, 3),
  (5, '2016-11-04', 5, 4)
;

Итак, имеем:

  • 3 предмета в CZ в 1 в PL
  • 370 заработано в CZ и 25 в PL
  • 350 стоит в CZ и 20 в PL
  • 11 дополнительных заработанных в CZ и 5 дополнительных заработанных в PL

Теперь я хочу получить ответы на следующие вопросы:

  1. Сколько предметов у нас было в прошлом месяце в каждой стране?
  2. Какова была общая заработанная сумма (сумма платежей. Сумм) в каждой стране?
  3. Какова была общая стоимость (сумма items.price) в каждой стране?
  4. Каков был общий дополнительный заработок (сумма extras.amount) в каждой стране?

С помощью следующего запроса ( SQLFiddle ):

SELECT
  country                  AS "group_by",
  COUNT(DISTINCT items.id) AS "item_count",
  SUM(items.price)         AS "cost",
  SUM(payments.amount)     AS "earned",
  SUM(extras.amount)       AS "extra_earned"
FROM items
  LEFT OUTER JOIN payments ON (items.id = payments.item_id)
  LEFT OUTER JOIN extras ON (items.id = extras.item_id)
GROUP BY 1;

Результаты неверны:

 group_by | item_count |  cost  | earned | extra_earned
----------+------------+--------+--------+--------------
 CZ       |          3 | 450.00 | 370.00 |        16.00
 PL       |          1 |  40.00 |  50.00 |         5.00

Стоимость и extra_earned для CZ недействительны - 450 вместо 350 и 16 вместо 11. Стоимость и заработанные за PL также недействительны - они удваиваются.

Я понимаю, что в случае LEFT OUTER JOINбудет 2 строки для элемента с items.id = 1 (и так далее для других совпадений), но я не знаю, как построить правильный запрос.

Вопросы :

  1. Как избежать ошибочных результатов при агрегировании в запросах к нескольким таблицам?
  2. Каков наилучший способ расчета суммы по отдельным значениям (в этом случае items.id)?

Версия PostgreSQL : 9.6.1

Stranger6667
источник
Смотрите вариант 3 в моем ответе здесь: dba.stackexchange.com/questions/17012/help-with-this-query/… Вы также можете сделать вариант 4, переписав OUTER APPLYи используя LATERALсоединения вместо этого.
ypercubeᵀᴹ
Вариант 3 будет работать, но в этом случае он потребует Seq Scanплатежей, что означает, что статистика будет пересчитана по всем статьям. Я не упомянул об этом в этом вопросе, но я хочу также фильтровать элементы по времени создания, поэтому мне понадобится только конкретное подмножество агрегированных данных. Я обновлю вопрос
Stranger6667
Вы можете добавлять WHEREпредложения или объединения в подзапросах. Но проверить вариант 4 тоже используя LATERAL.
ypercubeᵀᴹ
Вы имеете в виду ПРИСОЕДИНИТЬСЯ paymentsи itemsв подзапросе и добавить WHERE к нему? Мне нужно будет сравнить все варианты :)
Stranger6667
Если вы хотите ограничить подмножество на основе items.created_at, да.
ypercubeᵀᴹ

Ответы:

9

Так как может быть несколько paymentsи несколько для extrasкаждого item, вы сталкиваетесь с «перекрестным соединением прокси» между этими двумя таблицами. Агрегируйте строки за item_id до присоединения, itemи все должно быть правильно:

SELECT i.country         AS group_by
     , COUNT(*)          AS item_count
     , SUM(i.price)      AS cost
     , SUM(p.sum_amount) AS earned
     , SUM(e.sum_amount) AS extra_earned
FROM  items i
LEFT  JOIN (
   SELECT item_id, SUM(amount) AS sum_amount
   FROM   payments
   GROUP  BY 1
   ) p ON p.item_id = i.id
LEFT  JOIN (
   SELECT item_id, SUM(amount) AS sum_amount
   FROM   extras
   GROUP  BY 1
   ) e ON e.item_id = i.id
GROUP BY 1;

Рассмотрим пример "рыбного рынка":

Чтобы быть точным, SUM(i.price)будет неправильно после объединения в одну n-таблицу, которая умножает каждую цену на количество связанных строк. Если сделать это дважды, это только усугубит ситуацию, а также может привести к большим вычислительным затратам.

Да, и так как мы не умножаем строки itemsсейчас, мы можем просто использовать более дешевый count(*)вместо count(DISTINCT i.id). ( idсущество NOT NULL PRIMARY KEY.)

SQL Fiddle.

Но если я хочу отфильтровать items.created?

Обращаясь к вашему комментарию.

Это зависит. Можем ли мы применить тот же фильтр к payments.createdи extras.created?

Если да, то просто добавьте фильтры в подзапросах. (В данном случае не похоже.)

Если нет, но мы по-прежнему выбираем большинство элементов , приведенный выше запрос будет наиболее эффективным. Некоторые объединения в подзапросах исключаются в объединениях, но это все же дешевле, чем более сложные запросы.

Если нет, и мы выбираем небольшую часть элементов, я предлагаю соотнесенные подзапросы или LATERALобъединения. Примеры:

Эрвин Брандштеттер
источник
Спасибо за ответ! Но если я хочу отфильтровать, items.createdкакой самый эффективный способ сделать это? Должен ли я добавить дополнительный JOINна itemsдля подзапросов ( pи eв вашем примере) , чтобы выполнить такую фильтрацию , как @ ypercubeᵀᴹ упоминается?
Stranger6667
@ Stranger6667: Это зависит. И это действительно другой вопрос. Я добавил ответ выше.
Эрвин Брандштеттер
LATERAL JOINработает для меня! Спасибо за чистое объяснение :)
Stranger6667