SUMA sobre filas distintas con múltiples combinaciones


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

Datos :

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

Entonces tenemos:

  • 3 artículos en CZ en 1 en PL
  • 370 ganados en CZ y 25 en PL
  • 350 costo en CZ y 20 en PL
  • 11 extra ganados en CZ y 5 extra ganados en PL

Ahora quiero obtener respuestas a las siguientes preguntas:

  1. ¿Cuántos artículos tuvimos el mes pasado en cada país?
  2. ¿Cuál fue el monto total ganado (suma de pagos. Montos) en cada país?
  3. ¿Cuál fue el costo total (suma de artículos.precio) en cada país?
  4. ¿Cuál fue el total de ganancias adicionales (suma de cantidades adicionales) en cada país?

Con la siguiente 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;

Los resultados son incorrectos:

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

El costo y extra_earned para CZ no son válidos: 450 en lugar de 350 y 16 en lugar de 11. El costo y el ganado para PL también no son válidos: se duplican.

Entiendo que en caso de LEFT OUTER JOINque haya 2 filas para el elemento con items.id = 1 (y así sucesivamente para otras coincidencias), pero no sé cómo construir una consulta adecuada.

Preguntas :

  1. ¿Cómo evitar resultados incorrectos en la agregación de consultas en varias tablas?
  2. ¿Cuál es la mejor manera de calcular la suma sobre valores distintos (items.id en ese caso)?

Versión PostgreSQL : 9.6.1


Vea la opción 3 en mi respuesta aquí: dba.stackexchange.com/questions/17012/help-with-this-query/… También puede hacer la opción 4 reescribiendo OUTER APPLYy utilizando LATERALcombinaciones en su lugar.
ypercubeᵀᴹ

La opción 3 funcionará, pero en ese caso requerirá Seq Scanpagos, lo que significa que la estadística se volverá a calcular en todos los artículos. No mencioné esto en la pregunta, pero también quiero filtrar los elementos por tiempo de creación, por lo que solo necesitaré un subconjunto específico de los datos agregados. Actualizaré la pregunta
Stranger6667,

Puede agregar WHEREcláusulas o uniones en las subconsultas. Pero marque la opción 4, también, usando LATERAL.
ypercubeᵀᴹ

¿Te refieres a UNIRSE paymentsy itemsen subconsulta y agregarle WHERE ? Tendré que comparar todas las opciones :)
Stranger6667

Si desea restringir el subconjunto basado en items.created_at, sí.
ypercubeᵀᴹ

Respuestas:


9

Como puede haber múltiples paymentsy múltiples extraspor item, se encuentra con una "unión cruzada de proxy" entre esas dos tablas. Agregue filas por item_id antes de unirse itemy todo debería ser correcto:

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 el ejemplo del "mercado de pescado":

Para ser precisos, SUM(i.price)sería incorrecto después de unirse a una sola n-tabla, que multiplica cada precio por el número de filas relacionadas. Hacerlo dos veces solo lo empeora, y también es potencialmente costoso computacionalmente.

Ah, y dado que no multiplicamos las filas itemsahora, podemos usar el más barato en count(*)lugar de count(DISTINCT i.id). ( idser NOT NULL PRIMARY KEY)

SQL Fiddle.

Pero si quiero filtrar por items.created?

Dirigiendo tu comentario.

Depende. ¿Podemos aplicar el mismo filtro a payments.createdyextras.created ?

En caso afirmativo, simplemente agregue los filtros en las subconsultas también. (No parece probable en este caso).

Si no, pero todavía estamos seleccionando la mayoría de los elementos , la consulta anterior aún sería más eficiente. Algunas de las agregaciones en las subconsultas se eliminan en las uniones, pero eso sigue siendo más barato que las consultas más complejas.

Si no, y estamos seleccionando una pequeña fracción de elementos, sugiero subconsultas o LATERALuniones correlacionadas . Ejemplos:


¡Gracias por la respuesta! Pero si quiero filtrar, items.created¿cuál es la forma más eficiente de hacer esto? Debo añadir el suplemento JOINde itemsa subconsultas ( py een su ejemplo) para efectuar dicho filtración como @ ypercubeᵀᴹ mencionado?
Stranger6667

@ Stranger6667: Depende. Y es una pregunta diferente, de verdad. Agregué una respuesta arriba.
Erwin Brandstetter

LATERAL JOIN¡funciona para mi! Gracias por la explicación limpia :)
Stranger6667
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.