Bagaimana membuat DISTINCT ON lebih cepat di PostgreSQL?

13

Saya punya tabel station_logsdi database 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)

Saya mencoba untuk mendapatkan nilai terakhir level_sensorberdasarkan submitted_at, untuk masing-masing station_id. Ada sekitar 400 station_idnilai unik , dan sekitar 20rb baris per hari per station_id.

Sebelum membuat indeks:

EXPLAIN ANALYZE
SELECT DISTINCT ON(station_id) station_id, submitted_at, level_sensor
FROM station_logs ORDER BY station_id, submitted_at DESC;
 Unik (biaya = 4347852.14..4450301.72 baris = 89 lebar = 20) (waktu aktual = 22202.080..27619.167 baris = 98 loop = 1)
   -> Sortir (biaya = 4347852.14..4399076.93 baris = 20489916 lebar = 20) (waktu aktual = 22202.077..26540.827 baris = 20489812 loop = 1)
         Sortir Key: station_id, dikirimkan_at DESC
         Metode Sortir: disk gabungan eksternal: 681040kB
         -> Pemindaian Seq pada station_logs (biaya = 0,00..598895.16 baris = 20489916 lebar = 20) (waktu aktual = 0,023..3443,587 baris = 20489812 loop = $
 Waktu perencanaan: 0,072 ms
 Waktu pelaksanaan: 27690.644 ms

Membuat indeks:

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

Setelah membuat indeks, untuk permintaan yang sama:

 Unik (biaya = 0,56..2156367,51 baris = 89 lebar = 20) (waktu aktual = 0,184..16263,413 baris = 98 loop = 1)
   -> Pemindaian Indeks menggunakan station_id__submitted_at pada station_logs (biaya = 0,56..2105142,98 baris = 20489812 lebar = 20) (waktu aktual = 0,181..1 $
 Waktu perencanaan: 0,206 ms
 Waktu pelaksanaan: 16263.490 ms

Apakah ada cara untuk membuat kueri ini lebih cepat? Seperti 1 detik misalnya, 16 detik masih terlalu banyak.

Kokizzu
sumber
2
Berapa banyak id stasiun yang berbeda, yaitu berapa banyak baris yang dikembalikan oleh kueri? Dan versi Postgres apa?
ypercubeᵀᴹ
Postgre 9.6, sekitar 400 unik station_id, dan sekitar 20k catatan per hari per station_id
Kokizzu,
Query ini mengembalikan sebuah "nilai level_sensor terakhir berdasarkan submitted_at, untuk setiap station_id". DISTINCT ON melibatkan pilihan acak kecuali dalam kasus di mana Anda tidak membutuhkannya.
philipxy

Jawaban:

18

Untuk hanya 400 stasiun, query ini akan menjadi besar-besaran lebih cepat:

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 di sini
(membandingkan paket untuk kueri ini, alternatif Abelisto, dan asli Anda)

Hasil EXPLAIN ANALYZEseperti yang disediakan oleh OP:

 Nested Loop (biaya = 0,56..356,65 baris = 102 lebar = 20) (waktu aktual = 0,034..0.979 baris = 98 loop = 1)
   -> Pemindaian Seq pada stasiun s (biaya = 0,00..3.02 baris = 102 lebar = 4) (waktu aktual = 0,009..0.016 baris = 102 loop = 1)
   -> Batas (biaya = 0,56..3.45 baris = 1 lebar = 16) (waktu aktual = 0.009..0.009 baris = 1 loop = 102)
         -> Pemindaian Indeks menggunakan station_id__submitted_at pada station_logs (biaya = 0,56..664062,38 baris = 230223 lebar = 16) (waktu aktual = 0,009 $
               Indeks Cond: (station_id = s.id)
 Waktu perencanaan: 0,542 ms
 Waktu pelaksanaan: 1.013 ms   - !!

Satu-satunya indeks yang Anda butuhkan adalah satu Anda buat: station_id__submitted_at. The UNIQUEkendala uniq_sid_satjuga melakukan pekerjaan, pada dasarnya. Mempertahankan keduanya tampak seperti pemborosan ruang disk dan kinerja penulisan.

Saya menambahkan NULLS LASTke ORDER BYdalam permintaan karena submitted_attidak ditentukan NOT NULL. Idealnya, jika berlaku!, Tambahkan NOT NULLkendala ke kolom submitted_at, jatuhkan indeks tambahan dan hapus NULLS LASTdari kueri.

Jika submitted_atbisa NULL, buat UNIQUEindeks ini untuk mengganti indeks Anda saat ini dan batasan unik:

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

Mempertimbangkan:

Ini mengasumsikan tabel terpisahstation dengan satu baris per relevan station_id(biasanya PK) - yang seharusnya Anda miliki. Jika Anda tidak memilikinya, buat itu. Sekali lagi, sangat cepat dengan teknik rCTE ini:

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;

Saya menggunakannya di biola juga. Anda bisa menggunakan kueri serupa untuk menyelesaikan tugas Anda secara langsung, tanpa stationtabel - jika Anda tidak dapat diyakinkan untuk membuatnya.

Instruksi terperinci, penjelasan dan alternatif:

Optimalkan indeks

Permintaan Anda harus sangat cepat sekarang. Hanya jika Anda masih perlu mengoptimalkan kinerja baca ...

Mungkin masuk akal untuk menambahkan level_sensorsebagai kolom terakhir ke indeks untuk memungkinkan pemindaian hanya indeks , seperti komentar joanolo .
Con: Itu membuat indeks lebih besar - yang menambahkan sedikit biaya untuk semua permintaan menggunakannya.
Pro: Jika Anda benar-benar mendapatkan hanya scan indeks dari itu, permintaan di tangan tidak harus mengunjungi halaman tumpukan sama sekali, yang membuatnya sekitar dua kali lebih cepat. Tapi itu mungkin keuntungan yang tidak substansial untuk permintaan yang sangat cepat sekarang.

Namun , saya tidak berharap itu bekerja untuk kasus Anda. Anda menyebutkan:

... sekitar 20rb baris per hari per station_id.

Biasanya, itu akan menunjukkan beban tulis tanpa henti (1 per station_idsetiap 5 detik). Dan Anda tertarik dengan baris terbaru . Pemindaian hanya indeks hanya berfungsi untuk menumpuk halaman yang terlihat oleh semua transaksi (bit dalam peta visibilitas diatur). Anda harus menjalankan VACUUMpengaturan yang sangat agresif agar tabel dapat mengikuti beban penulisan, dan sebagian besar waktu tidak akan berfungsi. Jika asumsi saya benar, hanya pemindaian indeks keluar, jangan tambahkan level_sensorke indeks.

OTOH, jika asumsi saya bertahan, dan meja Anda tumbuh sangat besar , indeks BRIN mungkin membantu. Terkait:

Atau, yang lebih terspesialisasi dan lebih efisien: Indeks parsial hanya untuk penambahan terbaru untuk memotong sebagian besar baris yang tidak relevan:

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

Pilih stempel waktu yang Anda tahu bahwa baris yang lebih muda harus ada. Anda harus menambahkan WHEREkondisi yang cocok ke semua permintaan, seperti:

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

Anda harus menyesuaikan indeks dan kueri dari waktu ke waktu.
Jawaban terkait dengan detail lebih lanjut:

Erwin Brandstetter
sumber
Setiap kali saya tahu bahwa saya ingin loop bersarang (sering), menggunakan LATERAL adalah peningkatan kinerja untuk sejumlah situasi.
Paul Draper
6

Coba cara klasik:

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

JELASKAN ANALISIS oleh 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
Abelisto
sumber