SUM sobre linhas distintas com várias junções

10

Esquema :

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
);

Dados :

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)
;

Então nós temos:

  • 3 itens em CZ em 1 em PL
  • 370 ganhos em CZ e 25 em PL
  • Custo de 350 em CZ e 20 em PL
  • 11 ganhos extras em CZ e 5 ganhos extras em PL

Agora, quero obter respostas para as seguintes perguntas:

  1. Quantos itens tivemos no mês passado em todos os países?
  2. Qual foi o valor total ganho (soma dos pagamentos. montantes) em cada país?
  3. Qual foi o custo total (soma dos itens.preço) em cada país?
  4. Qual foi o total de ganhos extras (soma dos valores extras) em cada país?

Com a seguinte consulta ( 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;

Os resultados estão errados:

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

Custo e extra_ aprendido para CZ são inválidos - 450 em vez de 350 e 16 em vez de 11. Custo e ganhos para PL também são inválidos - eles são duplicados.

Entendo que, no caso de LEFT OUTER JOIN, haverá 2 linhas para o item com items.id = 1 (e assim por diante para outras correspondências), mas não sei como criar uma consulta adequada.

Perguntas :

  1. Como evitar resultados errados na agregação de consultas em várias tabelas?
  2. Qual é a melhor maneira de calcular a soma sobre valores distintos (items.id nesse caso)?

Versão do PostgreSQL : 9.6.1

Stranger6667
fonte
Consulte a opção 3 na minha resposta aqui: dba.stackexchange.com/questions/17012/help-with-this-query/… Você também pode fazer a opção 4 reescrevendo OUTER APPLYe usando LATERALjunções.
precisa saber é o seguinte
A opção 3 funcionará, mas, nesse caso, exigirá Seq Scanpagamentos, o que significa que a estatística será recalculada em todos os itens. Eu não mencionei isso na pergunta, mas quero filtrar itens também na hora da criação, portanto, precisarei apenas de um subconjunto específico dos dados agregados. Atualizo a pergunta
Stranger6667
Você pode adicionar WHEREcláusulas ou junções nas subconsultas. Mas verifique também a opção 4 usando LATERAL.
precisa saber é o seguinte
Você pretende Cadastre paymentse itemsna subconsulta e adicionar WHERE a ele? Eu vou ter de referência todas as opções :)
Stranger6667
Se você deseja restringir o subconjunto com base em items.created_at, sim.
precisa saber é o seguinte

Respostas:

9

Como pode haver vários paymentse múltiplos extraspor item, você executa uma "junção cruzada de proxy" entre essas duas tabelas. Agregue linhas por item_id antes de ingressar iteme tudo deve estar correto:

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;

Considere o exemplo "mercado de peixe":

Para ser mais preciso, SUM(i.price)seria incorreto depois de ingressar em uma única tabela n, que multiplica cada preço pelo número de linhas relacionadas. Fazer isso duas vezes só piora - e também é potencialmente caro em termos de computação.

Ah, e como não multiplicamos linhas itemsagora, podemos usar o mais barato em count(*)vez de count(DISTINCT i.id). ( idsendo NOT NULL PRIMARY KEY.)

SQL Fiddle.

Mas se eu quiser filtrar por items.created?

Endereçando seu comentário.

Depende. Podemos aplicar o mesmo filtro a payments.createde extras.created?

Se sim, adicione também os filtros nas subconsultas. (Não parece provável neste caso.)

Se não, mas ainda estamos selecionando a maioria dos itens , a consulta acima ainda seria mais eficiente. Algumas agregações nas subconsultas são eliminadas nas junções, mas isso ainda é mais barato que as consultas mais complexas.

Se não, e estamos selecionando uma pequena fração de itens, sugiro subconsultas ou LATERALjunções correlatas . Exemplos:

Erwin Brandstetter
fonte
Obrigado pela resposta! Mas se eu quiser filtrar por items.createdqual é a maneira mais eficiente de fazer isso? Devo acrescentar mais JOINsobre itemsa subconsultas ( pe eno seu exemplo) para executar tais filtração como @ ypercubeᵀᴹ mencionado?
Stranger6667
@ Stranger6667: Depende. E é uma pergunta diferente, realmente. Eu adicionei uma resposta acima.
Erwin Brandstetter
LATERAL JOINfunciona para mim! Obrigado pela explicação limpo :)
Stranger6667