Obtenir le nombre incrémentiel d'une valeur agrégée dans une table jointe


10

J'ai deux tables dans une base de données MySQL 5.7.22: postset reasons. Chaque ligne de publication a et appartient à de nombreuses lignes de raison. Chaque raison a un poids qui lui est associé, et chaque poste a donc un poids total agrégé qui lui est associé.

Pour chaque incrément de 10 points de poids (c'est-à-dire pour 0, 10, 20, 30, etc.), je veux obtenir un nombre de messages dont le poids total est inférieur ou égal à cet incrément. Je m'attendrais à ce que les résultats ressemblent à ceci:

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

Les poids totaux sont approximativement normalement distribués, avec quelques valeurs très faibles et quelques valeurs très élevées (le maximum est actuellement 1277), mais la majorité au milieu. Il y a un peu moins de 120 000 rangées postset environ 120 pouces reasons. Chaque message a en moyenne 5 ou 6 raisons.

Les parties pertinentes des tableaux ressemblent à ceci:

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

Jusqu'à présent, j'ai essayé de supprimer l'ID du message et le poids total dans une vue, puis de joindre cette vue à elle-même pour obtenir un nombre agrégé:

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;

C'est cependant inhabituellement lent - je l'ai laissé fonctionner pendant 15 minutes sans terminer, ce que je ne peux pas faire en production.

Existe-t-il un moyen plus efficace de procéder?

Si vous souhaitez tester l'ensemble de données, il est téléchargeable ici . Le fichier fait environ 60 Mo, il s'étend à environ 250 Mo. Alternativement, il y a 12 000 lignes dans un résumé GitHub ici .

Réponses:


8

L'utilisation de fonctions ou d'expressions dans des conditions JOIN est généralement une mauvaise idée, je dis généralement parce que certains optimiseurs peuvent le gérer assez bien et utiliser des index de toute façon. Je suggère de créer une table pour les poids. Quelque chose comme:

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

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

Assurez-vous d'avoir des index sur posts_reasons:

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

Une requête comme:

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;

Ma machine à la maison a probablement 5-6 ans, elle a un processeur Intel (R) Core (TM) i5-3470 à 3,20 GHz et 8 Go de RAM.

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

J'ai testé contre:

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 les performances sont critiques et que rien d'autre ne vous aide, vous pouvez créer un tableau récapitulatif pour:

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

Vous pouvez gérer cette table via des déclencheurs

Puisqu'il y a une certaine quantité de travail à faire pour chaque poids en poids, il peut être avantageux de limiter ce tableau.

    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

Étant donné que j'avais beaucoup de lignes inutiles dans ma table de poids (max 2590), la restriction ci-dessus a réduit le temps d'exécution de 9 à 4 secondes.


Clarification: cela ressemble à des raisons de compter avec un poids inférieur à w.weight- est-ce vrai? Je cherche à compter les messages avec un poids total (somme des poids de leurs lignes de raison associées) de lte w.weight.
ArtOfCode

Ah désolé. Je vais réécrire la requête
Lennart

Cela m'a cependant permis de continuer, alors merci! Juste besoin de sélectionner dans la post_weightsvue existante que j'ai déjà créée à la place reasons.
ArtOfCode

@ArtOfCode, ai-je bien compris la requête révisée? BTW, merci pour une excellente question. Clair, concis et avec beaucoup d'échantillons de données. Bravo
Lennart

7

Dans MySQL, les variables peuvent être utilisées dans les requêtes à la fois pour être calculées à partir des valeurs des colonnes et pour être utilisées dans l'expression pour les nouvelles colonnes calculées. Dans ce cas, l'utilisation d'une variable entraîne une requête efficace:

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 dtable dérivée est en fait votre post_weightsvue. Par conséquent, si vous prévoyez de conserver la vue, vous pouvez l'utiliser à la place de la table dérivée:

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
;

Une démonstration de cette solution, qui utilise une édition concise de la version réduite de votre configuration, peut être trouvée et jouée avec à SQL Fiddle .


J'ai essayé votre requête avec l'ensemble de données complet. Je ne sais pas pourquoi (la requête me semble correcte) mais MariaDB se plaint ERROR 1055 (42000): 'd.reason_weight' isn't in GROUP BYsi ONLY_FULL_GROUP_BYest dans @@ sql_mode. En le désactivant, j'ai remarqué que votre requête est plus lente que la mienne la première fois (~ 11 sec). Une fois les données mises en cache, elles sont plus rapides (~ 1 sec). Ma requête s'exécute en environ 4 secondes à chaque fois.
Lennart

1
@Lennart: C'est parce que ce n'est pas la requête réelle. Je l'ai corrigé au violon mais j'ai oublié de mettre à jour la réponse. Le mettre à jour maintenant, merci pour l'avertissement.
Andriy M

@Lennart: En ce qui concerne les performances, je peux avoir une idée fausse sur ce type de requête. Je pensais que cela devrait fonctionner efficacement car les calculs seraient terminés en un seul passage sur la table. Ce n'est peut-être pas nécessairement le cas avec les tableaux dérivés, en particulier ceux qui utilisent l'agrégation. Je crains cependant de ne pas avoir une installation MySQL appropriée ni une expertise suffisante pour analyser plus en profondeur.
Andriy M

@Andriy_M, il semble que ce soit un bug dans ma version MariaDB. Il n'aime pas GROUP BY FLOOR(reason_weight / 10)mais accepte GROUP BY reason_weight. En ce qui concerne les performances, je ne suis certainement pas un expert non plus en ce qui concerne MySQL, c'était juste une observation sur ma machine de merde. Depuis que j'ai exécuté ma requête en premier, toutes les données devraient déjà avoir été mises en cache, donc je ne sais pas pourquoi elles ont été plus lentes lors de leur première exécution.
Lennart
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.