Indeks untuk query SQL dengan kondisi WHERE dan GROUP BY

15

Saya mencoba menentukan indeks mana yang akan digunakan untuk permintaan SQL dengan WHEREkondisi dan GROUP BYyang saat ini berjalan sangat lambat.

Permintaan saya:

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

Tabel saat ini memiliki 32.000.000 baris. Waktu eksekusi permintaan meningkat banyak ketika saya meningkatkan kerangka waktu.

Tabel tersebut terlihat seperti ini:

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

Saat ini saya memiliki indeks berikut, tetapi kinerjanya masih lambat:

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

Menjalankan EXPLAIN pada kueri memberikan hasil berikut:

"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 dengan contoh data: http://sqlfiddle.com/#!15/7492b/1

Pertanyaan

Dapatkah kinerja kueri ini ditingkatkan dengan menambahkan indeks yang lebih baik, atau haruskah saya meningkatkan kekuatan pemrosesan?

Edit 1

PostgreSQL versi 9.3.2 digunakan.

Edit 2

Saya mencoba proposal @Erwin dengan 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'
   );

Namun sayangnya ini tampaknya tidak meningkatkan kinerja. Rencana Kueri:

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

Edit 3

Paket permintaan untuk permintaan LATERAL dari 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))"
uldall
sumber
Berapa banyak group_idnilai berbeda yang ada di atas meja?
ypercubeᵀᴹ
Ada 133 group_id yang berbeda.
Stempel waktu berkisar dari 2011 hingga 2014. Detik dan milidetik digunakan.
Apakah Anda hanya tertarik group_iddan tidak masuk hitungan?
Erwin Brandstetter
@ Erwin Kami tertarik pada max () dan (min) juga pada kolom keempat yang tidak ditampilkan dalam contoh.
uldall

Jawaban:

6

Gagasan lain, yang juga menggunakan groupstabel dan konstruksi yang disebut LATERALjoin (untuk penggemar SQL-Server, ini hampir identik dengan OUTER APPLY). Ini memiliki keuntungan bahwa agregat dapat dihitung dalam subquery:

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 ;

Tes di SQL-Fiddle menunjukkan bahwa kueri melakukan pemindaian indeks pada (group_id, ts)indeks.

Rencana serupa diproduksi menggunakan 2 gabungan lateral, satu untuk min dan satu untuk maks dan juga dengan 2 subquery berkorelasi sebaris. Mereka juga dapat digunakan jika Anda perlu menunjukkan seluruh counterbaris selain tanggal min dan maks:

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ᵀᴹ
sumber
@ ypercube Saya menambahkan paket permintaan untuk pertanyaan Anda ke pertanyaan awal. Kueri berjalan di bawah 50 ms bahkan pada rentang waktu yang besar.
uldall
5

Karena Anda tidak memiliki agregat dalam daftar pilih, maka group byhampir sama dengan menempatkan a distinctdalam daftar pilih, kan?

Jika itu yang Anda inginkan, Anda mungkin bisa mendapatkan pencarian indeks cepat di comp_2_index dengan menulis ulang ini untuk menggunakan permintaan rekursif, seperti yang dijelaskan pada wiki PostgreSQL .

Buat tampilan untuk secara efisien mengembalikan group_ids yang berbeda:

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

Dan kemudian gunakan tampilan itu di tempat tabel pencarian di existssemi-join Erwin .

jjanes
sumber
4

Karena hanya ada 133 different group_id's, Anda dapat menggunakan integer(atau bahkan smallint) untuk group_id. Ini tidak akan membeli banyak, karena padding hingga 8 byte akan memakan sisanya di meja Anda dan kemungkinan indeks multikolom. Pemrosesan polos integerharus sedikit lebih cepat. Lebih lanjut tentang intvsint2 .

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

@ Leo: cap waktu disimpan sebagai bilangan bulat 8-byte dalam instalasi modern dan dapat diproses dengan sangat cepat. Detail

@ ypercube: Indeks aktif (group_id, ts)tidak dapat membantu, karena tidak ada kondisi pada group_idkueri.

Masalah utama Anda adalah banyaknya data yang harus diproses:

Pindaian indeks menggunakan ts_index on counter (biaya = 0,56.,467470,93 baris = 194892 lebar = 4)

Saya melihat Anda hanya tertarik pada keberadaan group_id, dan tidak ada hitungan yang sebenarnya. Juga, hanya ada 133 group_ids berbeda . Karenanya, kueri Anda dapat dipenuhi dengan klik pertama per gorup_iddalam kerangka waktu. Karenanya saran ini untuk kueri alternatif dengan EXISTSsemi-gabung :

Dengan asumsi tabel pencarian untuk grup:

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

Indeks Anda comp_2_indexdi (group_id, ts)menjadi instrumen sekarang.

SQL Fiddle (membangun biola yang disediakan oleh @ypercube di komentar)

Di sini, kueri lebih memilih indeks aktif (ts, group_id) , tapi saya pikir itu karena pengaturan tes dengan cap waktu "berkerumun". Jika Anda menghapus indeks dengan memimpin ts( lebih lanjut tentang itu ), perencana dengan senang hati akan menggunakan indeks (group_id, ts)juga - terutama dalam Pemindaian Indeks Saja .

Jika berhasil, Anda mungkin tidak memerlukan peningkatan lain yang mungkin: data pra-agregat dalam tampilan terwujud untuk secara drastis mengurangi jumlah baris. Ini akan masuk akal secara khusus, jika Anda juga membutuhkan penghitungan aktual tambahan. Maka Anda memiliki biaya untuk memproses banyak baris sekaligus saat memperbarui mv. Anda bahkan dapat menggabungkan agregat harian dan per jam (dua tabel terpisah) dan menyesuaikan kueri Anda dengan itu.

Apakah kerangka waktu dalam kueri Anda sewenang-wenang? Atau sebagian besar pada menit / jam / hari penuh?

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;

Buat indeks yang diperlukan pada counter_mvdan sesuaikan kueri Anda untuk bekerja dengannya ...

Erwin Brandstetter
sumber
1
Saya mencoba beberapa hal serupa di SQL-Fiddle , dengan baris 10k, tetapi semua menunjukkan beberapa pemindaian berurutan. Apakah menggunakan groupstabel membuat perbedaan?
ypercubeᵀᴹ
@ ypercube: Saya kira begitu. Juga, ANALYZEmembuat perbedaan. Tetapi indeks pada counterbahkan digunakan tanpa ANALYZEsegera setelah saya memperkenalkan groupstabel. Intinya adalah, tanpa tabel itu, seqscan tetap diperlukan untuk membangun set group_id yang mungkin. Saya menambahkan lebih banyak ke jawaban saya. Dan terima kasih untuk biola Anda!
Erwin Brandstetter
Itu aneh. Anda mengatakan bahwa pengoptimal Postgres tidak akan menggunakan indeks group_idbahkan untuk SELECT DISTINCT group_id FROM t;permintaan?
ypercubeᵀᴹ
1
@ ErwinBrandstetter Itulah yang saya pikirkan juga, dan sangat terkejut mengetahui sebaliknya. Tanpa a LIMIT 1, ia dapat memilih pemindaian indeks bitmap, yang tidak mendapat manfaat dari penghentian dini dan membutuhkan waktu lebih lama. (Tetapi jika tabel tersebut baru disedot, mungkin lebih suka pemindaian indeks saja dari pemindaian bitmap, jadi perilaku mana yang Anda lihat tergantung pada status vakum dari tabel).
jjanes
1
@uldall: Agregat harian akan secara drastis mengurangi jumlah baris. Itu harus melakukan trik. Tapi pastikan untuk mencoba EXISTS-query. Mungkin sangat cepat. Tidak akan bekerja untuk min / maks tambahan. Saya akan tertarik dengan kinerja yang dihasilkan, jika Anda mau berbaris di sini.
Erwin Brandstetter