Ottieni conteggi incrementali di un valore aggregato in una tabella unita


10

Ho due tabelle in un database MySQL 5.7.22: postse reasons. Ogni riga del post ha e appartiene a molte righe del motivo. Ogni motivo ha un peso associato ad esso e ogni post ha quindi un peso totale aggregato ad esso associato.

Per ogni incremento di 10 punti di peso (cioè per 0, 10, 20, 30, ecc.), Voglio ottenere un conteggio di post che abbiano un peso totale inferiore o uguale a quell'incremento. Mi aspetto che i risultati siano simili a questi:

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

I pesi totali sono approssimativamente distribuiti normalmente, con pochi valori molto bassi e pochi valori molto alti (il massimo è attualmente 1277), ma la maggior parte nel mezzo. Ci sono poco meno di 120.000 file dentro postse circa 120 pollici reasons. Ogni post ha in media 5 o 6 motivi.

Le parti pertinenti delle tabelle sono simili alle seguenti:

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

Finora, ho provato a far cadere l'ID del post e il peso totale in una vista, quindi unendo quella vista a se stessa per ottenere un conteggio aggregato:

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;

Questo è, comunque, insolitamente lento: l'ho lasciato funzionare per 15 minuti senza interruzione, cosa che non posso fare in produzione.

C'è un modo più efficiente per farlo?

Se sei interessato a testare l'intero set di dati, è scaricabile qui . Il file è di circa 60 MB, si espande a circa 250 MB. In alternativa, ci sono 12.000 righe in una sintesi di GitHub qui .

Risposte:


8

L'uso di funzioni o espressioni in condizioni JOIN è di solito una cattiva idea, dico di solito perché alcuni ottimizzatori possono gestirlo abbastanza bene e utilizzare comunque gli indici. Suggerirei di creare una tabella per i pesi. Qualcosa di simile a:

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

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

Assicurati di avere indici su posts_reasons:

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

Una query come:

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;

La mia macchina a casa probabilmente ha 5-6 anni, ha una CPU Intel (R) Core (TM) i5-3470 a 3,20 GHz e 8 GB di RAM.

uname -a Linux dustbite 4.16.6-302.fc28.x86_64 # 1 SMP mer 2 maggio 00:07:06 UTC 2018 x86_64 x86_64 x86_64 GNU / Linux

Ho testato contro:

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)

Se le prestazioni sono fondamentali e nient'altro ti aiuta, puoi creare una tabella di riepilogo per:

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

È possibile mantenere questa tabella tramite i trigger

Poiché è necessario eseguire una determinata quantità di lavoro per ciascun peso in pesi, può essere utile limitare questa tabella.

    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

Dato che nella mia tabella dei pesi c'erano molte file non necessarie (max 2590), la restrizione sopra ha ridotto i tempi di esecuzione da 9 a 4 secondi.


Chiarimento: sembra che stia contando le ragioni con un peso inferiore a w.weight- giusto? Sto cercando di contare i post con un peso totale (somma dei pesi delle righe dei motivi associati) di lte w.weight.
ArtOfCode

Mi dispiace. Riscriverò la domanda
Lennart

Questo mi ha fatto il resto, comunque, quindi grazie! Ho solo bisogno di selezionare dalla post_weightsvista esistente che ho già creato anziché reasons.
ArtOfCode

@ArtOfCode, ho capito bene per la query rivista? A proposito, grazie per un'ottima domanda. Chiaro, conciso e con molti dati di esempio. Bravo
Lennart

7

In MySQL, le variabili possono essere utilizzate nelle query sia per essere calcolate dai valori nelle colonne sia per essere utilizzate nell'espressione per nuove colonne calcolate. In questo caso, l'utilizzo di una variabile genera una query efficiente:

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 dtabella derivata è in realtà la tua post_weightsvista. Pertanto, se si prevede di mantenere la vista, è possibile utilizzarla al posto della tabella derivata:

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 demo di questa soluzione, che utilizza un'edizione concisa della versione ridotta della configurazione, è disponibile e riproducibile su SQL Fiddle .


Ho provato la tua query con il set di dati completo. Non sono sicuro del perché (la query mi sembra ERROR 1055 (42000): 'd.reason_weight' isn't in GROUP BYsoddisfacente ) ma MariaDB si lamenta se si ONLY_FULL_GROUP_BYtrova in @@ sql_mode. Disattivandolo ho notato che la tua query è più lenta della mia la prima volta che viene eseguita (~ 11 sec). Una volta memorizzati nella cache, i dati sono più veloci (~ 1 secondo). La mia query viene eseguita in circa 4 secondi ogni volta.
Lennart,

1
@Lennart: Questo perché non è la query effettiva. L'ho corretto nel violino ma ho dimenticato di aggiornare la risposta. Aggiornandolo ora, grazie per l'heads-up.
Andriy M,

@Lennart: Per quanto riguarda la performance, potrei avere un'idea sbagliata di questo tipo di query. Ho pensato che avrebbe dovuto funzionare in modo efficiente perché i calcoli sarebbero stati completati in un passaggio sopra il tavolo. Forse non è necessariamente il caso delle tabelle derivate, in particolare di quelle che usano l'aggregazione. Temo di non avere né una corretta installazione di MySQL né abbastanza esperienza per analizzare più a fondo, però.
Andriy M,

@Andriy_M, sembra essere un bug nella mia versione di MariaDB. Non gli piace GROUP BY FLOOR(reason_weight / 10)ma accetta GROUP BY reason_weight. Per quanto riguarda le prestazioni, di certo non sono un esperto neanche per quanto riguarda MySQL, era solo un'osservazione sulla mia macchina scadente. Da quando ho eseguito prima la mia query, tutti i dati avrebbero già dovuto essere memorizzati nella cache, quindi non so perché sia ​​stato più lento la prima volta che è stato eseguito.
Lennart,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.