Obtenga recuentos incrementales de un valor agregado en una tabla unida

10

Tengo dos tablas en una base de datos MySQL 5.7.22: postsy reasons. Cada fila de publicación tiene y pertenece a muchas filas de razones. Cada razón tiene un peso asociado y, por lo tanto, cada publicación tiene un peso agregado total asociado.

Para cada incremento de 10 puntos de peso (es decir, para 0, 10, 20, 30, etc.), quiero obtener un recuento de publicaciones que tengan un peso total menor o igual a ese incremento. Esperaría que los resultados se vean así:

 weight | post_count
--------+------------
      0 | 0
     10 | 5
     20 | 12
     30 | 18
    ... | ...
    280 | 20918
    290 | 21102
    ... | ...
   1250 | 118005
   1260 | 118039
   1270 | 118040

Los pesos totales se distribuyen aproximadamente normalmente, con unos valores muy bajos y unos valores muy altos (el máximo es actualmente 1277), pero la mayoría en el medio. Hay poco menos de 120,000 filas postsy alrededor de 120 pulgadas reasons. Cada publicación tiene en promedio 5 o 6 razones.

Las partes relevantes de las tablas se ven así:

CREATE TABLE `posts` (
  id BIGINT PRIMARY KEY
);

CREATE TABLE `reasons` (
  id BIGINT PRIMARY KEY,
  weight INT(11) NOT NULL
);

CREATE TABLE `posts_reasons` (
  post_id BIGINT NOT NULL,
  reason_id BIGINT NOT NULL,
  CONSTRAINT fk_posts_reasons_posts (post_id) REFERENCES posts(id),
  CONSTRAINT fk_posts_reasons_reasons (reason_id) REFERENCES reasons(id)
);

Hasta ahora, he intentado colocar el ID de la publicación y el peso total en una vista, luego unir esa vista para obtener un recuento agregado:

CREATE VIEW `post_weights` AS (
    SELECT 
        posts.id,
        SUM(reasons.weight) AS reason_weight
    FROM posts
    INNER JOIN posts_reasons ON posts.id = posts_reasons.post_id
    INNER JOIN reasons ON posts_reasons.reason_id = reasons.id
    GROUP BY posts.id
);

SELECT
    FLOOR(p1.reason_weight / 10) AS weight,
    COUNT(DISTINCT p2.id) AS cumulative
FROM post_weights AS p1
INNER JOIN post_weights AS p2 ON FLOOR(p2.reason_weight / 10) <= FLOOR(p1.reason_weight / 10)
GROUP BY FLOOR(p1.reason_weight / 10)
ORDER BY FLOOR(p1.reason_weight / 10) ASC;

Sin embargo, eso es inusualmente lento: lo dejé correr durante 15 minutos sin terminar, lo que no puedo hacer en producción.

¿Hay una manera más eficiente de hacer esto?

En caso de que esté interesado en probar todo el conjunto de datos, se puede descargar aquí . El archivo tiene alrededor de 60 MB, se expande a alrededor de 250 MB. Alternativamente, hay 12,000 filas en una esencia de GitHub aquí .

ArtOfCode
fuente

Respuestas:

8

Usar funciones o expresiones en condiciones JOIN suele ser una mala idea, digo generalmente porque algunos optimizadores pueden manejarlo bastante bien y utilizar índices de todos modos. Sugeriría crear una tabla para los pesos. Algo como:

CREATE TABLE weights
( weight int not null primary key 
);

INSERT INTO weights (weight) VALUES (0),(10),(20),...(1270);

Asegúrese de tener índices en posts_reasons:

CREATE UNIQUE INDEX ... ON posts_reasons (reason_id, post_id);

Una consulta como:

SELECT w.weight
     , COUNT(1) as post_count
FROM weights w
JOIN ( SELECT pr.post_id, SUM(r.weight) as sum_weight     
       FROM reasons r
       JOIN posts_reasons pr
             ON r.id = pr.reason_id
       GROUP BY pr.post_id
     ) as x
    ON w.weight > x.sum_weight
GROUP BY w.weight;

Mi máquina en casa probablemente tiene entre 5 y 6 años, tiene una CPU Intel (R) Core (TM) i5-3470 a 3.20 GHz y 8 Gb de ram.

uname -a Linux dustbite 4.16.6-302.fc28.x86_64 # 1 SMP mié 2 de mayo 00:07:06 UTC 2018 x86_64 x86_64 x86_64 GNU / Linux

Probé contra:

https://drive.google.com/open?id=1q3HZXW_qIZ01gU-Krms7qMJW3GCsOUP5

MariaDB [test3]> select @@version;
+-----------------+
| @@version       |
+-----------------+
| 10.2.14-MariaDB |
+-----------------+
1 row in set (0.00 sec)


SELECT w.weight
     , COUNT(1) as post_count
FROM weights w
JOIN ( SELECT pr.post_id, SUM(r.weight) as sum_weight     
       FROM reasons r
       JOIN posts_reasons pr
             ON r.id = pr.reason_id
       GROUP BY pr.post_id
     ) as x
    ON w.weight > x.sum_weight
GROUP BY w.weight;

+--------+------------+
| weight | post_count |
+--------+------------+
|      0 |          1 |
|     10 |       2591 |
|     20 |       4264 |
|     30 |       4386 |
|     40 |       5415 |
|     50 |       7499 |
[...]   
|   1270 |     119283 |
|   1320 |     119286 |
|   1330 |     119286 |
[...]
|   2590 |     119286 |
+--------+------------+
256 rows in set (9.89 sec)

Si el rendimiento es crítico y nada más lo ayuda, podría crear una tabla resumen para:

SELECT pr.post_id, SUM(r.weight) as sum_weight     
FROM reasons r
JOIN posts_reasons pr
    ON r.id = pr.reason_id
GROUP BY pr.post_id

Puedes mantener esta tabla a través de disparadores

Dado que hay una cierta cantidad de trabajo que debe hacerse para cada peso en pesos, puede ser beneficioso limitar esta tabla.

    ON w.weight > x.sum_weight 
WHERE w.weight <= (select MAX(sum_weights) 
                   from (SELECT SUM(weight) as sum_weights 
                   FROM reasons r        
                   JOIN posts_reasons pr
                       ON r.id = pr.reason_id 
                   GROUP BY pr.post_id) a
                  ) 
GROUP BY w.weight

Como tenía muchas filas innecesarias en mi tabla de pesos (máximo 2590), la restricción anterior redujo el tiempo de ejecución de 9 a 4 segundos.

Lennart
fuente
Aclaración: Parece que son razones de contar con un peso menor que w.weight, ¿es así? Estoy buscando contar publicaciones con un peso total (suma de pesos de sus filas de razón asociadas) de lte w.weight.
ArtOfCode
Oh, lo siento. Reescribiré la consulta
Lennart,
Sin embargo, esto me dejó el resto del camino, ¡así que gracias! Solo necesitaba seleccionar de la post_weightsvista existente que ya creé en lugar de reasons.
ArtOfCode
@ArtOfCode, ¿lo hice bien para la consulta revisada? Por cierto, gracias por una excelente pregunta. Claro, conciso y con muchos datos de muestra. Bravo
Lennart
7

En MySQL, las variables pueden usarse en consultas tanto para calcular a partir de valores en columnas como para usar en la expresión de columnas nuevas calculadas. En este caso, el uso de una variable da como resultado una consulta eficiente:

SELECT
  weight,
  @cumulative := @cumulative + post_count AS post_count
FROM
  (SELECT @cumulative := 0) AS x,
  (
    SELECT
      FLOOR(reason_weight / 10) * 10 AS weight,
      COUNT(*)                       AS post_count
    FROM
      (
        SELECT 
          p.id,
          SUM(r.weight) AS reason_weight
        FROM
          posts AS p
          INNER JOIN posts_reasons AS pr ON p.id = pr.post_id
          INNER JOIN reasons AS r ON pr.reason_id = r.id
        GROUP BY
          p.id
      ) AS d
    GROUP BY
      FLOOR(reason_weight / 10)
    ORDER BY
      FLOOR(reason_weight / 10) ASC
  ) AS derived
;

La dtabla derivada es en realidad su post_weightspunto de vista. Por lo tanto, si planea mantener la vista, puede usarla en lugar de la tabla derivada:

SELECT
  weight,
  @cumulative := @cumulative + post_count AS post_count
FROM
  (SELECT @cumulative := 0),
  (
    SELECT
      FLOOR(reason_weight / 10) * 10 AS weight,
      COUNT(*)                       AS post_count
    FROM
      post_weights
    GROUP BY
      FLOOR(reason_weight / 10)
    ORDER BY
      FLOOR(reason_weight / 10) ASC
  ) AS derived
;

Una demostración de esta solución, que utiliza una edición concisa de la versión reducida de su configuración, se puede encontrar y jugar en SQL Fiddle .

Andriy M
fuente
Intenté su consulta con el conjunto de datos completo. No estoy seguro de por qué (la consulta me parece bien), pero MariaDB se queja de ERROR 1055 (42000): 'd.reason_weight' isn't in GROUP BYsi ONLY_FULL_GROUP_BYestá en @@ sql_mode. Inhabilitándolo, noté que su consulta es más lenta que la mía la primera vez que se ejecuta (~ 11 segundos). Una vez que los datos se almacenan en caché, es más rápido (~ 1 segundo). Mi consulta se ejecuta en aproximadamente 4 segundos cada vez.
Lennart
1
@Lennart: Eso es porque no es la consulta real. Lo corregí en el violín, pero olvidé actualizar la respuesta. Actualizándolo ahora, gracias por el aviso.
Andriy M
@Lennart: En cuanto al rendimiento, puedo tener una idea errónea sobre este tipo de consulta. Pensé que debería funcionar de manera eficiente porque los cálculos se completarían de una sola vez sobre la mesa. Quizás ese no sea necesariamente el caso con las tablas derivadas, en particular las que usan agregación. Sin embargo, me temo que no tengo ni una instalación adecuada de MySQL ni suficiente experiencia para analizar en profundidad.
Andriy M
@Andriy_M, parece ser un error en mi versión de MariaDB. No le gusta GROUP BY FLOOR(reason_weight / 10)pero acepta GROUP BY reason_weight. En cuanto al rendimiento, ciertamente tampoco soy un experto cuando se trata de MySQL, fue solo una observación en mi máquina de mierda. Como ejecuté mi consulta primero, todos los datos ya deberían haberse almacenado en caché, por lo que no sé por qué fue más lento la primera vez que se ejecutó.
Lennart