Birleştirilmiş tabloda toplam değerin artımlı sayılarını alma


10

MySQL 5.7.22 veritabanında iki tablo var: postsve reasons. Her gönderi satırında birçok neden satırı vardır ve bunlara aittir. Her nedenin kendisiyle ilişkili bir ağırlığı vardır ve bu nedenle her bir gönderinin kendisiyle ilişkili toplam bir toplam ağırlığı vardır.

10 puan ağırlığındaki her bir artış için (yani, 0, 10, 20, 30, vb. İçin), toplam ağırlığı bu artıştan daha az veya ona eşit olan bir dizi direk almak istiyorum. Bunun için böyle bir sonuç bekliyorum:

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

Toplam ağırlıklar yaklaşık olarak normal olarak dağıtılır, birkaç çok düşük değer ve birkaç çok yüksek değer (maksimum şu anda 1277'dir), ancak ortadaki çoğunluk. Yaklaşık 120.000 satır postsve 120 inç civarında satırlar var reasons. Her gönderinin ortalama 5 veya 6 nedeni vardır.

Tabloların ilgili kısımları aşağıdaki gibidir:

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

Şimdiye kadar, bir numaraya gönderi kimliğini ve toplam ağırlığını bırakmayı , ardından toplu bir sayı elde etmek için bu görünüme katılmayı denedim :

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;

Ancak bu, alışılmadık derecede yavaş - sonlandırmadan 15 dakika boyunca çalışmasına izin verdim, bu da üretimde yapamam.

Bunu yapmanın daha etkili bir yolu var mı?

Tüm veri kümesini test etmekle ilgileniyorsanız, buradan indirilebilir . Dosya yaklaşık 60 MB, yaklaşık 250 MB'a kadar genişler. Alternatif olarak, burada bir GitHub özünde 12.000 satır var .

Yanıtlar:


8

JOIN koşullarında işlevlerin veya ifadelerin kullanılması genellikle kötü bir fikirdir, genellikle diyorum çünkü bazı optimizasyoncular bunu oldukça iyi idare edebilir ve yine de indeksleri kullanabilirler. Ağırlıklar için bir tablo oluşturmayı öneririm. Gibi bir şey:

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

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

Üzerinde dizinler bulunduğundan emin olun posts_reasons:

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

Şunun gibi bir sorgu:

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;

Evdeki makinem muhtemelen 5-6 yaşında, Intel (R) Core (TM) i5-3470 CPU @ 3.20GHz ve 8Gb ram'ye sahip.

uname -Laux dustbite 4.16.6-302.fc28.x86_64 # 1 SMP Çar 2 Mayıs 00:07:06 UTC 2018 x86_64 x86_64 x86_64 GNU / Linux

Test ettim:

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)

Performans kritikse ve başka hiçbir şey yardımcı olmazsa, aşağıdakiler için bir özet tablo oluşturabilirsiniz:

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

Bu tabloyu tetikleyiciler aracılığıyla koruyabilirsiniz

Ağırlıklardaki her ağırlık için yapılması gereken belirli bir miktar iş olduğundan, bu tabloyu sınırlamak faydalı olabilir.

    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

Ağırlık masamda (max 2590) çok fazla gereksiz satırım olduğundan, yukarıdaki kısıtlama yürütme süresini 9'dan 4 saniyeye düşürdü.


Açıklama: Bu , ağırlığı daha düşük olan nedenleri sayıyor gibi görünüyor w.weight- doğru mu? Lte toplam ağırlığı (ilişkili neden satırlarının ağırlık toplamı) ile mesajları saymak için arıyorum w.weight.
ArtOfCode

Ah Üzgünüm. Sorguyu yeniden yazacağım
Lennart

Bu beni yolun geri kalanına götürdü, ama teşekkürler! Bunun post_weightsyerine önceden oluşturduğum mevcut görünümden seçim yapmam gerekiyordu reasons.
ArtOfCode

@ArtOfCode, düzeltilmiş sorgu için doğru anladım mı? BTW, mükemmel bir soru için teşekkürler. Net, özlü ve çok sayıda örnek veri ile. Bravo
Lennart

7

MySQL'de değişkenler, hem sütunlardaki değerlerden hesaplanmak hem de yeni, hesaplanmış sütunlar için ifadede kullanılmak üzere sorgularda kullanılabilir. Bu durumda, değişken kullanmak etkili bir sorgu ile sonuçlanır:

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
;

dTüretilmiş tablo aslında bir post_weightsgörünümü. Bu nedenle, görünümü tutmayı planlıyorsanız, türetilmiş tablo yerine görünümü kullanabilirsiniz:

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
;

Kurulumunuzun indirgenmiş sürümünün kısa bir sürümünü kullanan bu çözümün bir demosunu SQL Fiddle'da bulabilirsiniz .


Sorgunuzu tam veri kümesiyle denedim. Neden (sorgu bana iyi görünüyor) emin değilim ama MariaDB @ @ sql_mode ERROR 1055 (42000): 'd.reason_weight' isn't in GROUP BYolup olmadığını şikayet ediyor ONLY_FULL_GROUP_BY. Devre dışı bırakma Sorgunuzun ilk çalıştırıldığında (~ 11 sn) benimkinden daha yavaş olduğunu fark ettim. Veriler önbelleğe alındıktan sonra daha hızlıdır (~ 1 sn). Sorgum her seferinde yaklaşık 4 saniye içinde çalışıyor.
Lennart

1
@Lennart: Bunun nedeni asıl sorgu değil. Kemanda düzelttim ama cevabı güncellemeyi unuttum. Şimdi güncelleniyor, dikkatinizi çektiğiniz için teşekkürler.
Andriy M

@Lennart: Performansa gelince, bu tür bir sorgu hakkında bir yanlış anlama olabilir. Bunun verimli çalışması gerektiğini düşündüm çünkü hesaplamalar masanın üzerinden tek seferde tamamlanacaktı. Belki de türetilmiş tablolarda, özellikle de kümelemeyi kullananlarda böyle bir durum söz konusu değildir. Korkarım ki uygun bir MySQL kurulumuna veya daha derin analiz için yeterli uzmanlığa sahip değilim.
Andriy M

@Andriy_M, MariaDB versiyonumda bir hata gibi görünüyor. O sevmez GROUP BY FLOOR(reason_weight / 10)ama kabul GROUP BY reason_weight. Performansa gelince, MySQL söz konusu olduğunda kesinlikle uzman değilim, sadece boktan makinemde bir gözlemdi. Sorgumu ilk çalıştırdığımdan beri tüm veriler zaten önbelleğe alınmış olmalı, bu yüzden ilk kez neden daha yavaş olduğunu bilmiyorum.
Lennart
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.