WHERE koşulu ve GROUP BY ile SQL sorgusu için dizinler


15

Bir WHEREkoşul ile bir SQL sorgusu için hangi dizinleri kullanmak için belirlemeye çalışıyorum ve GROUP BYşu anda çok yavaş çalışıyor.

Sorgum:

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

Tabloda şu anda 32.000.000 satır var. Zaman dilimini artırdığımda sorgunun yürütme süresi çok artar.

Söz konusu tablo şöyle:

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

Şu anda aşağıdaki dizinler var, ancak performans hala yavaş:

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

Sorguda EXPLAIN çalıştırıldığında aşağıdaki sonuç elde edilir:

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

Örnek verilerle SQL Fiddle: http://sqlfiddle.com/#!15/7492b/1

Soru

Bu sorgunun performansı daha iyi dizinler ekleyerek iyileştirilebilir mi, yoksa işlem gücünü artırmalı mıyım?

Düzenle 1

PostgreSQL sürüm 9.3.2 kullanılır.

Düzenle 2

@Erwin'in teklifini şunlar ile denedim 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'
   );

Ama ne yazık ki bu performansı artırmıyor gibi görünüyordu. Sorgu Planı:

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

Düzenle 3

Ypercube'den LATERAL sorgusu için sorgu planı:

"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_idTabloda kaç farklı değer var?
ypercubeᵀᴹ

133 farklı group_id var.

Zaman damgaları 2011 ile 2014 arasında değişmektedir. Hem saniye hem de milisaniye kullanımdadır.

Sadece ilgileniyor musunuz group_id, saymıyorsunuz?
Erwin Brandstetter

@Erwin Örnekte gösterilmeyen dördüncü bir sütunda da max () ve (min) ile ilgileniyoruz.
uldall

Yanıtlar:


6

groupsTablo ve LATERALjoin adlı bir yapı da kullanan başka bir fikir (SQL-Server hayranları için bu hemen hemen aynıdır OUTER APPLY). Toplamaların alt sorguda hesaplanabilmesi avantajına sahiptir:

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'da yapılan test , sorgunun dizinde dizin taramaları yaptığını gösterir (group_id, ts).

Benzer planlar, biri min için ve biri maks için olmak üzere 2 yanal birleşim kullanılarak ve ayrıca 2 satır içi ilişkili alt sorgu ile üretilir. counterMin ve maks tarihlerinin yanı sıra tüm satırları göstermeniz gerekiyorsa da kullanılabilirler :

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 Sorunuza ilişkin sorgu planını orijinal soruya ekledim. Sorgu büyük zaman aralıklarında bile 50 ms'nin altında çalışır.
Uldall

5

Seçim listesinde toplam olmadığından, seçim listesine group bya koymakla hemen hemen aynı distinct, değil mi?

Bu istediğini ise, açıklandığı gibi, bir özyinelemeli sorgu kullanmak için bu yeniden yazarak comp_2_index üzerinde hızlı bir indeks araması almak mümkün olabilir PostgreSQL wiki'de .

Farklı group_ids değerlerini verimli bir şekilde döndürmek için bir görünüm oluşturun:

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

Ve sonra bu görünümü Erwin'in existsyarı birleştirmesindeki arama tablosunun yerine kullanın .


4

Sadece olduğundan 133 different group_id'sşunu kullanabilirsiniz integer(hatta smallintGROUP_ID için). Sizi fazla satın almayacaktır, çünkü 8 bayta kadar dolgu, masanızdaki geri kalanını ve olası çok sütunlu dizinleri yiyecektir. Ovaların işlenmesi integerbiraz daha hızlı olmalıdır. Daha intvs.int2 .

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

@Leo: zaman damgaları modern kurulumlarda 8 bayt tamsayı olarak saklanır ve mükemmel hızlı bir şekilde işlenebilir. Ayrıntılar.

@ ypercube: Sorguda (group_id, ts)herhangi bir koşul olmadığından, üzerindeki dizin yardımcı olamaz group_id.

Sizin asıl sorun işlenecek vardır muazzam miktarda verinin geçerli:

Sayaçta ts_index kullanarak Dizin Tarama (maliyet = 0.56..467470.93 satır = 194892 genişlik = 4)

Görüyorum ki sadece a'nın varlığıyla ilgileniyorsun group_idve gerçek bir sayı yok. Ayrıca, sadece 133 farklı group_ids vardır. Bu nedenle, sorgunuz gorup_idzaman dilimindeki ilk isabetten memnun olabilir . Bu nedenle, EXISTSyarı birleştirmeli alternatif bir sorgu için bu öneri :

Gruplar için bir arama tablosu varsayarsak:

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

Sizin endeks comp_2_indexüzerinde (group_id, ts)artık enstrümantal hale gelir.

SQL Fiddle (yorumlarda @ ypercube tarafından sağlanan kemanın üzerine inşa edilir)

Burada, sorgu dizini tercih ediyor (ts, group_id), ama bunun nedeni "kümelenmiş" zaman damgaları ile test kurulumu nedeniyle düşünüyorum. Dizinleri satır aralığı işaretiyle kaldırırsanız ts( bunun hakkında daha fazla bilgi ), planlayıcı, dizini de mutlu bir şekilde kullanır (group_id, ts)- özellikle Yalnızca Dizin Taramasında .

Bu işe yararsa, bu diğer olası iyileştirmeye ihtiyacınız olmayabilir: Satır sayısını önemli ölçüde azaltmak için verileri materyal görünümünde önceden toplayın. Ek olarak gerçek sayılara da ihtiyacınız varsa, bu özellikle mantıklı olacaktır . Sonra mv güncellerken bir kez birçok satır işleme maliyeti var . Günlük ve saatlik toplamaları birleştirebilir (iki ayrı tablo) ve sorgunuzu buna uyarlayabilirsiniz.

Sorgularınızdaki zaman dilimleri keyfi mi? Yoksa çoğunlukla tam dakika / saat / gün?

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;

Üzerinde gerekli dizinleri oluşturun ve counter_mvsorgunuzu onunla çalışacak şekilde uyarlayın ...


1
SQL-Fiddle'da 10k satırlı birkaç benzer şey denedim , ancak hepsi sıralı bir tarama gösterdi. groupsTabloyu kullanmak fark yaratır mı?
ypercubeᵀᴹ

@ypercube: Sanırım. Ayrıca, ANALYZEfark yaratır. Ama endeksler üzerinde ben counterbile tabloyu ANALYZEtanıtmak en kısa sürede bile kullanılır groups. Mesele şu ki, bu tablo olmadan, olası group_id´s kümesini oluşturmak için yine de bir seqscan gereklidir. Cevabıma daha fazlasını ekledim. Ve keman için teşekkürler!
Erwin Brandstetter

Bu garip. Postgres'in optimize edicisinin group_idbir SELECT DISTINCT group_id FROM t;sorgu için bile dizini kullanmayacağını mı söylüyorsunuz ?
ypercubeᵀᴹ

1
@ErwinBrandstetter Ben de öyle düşünmüştüm ve başka türlü öğrenmek çok şaşırdı. Bir olmadan LIMIT 1, erken durmadan faydalanmayan ve çok daha uzun süren bir bitmap dizin taraması seçebilir. (Ancak tablo yeni vakumlanırsa, bitmap taraması üzerinden endeksli olarak taramayı tercih edebilir, böylece gördüğünüz davranış tablonun vakum durumuna bağlıdır).
jjanes

1
@uldall: Günlük agregalar satır sayısını büyük ölçüde azaltır. Hile yapmalı. Ancak EXISTS sorgusunu denediğinizden emin olun. Şaşırtıcı derecede hızlı olabilir. Ek olarak min / maks için çalışmaz. Yine de, buradaki bir çizgiyi bırakacak kadar nazik olsaydınız, sonuçta ortaya çıkan performansla ilgilenirim.
Erwin Brandstetter
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.