Dapatkan hitungan tambahan dari nilai agregat di tabel bergabung

10

Saya punya dua tabel di database MySQL 5.7.22: postsdan reasons. Setiap baris posting memiliki dan memiliki banyak alasan. Setiap alasan memiliki bobot yang terkait dengannya, dan oleh karena itu setiap pos memiliki total bobot agregat yang terkait dengannya.

Untuk setiap kenaikan 10 poin berat (yaitu untuk 0, 10, 20, 30, dll), saya ingin mendapatkan hitungan posting yang memiliki berat total kurang dari atau sama dengan kenaikan itu. Saya berharap hasil untuk itu terlihat seperti ini:

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

Berat total kira-kira terdistribusi normal, dengan beberapa nilai sangat rendah dan beberapa nilai sangat tinggi (maksimum saat ini 1277), tetapi mayoritas di tengah. Ada hanya di bawah 120.000 baris posts, dan sekitar 120 di reasons. Setiap posting memiliki rata-rata 5 atau 6 alasan.

Bagian-bagian tabel yang relevan terlihat seperti ini:

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

Sejauh ini, saya sudah mencoba menjatuhkan ID pos dan bobot total ke tampilan, lalu menggabungkan tampilan itu ke dirinya sendiri untuk mendapatkan jumlah teragregasi:

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;

Namun, itu sangat lambat - saya membiarkannya berjalan selama 15 menit tanpa berhenti, yang tidak dapat saya lakukan dalam produksi.

Apakah ada cara yang lebih efisien untuk melakukan ini?

Jika Anda tertarik untuk menguji seluruh dataset, ini dapat diunduh di sini . File ini sekitar 60MB, diperluas menjadi sekitar 250MB. Bergantian, ada 12.000 baris dalam inti GitHub di sini .

ArtOfCode
sumber

Jawaban:

8

Menggunakan fungsi atau ekspresi dalam kondisi BERGABUNG biasanya merupakan ide yang buruk, saya katakan biasanya karena beberapa pengoptimal dapat menanganinya dengan cukup baik dan memanfaatkan indeks bagaimanapun juga. Saya akan menyarankan membuat tabel untuk bobot. Sesuatu seperti:

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

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

Pastikan Anda memiliki indeks di posts_reasons:

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

Kueri seperti:

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;

Mesin saya di rumah mungkin berusia 5-6 tahun, ia memiliki CPU Intel (R) Core (TM) i5-3470 @ 3.20GHz dan ram 8Gb.

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

Saya menguji terhadap:

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)

Jika kinerja sangat penting dan tidak ada yang membantu Anda bisa membuat tabel ringkasan untuk:

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

Anda bisa mempertahankan tabel ini melalui pemicu

Karena ada sejumlah pekerjaan yang perlu dilakukan untuk setiap berat dalam bobot, mungkin bermanfaat untuk membatasi tabel ini.

    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

Karena saya memiliki banyak baris yang tidak perlu pada tabel bobot saya (maks 2590), batasan di atas memangkas waktu eksekusi dari 9 menjadi 4 detik.

Lennart
sumber
Klarifikasi: Ini sepertinya menghitung alasan dengan bobot lebih rendah dari w.weight- apakah itu benar? Saya ingin menghitung posting dengan total bobot (jumlah bobot dari baris alasan terkait) dari lte w.weight.
ArtOfCode
Ah maaf. Saya akan menulis ulang kueri
Lennart
Ini membuat saya sisa, jadi terima kasih! Hanya perlu memilih dari tampilan yang ada post_weightsyang sudah saya buat bukan reasons.
ArtOfCode
@ArtOfCode, apakah saya sudah benar untuk permintaan yang direvisi? BTW, terima kasih atas pertanyaan yang bagus. Jelas, ringkas dan dengan banyak data sampel. Bravo
Lennart
7

Di MySQL, variabel dapat digunakan dalam kueri untuk dihitung dari nilai dalam kolom dan untuk digunakan dalam ekspresi untuk kolom baru yang dihitung. Dalam kasus ini, menggunakan variabel akan menghasilkan kueri yang efisien:

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
;

The dtable berasal sebenarnya Anda post_weightslihat. Oleh karena itu, jika Anda berencana mempertahankan tampilan, Anda dapat menggunakannya sebagai ganti tabel turunan:

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
;

Demo dari solusi ini, yang menggunakan edisi singkat dari versi yang dikurangi dari pengaturan Anda, dapat ditemukan dan dimainkan di SQL Fiddle .

Andriy M
sumber
Saya mencoba permintaan Anda dengan set data lengkap. Saya tidak yakin mengapa (kueri terlihat ok untuk saya) tetapi MariaDB mengeluh tentang ERROR 1055 (42000): 'd.reason_weight' isn't in GROUP BYapakah ONLY_FULL_GROUP_BYada di @@ sql_mode. Menonaktifkannya Saya perhatikan bahwa permintaan Anda lebih lambat dari saya pertama kali dijalankan (~ 11 detik). Setelah data di-cache lebih cepat (~ 1 detik). Permintaan saya berjalan sekitar 4 detik setiap kali.
Lennart
1
@Lennart: Itu karena itu bukan permintaan yang sebenarnya. Saya memperbaikinya di biola tetapi lupa untuk memperbarui jawabannya. Memperbarui itu sekarang, terima kasih atas bantuannya.
Andriy M
@Lennart: Adapun kinerja, saya mungkin memiliki kesalahpahaman tentang jenis permintaan ini. Saya pikir itu harus bekerja secara efisien karena perhitungan akan selesai dalam satu melewati tabel. Mungkin itu belum tentu halnya dengan tabel turunan, khususnya yang menggunakan agregasi. Saya khawatir saya tidak memiliki instalasi MySQL yang tepat atau keahlian yang cukup untuk menganalisis lebih dalam.
Andriy M
@ Andriy_M, sepertinya ada bug di versi MariaDB saya. Itu tidak suka GROUP BY FLOOR(reason_weight / 10)tetapi menerima GROUP BY reason_weight. Adapun kinerja saya tentu bukan ahli juga ketika datang ke MySQL, itu hanya pengamatan pada mesin jelek saya. Karena saya menjalankan kueri saya pertama-tama semua data seharusnya sudah di-cache, jadi saya tidak tahu mengapa ini lebih lambat saat pertama kali dijalankan.
Lennart