PostgreSQL'de DISTINCT'i nasıl daha hızlı hale getirebilirim?


13

station_logsPostgreSQL 9.6 veritabanında bir tablo var :

    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)

Her biri için son level_sensordeğeri almaya çalışıyorum . Yaklaşık 400 benzersiz değer ve günde yaklaşık 20 bin satır vardır .submitted_atstation_idstation_idstation_id

Dizin oluşturmadan önce:

EXPLAIN ANALYZE
SELECT DISTINCT ON(station_id) station_id, submitted_at, level_sensor
FROM station_logs ORDER BY station_id, submitted_at DESC;
 Benzersiz (maliyet = 4347852.14..4450301.72 satır = 89 genişlik = 20) (gerçek zaman = 22202.080..27619.167 satır = 98 döngü = 1)
   -> Sırala (maliyet = 4347852.14..4399076.93 satır = 20489916 genişlik = 20) (gerçek zaman = 22202.077..26540.827 satır = 20489812 döngü = 1)
         Sıralama Anahtarı: station_id, gönderilmiş_ DESC'de
         Sıralama Yöntemi: harici birleştirme Diski: 681040kB
         -> station_logs üzerinde Seq Scan (maliyet = 0.00..598895.16 satır = 20489916 genişlik = 20) (gerçek zaman = 0.023..3443.587 satır = 20489812 döngü = $
 Planlama süresi: 0.072 ms
 Yürütme süresi: 27690.644 ms

Dizin oluşturuluyor:

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

Dizin oluşturduktan sonra, aynı sorgu için:

 Benzersiz (maliyet = 0.56..2156367.51 satır = 89 genişlik = 20) (gerçek zaman = 0.184..16263.413 satır = 98 döngü = 1)
   -> station_logs üzerinde station_id__subored_at kullanarak İndeks Tarama (maliyet = 0.56..2105142.98 satır = 20489812 genişlik = 20) (gerçek zaman = 0.181..1 $
 Planlama süresi: 0.206 ms
 Yürütme süresi: 16263.490 ms

Bu sorguyu daha hızlı hale getirmenin bir yolu var mı? Örneğin 1 sn gibi, 16 sn hala çok fazla.


2
Kaç tane farklı istasyon kimliği var, yani sorgu kaç satır döndürüyor? Ve Postgres'in hangi sürümü?
ypercubeᵀᴹ

Postgre 9.6, yaklaşık 400 benzersiz station_id ve station_id başına günde yaklaşık 20 bin kayıt
Kokizzu

Bu sorgu döndürür bir "her station_id için submitted_at dayalı son level_sensor değerini,". DISTINCT ON, ihtiyacınız olmayan durumlar dışında rastgele bir seçim içerir.
philipxy

Yanıtlar:


18

Yalnızca 400 istasyon için bu sorgu çok daha hızlı olacaktır :

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 burada
(bu sorgu için planları, Abelis'in alternatifini ve orijinalinizi karşılaştırarak)

EXPLAIN ANALYZEOP tarafından sağlanan sonuç :

 İç İçe Döngü (maliyet = 0.56..356.65 satır = 102 genişlik = 20) (gerçek zaman = 0.034..0.979 satır = 98 döngü = 1)
   -> İstasyonlarda Seq Scan (s = maliyet = 0.00..3.02 satır = 102 genişlik = 4) (gerçek zaman = 0.009..0.016 satır = 102 döngü = 1)
   -> Limit (maliyet = 0.56..3.45 satır = 1 genişlik = 16) (gerçek zaman = 0.009..0.009 satır = 1 döngü = 102)
         -> station_logs üzerinde station_id__subored_at kullanılarak Dizin Tarama (maliyet = 0.56..664062.38 satır = 230223 genişlik = 16) (gerçek zaman = 0.009 $
               Endeks Koşulu: (station_id = s.id)
 Planlama süresi: 0.542 ms
 Yürütme süresi: 1,013 ms   - !!

Sadece endeks ihtiyacınız oluşturduğunuz biridir: station_id__submitted_at. UNIQUEKısıt uniq_sid_satda temelde iş yapar. Her ikisini de korumak, disk alanı ve yazma performansı kaybı gibi görünüyor.

Eklediğim NULLS LASTiçin ORDER BYçünkü sorguda submitted_attanımlı değil NOT NULL. İdeal olarak, varsa! Sütununa bir NOT NULLsınırlama ekleyin submitted_at, ek dizini bırakın NULLS LASTve sorgudan kaldırın .

Eğer submitted_atolabilir NULL, bu oluşturmak UNIQUEgeçerli dizini her ikisinin yerine indeksi ve benzersiz kısıtlama:

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

Düşünmek:

Bu, her bir şekilde olması gereken ilgili her satırda (genellikle PK) bir satır içeren ayrı bir tablostation olduğunu varsayar station_id. Elinizde yoksa oluşturun. Yine, bu rCTE tekniği ile çok hızlı:

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;

Bunu kemanda da kullanıyorum. Görevinizi stationtablo olmadan doğrudan çözmek için benzer bir sorgu kullanabilirsiniz - eğer bunu oluşturmak için ikna edilemiyorsanız.

Ayrıntılı talimatlar, açıklama ve alternatifler:

Dizini optimize et

Sorgunuz şimdi çok hızlı olmalı. Yalnızca yine de okuma performansını optimize etmeniz gerekiyorsa ...

Joanolo'nun yorumladığı gibi yalnızca dizin taramasınalevel_sensor izin vermek için dizine son sütun olarak eklemek mantıklı olabilir . Con: Dizini büyütür - bu, onu kullanan tüm sorgulara biraz maliyet katar. Pro: Eğer aslında sadece taramalar indeks alırsanız, eldeki sorgu yığın sayfaları hiç ziyaret etmek zorunda değilsiniz, bu da yaklaşık iki kat daha hızlı yapar. Ancak bu, şimdi çok hızlı bir sorgu için asılsız bir kazanç olabilir.

Ancak bunun davanız için çalışmasını beklemiyorum. Bahsettiniz:

... günde yaklaşık 20 bin satır station_id.

Tipik olarak, bu, kesintisiz yazma yükünü gösterir ( station_idher 5 saniyede bir 1). Ve son satırla ilgileniyorsunuz . Yalnızca dizin taramaları yalnızca tüm işlemler tarafından görülebilen yığın sayfaları için çalışır (görünürlük haritasındaki bit ayarlanır). VACUUMYazma yüküne ayak uydurmak için tablonun son derece agresif ayarlarını çalıştırmanız gerekir ve çoğu zaman işe yaramaz. Benim varsayımlar doğruysa, endeks salt taramalar, dışarı yok eklemek level_sensordizine.

Otoh, benim varsayımlar tutun ve masa büyüyor eğer çok büyük bir BRIN endeksi kudreti yardım. İlişkili:

Veya daha da özel ve daha verimli: Alakasız satırların büyük kısmını kesmek için yalnızca en son eklenenler için kısmi bir dizin:

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

Genç satırların var olması gerektiğini bildiğiniz bir zaman damgası seçin . WHEREAşağıdaki gibi tüm sorgulara eşleşen bir koşul eklemeniz gerekir :

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

Dizini ve sorguyu zaman zaman uyarlamanız gerekir.
Daha fazla ayrıntı ile ilgili cevaplar:


Ne zaman biliyorum LATERAL kullanarak iç içe bir döngü (genellikle) istiyorum, bir dizi durum için bir performans artışı.
Paul Draper

6

Klasik yolu deneyin:

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 tarafından EXPLAIN ANALYZE

 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
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.