วิธีทำให้ DISTINCT ON เร็วขึ้นใน PostgreSQL


13

ฉันมีตารางstation_logsในฐานข้อมูล PostgreSQL 9.6:

    Column     |            Type             |    
---------------+-----------------------------+
 id            | bigint                      | bigserial
 station_id    | integer                     | not null
 submitted_at  | timestamp without time zone | 
 level_sensor  | double precision            | 
Indexes:
    "station_logs_pkey" PRIMARY KEY, btree (id)
    "uniq_sid_sat" UNIQUE CONSTRAINT, btree (station_id, submitted_at)

ฉันพยายามที่จะได้รับที่ผ่านมาlevel_sensorค่าขึ้นอยู่กับแต่ละsubmitted_at station_idมีประมาณ 400 ที่ไม่ซ้ำกันมีstation_idค่านิยมและรอบ 20k station_idแถวต่อวันต่อ

ก่อนสร้างดัชนี:

EXPLAIN ANALYZE
SELECT DISTINCT ON(station_id) station_id, submitted_at, level_sensor
FROM station_logs ORDER BY station_id, submitted_at DESC;
 ไม่ซ้ำกัน (ราคา = 4347852.14..4450301.72 แถว = 89 กว้าง = 20) (เวลาจริง = 22202.080..27619.167 แถว = 98 ลูป = 1)
   -> เรียงลำดับ (ราคา = 4347852.14..4399076.93 แถว = 20489916 กว้าง = 20) (เวลาจริง = 22202.077..26540.827 แถว = 20489812 ลูป = 1)
         คีย์การเรียง: station_id, submitted_at DESC
         วิธีการเรียงลำดับ: ผสานภายนอกดิสก์: 681040kB
         -> Seq สแกนบน station_logs (ราคา = 0.00..598895.16 แถว = 20489916 กว้าง = 20) (เวลาจริง = 0.023..3443.587 แถว = 20489812 ลูป = $
 เวลาในการวางแผน: 0.072 ms
 เวลาดำเนินการ: 27690.644 ms

สร้างดัชนี:

CREATE INDEX station_id__submitted_at ON station_logs(station_id, submitted_at DESC);

หลังจากสร้างดัชนีสำหรับแบบสอบถามเดียวกัน:

 ไม่ซ้ำกัน (ราคา = 0.56..2156367.51 แถว = 89 กว้าง = 20) (เวลาจริง = 0.184..16263.413 แถว = 98 ลูป = 1)
   -> การสแกนดัชนีโดยใช้ station_id__submitted_at บน station_logs (ราคา = 0.56..2105142.98 แถว = 20489812 กว้าง = 20) (เวลาจริง = 0.181..1 $
 เวลาในการวางแผน: 0.206 ms
 เวลาดำเนินการ: 16263.490 ms

มีวิธีทำให้แบบสอบถามนี้เร็วขึ้นหรือไม่ เช่น 1 วินาทีเช่น 16 วินาทียังคงมากเกินไป


2
มีรหัสสถานีที่แตกต่างกันจำนวนเท่าใดเช่นคิวรีส่งคืนจำนวนแถวกี่แถว แล้ว Postgres รุ่นใด
ypercubeᵀᴹ

Postgre 9.6, 400 station_id ที่ไม่ซ้ำกันและบันทึก 20k ต่อวันต่อ station_id
Kokizzu

แบบสอบถามนี้ส่งกลับ"ค่า level_sensor สุดท้ายขึ้นอยู่กับ submitted_at สำหรับแต่ละ station_id" DISTINCT ON เกี่ยวข้องกับการสุ่มเลือกยกเว้นในกรณีที่คุณไม่ต้องการ
philipxy

คำตอบ:


18

สำหรับ 400 สถานีเท่านั้นการค้นหานี้จะเร็วขึ้นอย่างมาก :

SELECT s.station_id, l.submitted_at, l.level_sensor
FROM   station s
CROSS  JOIN LATERAL (
   SELECT submitted_at, level_sensor
   FROM   station_logs
   WHERE  station_id = s.station_id
   ORDER  BY submitted_at DESC NULLS LAST
   LIMIT  1
   ) l;

dbfiddle ที่นี่
(เปรียบเทียบแผนสำหรับแบบสอบถามนี้ทางเลือกของ Abelisto กับต้นฉบับของคุณ)

ผลEXPLAIN ANALYZEที่ได้รับจาก OP:

 วนซ้ำ (ต้นทุน = 0.56..356.65 แถว = ความกว้าง 102 = 20) (เวลาจริง = 0.034..0.979 แถว = 98 ลูป = 1)
   -> Seq สแกนสถานี s (ราคา = 0.00..3.02 แถว = 102 กว้าง = 4) (เวลาจริง = 0.009..0.016 แถว = 102 ลูป = 1)
   -> จำกัด (ค่า = 0.56..3.45 แถว = 1 ความกว้าง = 16) (เวลาจริง = 0.009..0.009 แถว = 1 ห่วง = 102)
         -> การสแกนดัชนีโดยใช้ station_id__submitted_at บน station_logs (ราคา = 0.56..664062.38 แถว = 230223 กว้าง = 16) (เวลาจริง = 0.009 $
               ดัชนี Cond: (station_id = s.id)
 เวลาในการวางแผน: 0.542 ms
 เวลาดำเนินการ: 1.013 ms   - !!

เพียงดัชนีstation_id__submitted_atที่คุณต้องมีหนึ่งคนที่คุณสร้าง: UNIQUEจำกัดuniq_sid_satยังไม่ทำงานโดยทั่วไป การบำรุงรักษาทั้งคู่ดูเหมือนว่าเป็นการสิ้นเปลืองพื้นที่ดิสก์และประสิทธิภาพการเขียน

ฉันจะเพิ่มNULLS LASTไปORDER BYในแบบสอบถามเพราะไม่ได้กำหนดไว้submitted_at NOT NULLเป็นการดีถ้ามีให้เพิ่มNOT NULLข้อ จำกัด ในคอลัมน์submitted_atให้ลบดัชนีเพิ่มเติมและลบออกNULLS LASTจากแบบสอบถาม

หากsubmitted_atทำได้ให้NULLสร้างUNIQUEดัชนีนี้เพื่อแทนที่ทั้งดัชนีปัจจุบันและข้อ จำกัด ที่ไม่ซ้ำกัน:

CREATE UNIQUE INDEX station_logs_uni ON station_logs(station_id, submitted_at DESC NULLS LAST);

พิจารณา:

นี่คือสมมติว่าตารางแยกต่างหากที่stationมีหนึ่งแถวต่อเกี่ยวข้องstation_id(โดยทั่วไปคือ PK) - ซึ่งคุณควรมีวิธีใดวิธีหนึ่ง ถ้าคุณไม่มีมันสร้างมันขึ้นมา อีกครั้งอย่างรวดเร็วด้วยเทคนิค rCTE นี้:

CREATE TABLE station AS
WITH RECURSIVE cte AS (
   (
   SELECT station_id
   FROM   station_logs
   ORDER  BY station_id
   LIMIT  1
   )
   UNION ALL
   SELECT l.station_id
   FROM   cte c
   ,      LATERAL (   
      SELECT station_id
      FROM   station_logs
      WHERE  station_id > c.station_id
      ORDER  BY station_id
      LIMIT  1
      ) l
   )
TABLE cte;

ฉันใช้มันในซอเช่นกัน คุณสามารถใช้แบบสอบถามที่คล้ายกันเพื่อแก้ไขงานของคุณโดยตรงโดยไม่ต้องมีstationตาราง - หากคุณไม่สามารถสร้างมันได้

คำแนะนำโดยละเอียดคำอธิบายและทางเลือก:

ปรับดัชนีให้เหมาะสม

คำถามของคุณน่าจะเร็วมาก เฉพาะในกรณีที่คุณยังต้องการเพิ่มประสิทธิภาพการอ่าน ...

มันอาจจะทำให้ความรู้สึกที่จะเพิ่มlevel_sensorเป็นคอลัมน์สุดท้ายเพื่อจัดทำดัชนีเพื่อให้การสแกนดัชนีเท่านั้นเช่นความเห็น joanolo
คอนดิชั่น:มันทำให้ดัชนีมีขนาดใหญ่ขึ้นซึ่งเพิ่มค่าใช้จ่ายเล็กน้อยสำหรับการสืบค้นทั้งหมดที่ใช้
Pro:หากคุณได้รับการสแกนดัชนีเท่านั้นจริง ๆ แล้วแบบสอบถามในมือไม่จำเป็นต้องไปที่หน้าฮีปเลยซึ่งทำให้เร็วขึ้นเป็นสองเท่า แต่นั่นอาจเป็นกำไรที่ไร้สาระสำหรับการค้นหาที่รวดเร็วมากในขณะนี้

อย่างไรก็ตามฉันไม่คาดหวังว่าจะทำงานให้กับกรณีของคุณ คุณพูดถึง:

... รอบ 20k station_idแถวต่อวันต่อ

โดยทั่วไปแล้วจะระบุการโหลดการเขียนที่ไม่หยุดหย่อน (1 ต่อstation_idทุก 5 วินาที) และคุณมีความสนใจในแถวล่าสุด การสแกนแบบดัชนีเท่านั้นทำงานได้เฉพาะกับเพจฮีพที่สามารถเห็นได้กับธุรกรรมทั้งหมด (บิตในแผนที่การมองเห็นถูกตั้งค่าไว้) คุณจะต้องเรียกใช้การVACUUMตั้งค่าที่ก้าวร้าวอย่างมากสำหรับตารางเพื่อให้ทันกับการโหลดการเขียนและมันจะยังไม่ทำงานส่วนใหญ่ หากสมมติฐานของฉันถูกต้องการสแกนเฉพาะดัชนีจะไม่เพิ่มลงlevel_sensorในดัชนี

OTOH ถ้าสมมติฐานของฉันค้างไว้และตารางของคุณมีการเจริญเติบโตที่ใหญ่มากเป็นดัชนี Brinอาจช่วย ที่เกี่ยวข้อง:

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

CREATE INDEX station_id__submitted_at_recent_idx ON station_logs(station_id, submitted_at DESC NULLS LAST)
WHERE submitted_at > '2017-06-24 00:00';

เลือกเวลาประทับที่คุณรู้ว่าต้องมีแถวที่อายุน้อยกว่า คุณต้องเพิ่มWHEREเงื่อนไขการจับคู่ให้กับแบบสอบถามทั้งหมดเช่น:

...
WHERE  station_id = s.station_id
AND    submitted_at > '2017-06-24 00:00'
...

คุณต้องปรับดัชนีและแบบสอบถามเป็นครั้งคราว
คำตอบที่เกี่ยวข้องพร้อมรายละเอียดเพิ่มเติม:


เมื่อใดก็ตามที่ฉันรู้ว่าฉันต้องการวนซ้ำซ้อน (บ่อยครั้ง) การใช้ LATERAL เป็นการเพิ่มประสิทธิภาพสำหรับสถานการณ์ต่างๆ
พอลเดรเปอร์

6

ลองด้วยวิธีคลาสสิก:

create index idx_station_logs__station_id on station_logs(station_id);
create index idx_station_logs__submitted_at on station_logs(submitted_at);

analyse station_logs;

with t as (
  select station_id, max(submitted_at) submitted_at 
  from station_logs 
  group by station_id)
select * 
from t join station_logs l on (
  l.station_id = t.station_id and l.submitted_at = t.submitted_at);

dbfiddle

อธิบายการวิเคราะห์โดย ThreadStarter

 Nested Loop  (cost=701344.63..702110.58 rows=4 width=155) (actual time=6253.062..6253.544 rows=98 loops=1)
   CTE t
     ->  HashAggregate  (cost=701343.18..701344.07 rows=89 width=12) (actual time=6253.042..6253.069 rows=98 loops=1)
           Group Key: station_logs.station_id
           ->  Seq Scan on station_logs  (cost=0.00..598894.12 rows=20489812 width=12) (actual time=0.034..1841.848 rows=20489812 loop$
   ->  CTE Scan on t  (cost=0.00..1.78 rows=89 width=12) (actual time=6253.047..6253.085 rows=98 loops=1)
   ->  Index Scan using station_id__submitted_at on station_logs l  (cost=0.56..8.58 rows=1 width=143) (actual time=0.004..0.004 rows=$
         Index Cond: ((station_id = t.station_id) AND (submitted_at = t.submitted_at))
 Planning time: 0.542 ms
 Execution time: 6253.701 ms
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.