รับจำนวนที่เพิ่มขึ้นของมูลค่ารวมในตารางที่เข้าร่วม


10

ฉันมีสองตารางในฐานข้อมูล MySQL 5.7.22: และposts reasonsแต่ละแถวโพสต์มีและอยู่ในหลายเหตุผลแถว แต่ละเหตุผลมีน้ำหนักที่เกี่ยวข้องและดังนั้นแต่ละโพสต์จึงมีน้ำหนักรวมทั้งหมดที่เกี่ยวข้อง

สำหรับการเพิ่มน้ำหนักแต่ละจุด 10 คะแนน (เช่น 0, 10, 20, 30, ฯลฯ ) ฉันต้องการรับจำนวนการโพสต์ที่มีน้ำหนักรวมน้อยกว่าหรือเท่ากับการเพิ่มขึ้นนั้น ฉันคาดหวังผลลัพธ์ที่จะมีลักษณะเช่นนี้:

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

น้ำหนักรวมโดยทั่วไปจะกระจายประมาณโดยมีค่าน้อยมากและค่าสูงมากเล็กน้อย (สูงสุดคือ 1277 ปัจจุบัน) แต่ส่วนใหญ่อยู่ตรงกลาง มีเพียงภายใต้ 120,000 แถวอยู่ในpostsและรอบ reasons120 แต่ละโพสต์มีเหตุผลโดยเฉลี่ย 5 หรือ 6 ข้อ

ส่วนที่เกี่ยวข้องของตารางมีลักษณะดังนี้:

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

จนถึงตอนนี้ฉันได้ลองโพสต์ ID และน้ำหนักรวมลงในมุมมองจากนั้นเข้าร่วมมุมมองนั้นกับตัวเองเพื่อรับการนับรวม:

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;

นั่นคืออย่างไรก็ตามช้าผิดปกติ - ฉันปล่อยให้มันทำงานเป็นเวลา 15 นาทีโดยไม่สิ้นสุดซึ่งฉันไม่สามารถทำในการผลิต

มีวิธีที่มีประสิทธิภาพมากกว่านี้หรือไม่?

ในกรณีที่คุณมีความสนใจในการทดสอบชุดข้อมูลทั้งหมดก็สามารถดาวน์โหลดได้ที่นี่ ไฟล์มีขนาดประมาณ 60MB และขยายเป็นประมาณ 250MB อีกวิธีหนึ่งที่มี 12,000 แถวในส่วนสำคัญ GitHub ที่นี่

คำตอบ:


8

การใช้ฟังก์ชั่นหรือการแสดงออกในเงื่อนไขการเข้าร่วมมักเป็นความคิดที่ไม่ดีฉันพูดได้บ่อยครั้งเนื่องจากเครื่องมือเพิ่มประสิทธิภาพบางอย่างสามารถจัดการได้ดีพอสมควรและใช้ดัชนีอย่างไร ฉันขอแนะนำให้สร้างตารางสำหรับน้ำหนัก สิ่งที่ต้องการ:

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

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

ตรวจสอบให้แน่ใจว่าคุณมีดัชนีในposts_reasons:

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

แบบสอบถามที่ชอบ:

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;

เครื่องที่บ้านของฉันน่าจะมีอายุ 5-6 ปี แต่ก็มี Intel (R) Core (TM) i5-3470 CPU @ 3.20GHz และ RAM 8Gb

uname -a Linux dustbite 4.16.6-302.fc28.x86_64 # 1 SMP พุธ 2 พฤษภาคม 00:07:06 UTC 2018 x86_64 x86_64 x86_64 GNU / Linux

ฉันทดสอบกับ:

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)

หากประสิทธิภาพมีความสำคัญและไม่มีสิ่งใดช่วยคุณสามารถสร้างตารางสรุปสำหรับ:

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

คุณสามารถบำรุงรักษาตารางนี้ผ่านทริกเกอร์

เนื่องจากมีจำนวนงานที่ต้องทำสำหรับน้ำหนักแต่ละน้ำหนักจึงอาจเป็นประโยชน์ในการ จำกัด ตารางนี้

    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

เนื่องจากฉันมีแถวที่ไม่จำเป็นจำนวนมากในตารางน้ำหนักของฉัน (สูงสุด 2590) ข้อ จำกัด ข้างต้นจึงลดเวลาดำเนินการจาก 9 ลงเหลือ 4 วินาที


ชี้แจง: ดูเหมือนว่ามันจะนับเหตุผลด้วยน้ำหนักที่ต่ำกว่าw.weight- ใช่ไหม? ฉันกำลังมองหาที่จะนับโพสต์ด้วยรวมน้ำหนัก (ผลรวมของน้ำหนักของแถวเหตุผลที่เกี่ยวข้อง) ของ w.weightLTE
ArtOfCode

ขอโทษด้วย ฉันจะเขียนข้อความค้นหาอีกครั้ง
Lennart

สิ่งนี้ทำให้ฉันได้รับส่วนที่เหลือแม้ว่าจะขอบคุณมาก! เพียงต้องการที่จะเลือกจากที่มีอยู่ในมุมมองที่ผมได้สร้างแทนpost_weights reasons
ArtOfCode

@ArtOfCode ฉันทำให้ถูกต้องสำหรับคำค้นหาที่แก้ไขหรือไม่ BTW ขอบคุณสำหรับคำถามที่ยอดเยี่ยม ชัดเจนกระชับและมีข้อมูลตัวอย่างจำนวนมาก Bravo
Lennart

7

ใน MySQL ตัวแปรสามารถใช้ในแบบสอบถามทั้งสองเพื่อคำนวณจากค่าในคอลัมน์และใช้ในการแสดงออกสำหรับคอลัมน์ใหม่ที่คำนวณได้ ในกรณีนี้การใช้ตัวแปรจะส่งผลให้การสืบค้นมีประสิทธิภาพ:

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
;

dตารางมาเป็นจริงของคุณpost_weightsมุมมอง ดังนั้นหากคุณวางแผนที่จะรักษามุมมองคุณสามารถใช้มันแทนตารางที่ได้รับ:

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
;

การสาธิตของการแก้ปัญหานี้ซึ่งใช้ฉบับสั้นของรุ่นที่ลดลงของการติดตั้งของคุณสามารถพบและเล่นกับที่ SQL ซอ


ฉันลองสืบค้นด้วยชุดข้อมูลแบบเต็ม ฉันไม่แน่ใจว่าทำไม (แบบสอบถามดูโอเคกับฉัน) แต่ MariaDB บ่นERROR 1055 (42000): 'd.reason_weight' isn't in GROUP BYว่าONLY_FULL_GROUP_BYอยู่ใน @@ sql_mode หรือไม่ ปิดการใช้งานฉันสังเกตเห็นว่าการค้นหาของคุณช้ากว่าครั้งแรกที่เรียกใช้ (~ 11 วินาที) เมื่อข้อมูลถูกแคชก็จะเร็วขึ้น (~ 1 วินาที) ข้อความค้นหาของฉันทำงานในเวลาประมาณ 4 วินาทีทุกครั้ง
Lennart

1
@ เลนนาร์ท: นั่นเป็นเพราะไม่ใช่คำค้นหาจริง ฉันแก้ไขในซอ แต่ลืมปรับปรุงคำตอบ อัปเดตทันทีขอบคุณสำหรับการเฮด
Andriy M

@ เลนนาร์ท: สำหรับประสิทธิภาพฉันอาจมีความเข้าใจผิดเกี่ยวกับข้อความค้นหาประเภทนี้ ฉันคิดว่ามันควรจะทำงานได้อย่างมีประสิทธิภาพเพราะการคำนวณจะเสร็จสมบูรณ์ในหนึ่งรอบผ่านตาราง บางทีนั่นอาจไม่ใช่กรณีที่มีตารางที่ได้รับโดยเฉพาะอย่างยิ่งตารางที่ใช้การรวม ฉันกลัวว่าฉันไม่มีการติดตั้ง MySQL ที่เหมาะสมและไม่มีความเชี่ยวชาญเพียงพอในการวิเคราะห์เชิงลึก
Andriy M

@Andriy_M ดูเหมือนว่าจะเป็นข้อบกพร่องในรุ่น MariaDB ของฉัน มันไม่ชอบแต่ยอมรับGROUP BY FLOOR(reason_weight / 10) GROUP BY reason_weightสำหรับประสิทธิภาพฉันไม่ได้เป็นผู้เชี่ยวชาญอย่างแน่นอนเมื่อพูดถึง MySQL มันเป็นเพียงการสังเกตจากเครื่องเส็งเคร็งของฉัน เนื่องจากฉันเรียกใช้แบบสอบถามก่อนข้อมูลทั้งหมดควรถูกแคชไว้แล้วดังนั้นฉันจึงไม่รู้ว่าทำไมจึงช้าลงในครั้งแรกที่เรียกใช้
Lennart
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.