Ruft inkrementelle Zählungen eines aggregierten Werts in einer verknüpften Tabelle ab

10

Ich habe zwei Tabellen in einer MySQL 5.7.22-Datenbank: postsund reasons. Jede Beitragszeile hat und gehört zu vielen Grundzeilen. Jeder Grund hat ein Gewicht mit ihm verbunden, und jeder Beitrag hat daher ein Gesamt aggregierte Gewicht mit ihm verbunden ist .

Für jedes Inkrement von 10 Gewichtspunkten (dh für 0, 10, 20, 30 usw.) möchte ich eine Anzahl von Posts erhalten, deren Gesamtgewicht kleiner oder gleich diesem Inkrement ist. Ich würde erwarten, dass die Ergebnisse dafür ungefähr so ​​aussehen:

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

Die Gesamtgewichte sind ungefähr normal verteilt, mit einigen sehr niedrigen Werten und einigen sehr hohen Werten (Maximum ist derzeit 1277), aber die Mehrheit in der Mitte. Es gibt knapp 120.000 Zeilen in postsund rund 120 in reasons. Jeder Beitrag hat durchschnittlich 5 oder 6 Gründe.

Die relevanten Teile der Tabellen sehen folgendermaßen aus:

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

Bisher habe ich versucht , die Post - ID und fallen Gesamtgewicht in eine Ansicht, dann verbindet diese Ansicht selbst eine aggregierte Zählung zu erhalten:

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;

Das ist jedoch ungewöhnlich langsam - ich lasse es 15 Minuten lang laufen, ohne es zu beenden, was ich in der Produktion nicht tun kann.

Gibt es einen effizienteren Weg, dies zu tun?

Wenn Sie den gesamten Datensatz testen möchten, können Sie ihn hier herunterladen . Die Datei ist ungefähr 60 MB groß und wird auf ungefähr 250 MB erweitert. Alternativ gibt es 12.000 Zeilen in einem GitHub Kern hier .

ArtOfCode
quelle

Antworten:

8

Die Verwendung von Funktionen oder Ausdrücken unter JOIN-Bedingungen ist normalerweise eine schlechte Idee, sage ich normalerweise, weil einige Optimierer damit ziemlich gut umgehen und trotzdem Indizes verwenden können. Ich würde vorschlagen, eine Tabelle für die Gewichte zu erstellen. Etwas wie:

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

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

Stellen Sie sicher, dass Sie Indizes haben für posts_reasons:

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

Eine Abfrage wie:

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;

Mein Computer zu Hause ist wahrscheinlich 5-6 Jahre alt und verfügt über eine Intel (R) Core (TM) i5-3470-CPU mit 3,20 GHz und 8 GB RAM.

uname -a Linux Dustbite 4.16.6-302.fc28.x86_64 # 1 SMP Mi 2. Mai 00:07:06 UTC 2018 x86_64 x86_64 x86_64 GNU / Linux

Ich habe getestet gegen:

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)

Wenn die Leistung kritisch ist und nichts anderes hilft, können Sie eine Übersichtstabelle erstellen für:

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

Sie können diese Tabelle über Trigger pflegen

Da für jedes Gewicht in Gewichten ein gewisser Arbeitsaufwand erforderlich ist, kann es vorteilhaft sein, diese Tabelle einzuschränken.

    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

Da meine Gewichtungstabelle viele unnötige Zeilen enthielt (max. 2590), reduzierte die obige Einschränkung die Ausführungszeit von 9 auf 4 Sekunden.

Lennart
quelle
Klarstellung: Es sieht so aus, als würde es Gründe mit einem Gewicht zählen, das niedriger ist als w.weight- stimmt das? Ich bin auf der Suche Beiträge mit einem zählen Gesamtgewicht (Summe der Gewichte ihrer zugehörigen Grunde Reihen) von lte w.weight.
ArtOfCode
Oh, tut mir leid. Ich werde die Abfrage umschreiben
Lennart
Das hat mich aber den Rest des Weges gebracht, also danke! Muss nur aus der vorhandenen post_weightsAnsicht auswählen , die ich bereits erstellt habe, anstatt reasons.
ArtOfCode
@ArtOfCode, habe ich es für die überarbeitete Abfrage richtig gemacht? Übrigens, danke für eine ausgezeichnete Frage. Klar, präzise und mit vielen Beispieldaten. Bravo
Lennart
7

In MySQL können Variablen in Abfragen verwendet werden, die sowohl aus Werten in Spalten berechnet als auch als Ausdruck für neue, berechnete Spalten verwendet werden. In diesem Fall führt die Verwendung einer Variablen zu einer effizienten Abfrage:

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
;

Die dabgeleitete Tabelle ist eigentlich Ihre post_weightsAnsicht. Wenn Sie die Ansicht beibehalten möchten, können Sie sie daher anstelle der abgeleiteten Tabelle verwenden:

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
;

Eine Demo dieser Lösung, die eine übersichtliche Version der reduzierten Version Ihres Setups verwendet, kann bei SQL Fiddle gefunden und gespielt werden .

Andriy M.
quelle
Ich habe Ihre Anfrage mit dem vollständigen Datensatz versucht. Ich bin mir nicht sicher warum (die Abfrage sieht für mich in Ordnung aus ), aber MariaDB beschwert sich darüber, ERROR 1055 (42000): 'd.reason_weight' isn't in GROUP BYob sie ONLY_FULL_GROUP_BYsich in @@ sql_mode befindet. Deaktivieren Ich habe festgestellt, dass Ihre Abfrage beim ersten Ausführen langsamer ist als meine (~ 11 Sek.). Sobald die Daten zwischengespeichert sind, sind sie schneller (~ 1 Sek.). Meine Abfrage wird jedes Mal in ca. 4 Sekunden ausgeführt.
Lennart
1
@ Lennart: Das liegt daran, dass es nicht die eigentliche Abfrage ist. Ich habe es in der Geige korrigiert, aber vergessen, die Antwort zu aktualisieren. Jetzt aktualisieren, danke für das Heads-up.
Andriy M
@ Lennart: Was die Leistung betrifft, habe ich möglicherweise ein Missverständnis über diese Art von Abfrage. Ich dachte, es sollte effizient funktionieren, da die Berechnungen in einem Durchgang über die Tabelle abgeschlossen wären. Möglicherweise ist dies bei abgeleiteten Tabellen nicht unbedingt der Fall, insbesondere bei solchen, die Aggregation verwenden. Ich fürchte, ich habe weder eine ordnungsgemäße MySQL-Installation noch genug Fachwissen, um tiefer zu analysieren.
Andriy M
@Andriy_M, es scheint ein Fehler in meiner MariaDB-Version zu sein. Es mag nicht, GROUP BY FLOOR(reason_weight / 10)aber akzeptiert GROUP BY reason_weight. Was die Leistung angeht, bin ich sicherlich auch kein Experte, wenn es um MySQL geht, es war nur eine Beobachtung auf meiner beschissenen Maschine. Da ich meine Abfrage zuerst ausgeführt habe, sollten alle Daten bereits zwischengespeichert sein, sodass ich nicht weiß, warum sie beim ersten Ausführen langsamer waren.
Lennart