ดัชนีสำหรับเคียวรี SQL ที่มีเงื่อนไข WHERE และ GROUP BY


15

ฉันพยายามที่จะกำหนดดัชนีที่จะใช้สำหรับแบบสอบถาม SQL ที่มีWHEREเงื่อนไขและGROUP BYที่กำลังทำงานช้ามาก

คำค้นหาของฉัน:

SELECT group_id
FROM counter
WHERE ts between timestamp '2014-03-02 00:00:00.0' and timestamp '2014-03-05 12:00:00.0'
GROUP BY group_id

ปัจจุบันตารางมี 32.000.000 แถว เวลาดำเนินการของแบบสอบถามเพิ่มขึ้นมากเมื่อฉันเพิ่มกรอบเวลา

ตารางที่สงสัยจะเป็นดังนี้:

CREATE TABLE counter (
    id bigserial PRIMARY KEY
  , ts timestamp NOT NULL
  , group_id bigint NOT NULL
);

ขณะนี้ฉันมีดัชนีต่อไปนี้ แต่ประสิทธิภาพยังคงช้า:

CREATE INDEX ts_index
  ON counter
  USING btree
  (ts);

CREATE INDEX group_id_index
  ON counter
  USING btree
  (group_id);

CREATE INDEX comp_1_index
  ON counter
  USING btree
  (ts, group_id);

CREATE INDEX comp_2_index
  ON counter
  USING btree
  (group_id, ts);

การใช้งานอธิบายในแบบสอบถามให้ผลลัพธ์ดังต่อไปนี้

"QUERY PLAN"
"HashAggregate  (cost=467958.16..467958.17 rows=1 width=4)"
"  ->  Index Scan using ts_index on counter  (cost=0.56..467470.93 rows=194892 width=4)"
"        Index Cond: ((ts >= '2014-02-26 00:00:00'::timestamp without time zone) AND (ts <= '2014-02-27 23:59:00'::timestamp without time zone))"

SQL Fiddle พร้อมข้อมูลตัวอย่าง: http://sqlfiddle.com/#!15/7492b/1

คำถาม

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

แก้ไข 1

ใช้ PostgreSQL รุ่น 9.3.2

แก้ไข 2

ฉันลองข้อเสนอของ @Erwin กับEXISTS:

SELECT group_id
FROM   groups g
WHERE  EXISTS (
   SELECT 1
   FROM   counter c
   WHERE  c.group_id = g.group_id
   AND    ts BETWEEN timestamp '2014-03-02 00:00:00'
                 AND timestamp '2014-03-05 12:00:00'
   );

แต่น่าเสียดายที่เรื่องนี้ดูเหมือนจะไม่เพิ่มประสิทธิภาพ แผนแบบสอบถาม:

"QUERY PLAN"
"Nested Loop Semi Join  (cost=1607.18..371680.60 rows=113 width=4)"
"  ->  Seq Scan on groups g  (cost=0.00..2.33 rows=133 width=4)"
"  ->  Bitmap Heap Scan on counter c  (cost=1607.18..158895.53 rows=60641 width=4)"
"        Recheck Cond: ((group_id = g.id) AND (ts >= '2014-01-01 00:00:00'::timestamp without time zone) AND (ts <= '2014-03-05 12:00:00'::timestamp without time zone))"
"        ->  Bitmap Index Scan on comp_2_index  (cost=0.00..1592.02 rows=60641 width=0)"
"              Index Cond: ((group_id = g.id) AND (ts >= '2014-01-01 00:00:00'::timestamp without time zone) AND (ts <= '2014-03-05 12:00:00'::timestamp without time zone))"

แก้ไข 3

แผนแบบสอบถามสำหรับแบบสอบถาม LATERAL จาก ypercube:

"QUERY PLAN"
"Nested Loop  (cost=8.98..1200.42 rows=133 width=20)"
"  ->  Seq Scan on groups g  (cost=0.00..2.33 rows=133 width=4)"
"  ->  Result  (cost=8.98..8.99 rows=1 width=0)"
"        One-Time Filter: ($1 IS NOT NULL)"
"        InitPlan 1 (returns $1)"
"          ->  Limit  (cost=0.56..4.49 rows=1 width=8)"
"                ->  Index Only Scan using comp_2_index on counter c  (cost=0.56..1098691.21 rows=279808 width=8)"
"                      Index Cond: ((group_id = $0) AND (ts IS NOT NULL) AND (ts >= '2010-03-02 00:00:00'::timestamp without time zone) AND (ts <= '2014-03-05 12:00:00'::timestamp without time zone))"
"        InitPlan 2 (returns $2)"
"          ->  Limit  (cost=0.56..4.49 rows=1 width=8)"
"                ->  Index Only Scan Backward using comp_2_index on counter c_1  (cost=0.56..1098691.21 rows=279808 width=8)"
"                      Index Cond: ((group_id = $0) AND (ts IS NOT NULL) AND (ts >= '2010-03-02 00:00:00'::timestamp without time zone) AND (ts <= '2014-03-05 12:00:00'::timestamp without time zone))"

group_idบนโต๊ะมีค่าต่างกันกี่ค่า?
ypercubeᵀᴹ

มีกลุ่มที่แตกต่างกัน 133 ของ

การประทับเวลาอยู่ในช่วง 2011 ถึง 2014 มีการใช้งานทั้งวินาทีและมิลลิวินาที

คุณสนใจgroup_idและไม่นับเท่านั้นหรือไม่?
Erwin Brandstetter

@Erwin เราสนใจ max () และ (min) และในคอลัมน์ที่สี่ที่ไม่แสดงในตัวอย่าง
uldall

คำตอบ:


6

อีกแนวคิดหนึ่งที่ใช้groupsตารางและสิ่งก่อสร้างที่เรียกว่าการLATERALเข้าร่วม (สำหรับแฟน ๆ ของ SQL-Server สิ่งนี้เกือบจะเหมือนกันOUTER APPLY) มันมีข้อดีที่สามารถคำนวณมวลรวมได้ในแบบสอบถามย่อย:

SELECT group_id, min_ts, max_ts
FROM   groups g,                    -- notice the comma here, is required
  LATERAL 
       ( SELECT MIN(ts) AS min_ts,
                MAX(ts) AS max_ts
         FROM counter c
         WHERE c.group_id = g.group_id
           AND c.ts BETWEEN timestamp '2011-03-02 00:00:00'
                        AND timestamp '2013-03-05 12:00:00'
       ) x 
WHERE min_ts IS NOT NULL ;

ทดสอบที่SQL-Fiddleแสดงให้เห็นว่าแบบสอบถามทำการสแกนดัชนีใน(group_id, ts)ดัชนีหรือไม่

แผนการที่คล้ายกันถูกสร้างขึ้นโดยใช้การรวม 2 ด้านข้างหนึ่งครั้งสำหรับหนึ่งนาทีและหนึ่งแผนสำหรับสูงสุดและยังมีแบบสอบถามย่อยที่สัมพันธ์กันแบบอินไลน์ 2 ชุด พวกเขาสามารถใช้หากคุณต้องการแสดงทั้งcounterแถวนอกเหนือจากวันที่ min และ max:

SELECT group_id, 
       min_ts, min_ts_id, 
       max_ts, max_ts_id 
FROM   groups g
  , LATERAL 
       ( SELECT ts AS min_ts, c.id AS min_ts_id
         FROM counter c
         WHERE c.group_id = g.group_id
           AND c.ts BETWEEN timestamp '2012-03-02 00:00:00'
                        AND timestamp '2014-03-05 12:00:00'
         ORDER BY ts ASC
         LIMIT 1
       ) xmin
  , LATERAL 
       ( SELECT ts AS max_ts, c.id AS max_ts_id
         FROM counter c
         WHERE c.group_id = g.group_id
           AND c.ts BETWEEN timestamp '2012-03-02 00:00:00'
                        AND timestamp '2014-03-05 12:00:00'
         ORDER BY ts DESC 
         LIMIT 1
       ) xmax
WHERE min_ts IS NOT NULL ;

@ypercube ฉันได้เพิ่มแผนแบบสอบถามสำหรับแบบสอบถามของคุณในคำถามเดิม แบบสอบถามทำงานในเวลาไม่เกิน 50 มิลลิวินาทีแม้ในช่วงเวลาที่มีขนาดใหญ่
uldall

5

เนื่องจากคุณไม่มีการรวมอยู่ในรายการที่เลือกแล้วgroup byมันก็เหมือนกับการใส่distinctในรายการที่เลือกใช่ไหม

ถ้านั่นคือสิ่งที่คุณต้องการคุณอาจจะสามารถที่จะได้รับการค้นหาดัชนีรวดเร็วใน comp_2_index โดยการเขียนใหม่นี้เพื่อใช้แบบสอบถาม recursive ตามที่อธิบายไว้ในวิกิพีเดีย PostgreSQL

สร้างมุมมองเพื่อส่งคืน group_ids ที่แตกต่างกันได้อย่างมีประสิทธิภาพ:

create or replace view groups as
WITH RECURSIVE t AS (
             SELECT min(counter.group_id) AS group_id
               FROM counter
    UNION ALL
             SELECT ( SELECT min(counter.group_id) AS min
                       FROM counter
                      WHERE counter.group_id > t.group_id) AS min
               FROM t
              WHERE t.group_id IS NOT NULL
    )
     SELECT t.group_id
       FROM t
      WHERE t.group_id IS NOT NULL
UNION ALL
     SELECT NULL::bigint AS col
      WHERE (EXISTS ( SELECT counter.id,
                counter.ts,
                counter.group_id
               FROM counter
              WHERE counter.group_id IS NULL));

จากนั้นใช้มุมมองนั้นแทนตารางการค้นหาในการรวมexistsกึ่งกลางของ Erwin


4

เนื่องจากมีเพียง133 different group_id'sคุณเท่านั้นที่สามารถใช้integer(หรือแม้กระทั่งsmallint) สำหรับ group_id มันจะไม่ซื้อคุณมากนักเนื่องจากการขยายไปถึง 8 ไบต์จะกินที่เหลือในตารางของคุณและดัชนีหลายคอลัมน์ที่เป็นไปได้ integerแม้ว่าการประมวลผลของที่ราบควรจะเร็วขึ้นเล็กน้อย เพิ่มเติมเกี่ยวกับintint2

CREATE TABLE counter (
    id bigserial PRIMARY KEY
  , ts timestamp NOT NULL
  , group_id int NOT NULL
);

@Leo: การประทับเวลาจะถูกเก็บไว้เป็นจำนวนเต็ม 8 ไบต์ในการติดตั้งที่ทันสมัยและสามารถประมวลผลได้อย่างรวดเร็วสมบูรณ์แบบ รายละเอียด

@ypercube: ดัชนีบน(group_id, ts)ไม่สามารถช่วยได้เนื่องจากไม่มีเงื่อนไขgroup_idในการสืบค้น

ปัญหาหลักของคุณคือข้อมูลจำนวนมหาศาลที่ต้องดำเนินการ:

การสแกนดัชนีโดยใช้ ts_index บนเคาน์เตอร์ (ราคา = 0.56..467470.93 แถว = 194892 width = 4)

ฉันเห็นว่าคุณสนใจเพียงแค่การมีอยู่ของgroup_idและไม่นับจริง นอกจากนี้ยังมีเพียง 133 ที่แตกต่างกันgroup_idของ ดังนั้นการค้นหาของคุณสามารถพอใจกับการเข้าชมครั้งแรกgorup_idในกรอบเวลา ดังนั้นคำแนะนำนี้สำหรับการค้นหาทางเลือกที่มีการEXISTSเข้าร่วมกึ่ง :

สมมติว่าตารางการค้นหาสำหรับกลุ่ม:

SELECT group_id
FROM   groups g
WHERE  EXISTS (
   SELECT 1
   FROM   counter c
   WHERE  c.group_id = g.group_id
   AND    ts BETWEEN timestamp '2014-03-02 00:00:00'
                 AND timestamp '2014-03-05 12:00:00'
   );

ดัชนีของคุณcomp_2_indexได้ที่(group_id, ts)จะกลายเป็นเครื่องมือในขณะนี้

SQL Fiddle (การสร้างซอที่มาจาก @ypercube ในความคิดเห็น)

ที่นี่แบบสอบถามต้องการดัชนี(ts, group_id)มากกว่า แต่ฉันคิดว่านั่นเป็นเพราะการตั้งค่าการทดสอบด้วยการประทับเวลา "แบบกลุ่ม" หากคุณลบดัชนีชั้นนำts( เพิ่มเติมเกี่ยวกับที่ ), การวางแผนอย่างมีความสุขจะใช้ดัชนีบน(group_id, ts)เช่นกัน - สะดุดตาใน เฉพาะดัชนีการสแกน

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

กรอบเวลาในแบบสอบถามของคุณเป็นไปตามอำเภอใจหรือไม่? หรือส่วนใหญ่เป็นนาทีเต็ม / ชั่วโมง / วัน?

CREATE MATERIALIZED VIEW counter_mv AS
SELECT date_trunc('hour', ts) AS hour
     , group_id
     , count(*) AS ct
GROUP BY 1,2
ORDER BY 1,2;

สร้างดัชนีที่จำเป็นบนcounter_mvและปรับการค้นหาของคุณให้ทำงานกับมัน ...


1
ฉันลองสิ่งที่คล้ายกันหลายอย่างในSQL-Fiddleกับแถว 10k แต่ทั้งหมดแสดงการสแกนตามลำดับ การใช้groupsตารางสร้างความแตกต่างหรือไม่
ypercubeᵀᴹ

@ypercube: ฉันคิดอย่างนั้น นอกจากนี้ยังANALYZEสร้างความแตกต่าง แต่ดัชนีcounterยังสามารถใช้ได้โดยไม่ต้องANALYZEทันทีที่ฉันแนะนำgroupsตาราง Point คือหากไม่มีตารางนั้นจำเป็นต้องใช้ seqscan เพื่อสร้างชุด group_id´s ที่เป็นไปได้ ฉันเพิ่มมากขึ้นในคำตอบของฉัน และขอบคุณสำหรับซอของคุณ!
Erwin Brandstetter

มันแปลกมาก คุณบอกว่าเครื่องมือเพิ่มประสิทธิภาพของ Postgres จะไม่ใช้ดัชนีในgroup_idการSELECT DISTINCT group_id FROM t;ค้นหาหรือไม่
ypercubeᵀᴹ

1
@ErwinBrandstetter นั่นคือสิ่งที่ฉันคิดเช่นกันและรู้สึกประหลาดใจมากที่ค้นพบวิธีอื่น หากไม่มี a LIMIT 1ก็สามารถเลือกสแกนดัชนีบิตแมปซึ่งไม่ได้รับประโยชน์จากการหยุดก่อนและใช้เวลานานขึ้น (แต่ถ้าตารางสูญญากาศใหม่อาจต้องสแกนแบบดัชนีเหนือการสแกนบิตแมปดังนั้นพฤติกรรมที่คุณเห็นขึ้นอยู่กับสถานะสูญญากาศของตาราง)
jjanes

1
@uldall: การรวมรายวันจะลดจำนวนแถวลงอย่างมาก นั่นควรทำเคล็ดลับ แต่ให้แน่ใจว่าได้ลอง EXISTS-query มันอาจจะเร็วอย่างน่าประหลาดใจ จะไม่ทำงานเป็นนาที / สูงสุดเพิ่มเติม ฉันจะให้ความสนใจกับประสิทธิภาพที่เกิดขึ้นแม้ว่าคุณจะมีความคิดเห็นที่ดี
Erwin Brandstetter
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.