Aralık türünde tam eşitlikten kaynaklanan hatalı sorgu planı nasıl ele alınır?


28

Bir tstzrangedeğişkende tam bir eşitlik gerektiren bir güncelleme yapıyorum . ~ 1M satır değiştirilir ve sorgu ~ 13 dakika sürer. Bunun sonucu buradaEXPLAIN ANALYZE görülebilir ve gerçek sonuçlar, sorgu planlayıcısı tarafından tahmin edilenlerden oldukça farklıdır. Sorun şu ki, endeks taramasının açık kalması tek bir satırın dönmesini bekliyor.t_range

Bu durum, aralık tiplerine ilişkin istatistiklerin diğer tiplerden farklı şekilde depolandığı gerçeğiyle ilgili gibi görünmektedir. pg_statsSütun görünümüne bakıldığında n_distinct, -1 olur ve diğer alanlar (örneğin most_common_vals, most_common_freqs) boştur.

Ancak, bir t_rangeyerde depolanan istatistikler olması gerekir . Kesin bir eşitlik yerine t_range'de bir 'inside' kullandığım son derece benzer bir güncelleme yapmak yaklaşık 4 dakika sürüyor ve önemli ölçüde farklı bir sorgu planı kullanıyor ( buraya bakın ). İkinci sorgu planı bana mantıklı geliyor çünkü temp tablosundaki her satır ve tarih tablosunun önemli bir bölümü kullanılacak. Daha önemlisi, sorgu planlayıcısı, filtre açık için yaklaşık olarak doğru sayıda satır öngörür t_range.

Dağılımı t_rangebiraz sıradışı. Bu tabloyu başka bir tablonun tarihsel durumunu saklamak için kullanıyorum ve diğer tablonun değişiklikleri aynı anda büyük dökümlerde gerçekleşti, bu nedenle çok belirgin değerleri yok t_range. İşte benzersiz değerlerin her birine karşılık gelen sayımlar t_range:

                              t_range                              |  count  
-------------------------------------------------------------------+---------
 ["2014-06-12 20:58:21.447478+00","2014-06-27 07:00:00+00")        |  994676
 ["2014-06-12 20:58:21.447478+00","2014-08-01 01:22:14.621887+00") |   36791
 ["2014-06-27 07:00:00+00","2014-08-01 07:00:01+00")               | 1000403
 ["2014-06-27 07:00:00+00",infinity)                               |   36791
 ["2014-08-01 07:00:01+00",infinity)                               |  999753

t_rangeYukarıda belirtilenler için sayımlar tamamlanmıştır, bu nedenle kardinalite ~ 3M'dir (bunun ~ 1M'si her iki güncelleme sorgusundan etkilenecektir).

Neden 1. sorgu 2. sorgudan çok daha düşük performans gösteriyor? Benim durumumda, sorgu 2 iyi bir alternatiftir, ancak kesin bir aralık eşitliği gerçekten gerekliyse, Postgres'in daha akıllı bir sorgu planı kullanmasını nasıl sağlayabilirim?

Dizinli tablo tanımı (alakasız sütunları bırakma):

       Column        |   Type    |                                  Modifiers                                   
---------------------+-----------+------------------------------------------------------------------------------
 history_id          | integer   | not null default nextval('gtfs_stop_times_history_history_id_seq'::regclass)
 t_range             | tstzrange | not null
 trip_id             | text      | not null
 stop_sequence       | integer   | not null
 shape_dist_traveled | real      | 
Indexes:
    "gtfs_stop_times_history_pkey" PRIMARY KEY, btree (history_id)
    "gtfs_stop_times_history_t_range" gist (t_range)
    "gtfs_stop_times_history_trip_id" btree (trip_id)

Sorgu 1:

UPDATE gtfs_stop_times_history sth
SET shape_dist_traveled = tt.shape_dist_traveled
FROM gtfs_stop_times_temp tt
WHERE sth.trip_id = tt.trip_id
AND sth.stop_sequence = tt.stop_sequence
AND sth.t_range = '["2014-08-01 07:00:01+00",infinity)'::tstzrange;

Sorgu 2:

UPDATE gtfs_stop_times_history sth
SET shape_dist_traveled = tt.shape_dist_traveled
FROM gtfs_stop_times_temp tt
WHERE sth.trip_id = tt.trip_id
AND sth.stop_sequence = tt.stop_sequence
AND '2014-08-01 07:00:01+00'::timestamptz <@ sth.t_range;

Q1, 999753 satırları güncelleştirir ve Q2, 999753 + 36791 = 1036544'ü güncelleştirir (yani geçici tablo, zaman aralığı koşuluyla eşleşen her satırın güncelleneceği şekildedir).

Bu sorguyu @ ypercube adlı kullanıcının yorumuna yanıt olarak denedim :

Sorgu 3:

UPDATE gtfs_stop_times_history sth
SET shape_dist_traveled = tt.shape_dist_traveled
FROM gtfs_stop_times_temp tt
WHERE sth.trip_id = tt.trip_id
AND sth.stop_sequence = tt.stop_sequence
AND sth.t_range <@ '["2014-08-01 07:00:01+00",infinity)'::tstzrange
AND '["2014-08-01 07:00:01+00",infinity)'::tstzrange <@ sth.t_range;

Sorgu planı ve sonuçları ( buraya bakın ) önceki iki vaka arasında (~ 6 dakika) orta derecedeydi.

2016/02/05 EDIT

1,5 yıl sonra verilere artık erişemiyor, aynı yapıya (indekssiz) ve benzer kardinaliteye sahip bir test masası oluşturdum. jjanes'ın cevabı , nedenin güncelleme için kullanılan geçici tablonun sıralanması olabileceğini öne sürdü. Hipotezi doğrudan test edemedim çünkü erişemem track_io_timing(Amazon RDS kullanarak).

  1. Genel sonuçlar çok daha hızlıydı (birkaç faktöre göre). Sanırım bunun Erwin'in cevabıyla tutarlı olan endekslerin kaldırılması yüzünden olduğunu düşünüyorum .

  2. Bu test durumunda, Sorgu 1 ve 2 temelde aynı miktarda zaman aldı, çünkü ikisi de birleştirme birleştirmesini kullandılar. Yani, Postgres'in karma birleştirmeyi seçmesine neden olan şeyi tetikleyemedim, bu nedenle Postgres'in neden düşük performans gösteren karma birleşimi ilk tercih ettiği konusunda netlik duymuyorum.


1
Ne eşitlik durumu dönüştürülmüş ise (a = b)ikiye koşulları "içerir": (a @> b AND b @> a)? Plan değişiyor mu?
ypercubeᵀᴹ

@ypercube: Plan hala çok iyi olmasa da önemli ölçüde değişiyor - 2 numaralı düzenlememe bakın.
abeboparebop

1
Başka bir fikir (lower(t_range),upper(t_range)), eşitliği kontrol ettiğinizden beri düzenli bir btree endeksi eklemek olacaktır .
ypercubeᵀᴹ

Yanıtlar:


9

Uygulama planlarınızdaki zamandaki en büyük fark, en üstteki düğüm olan UPDATE'tir. Bu, güncelleme sırasında zamanınızın çoğunun G / Ç'ye gideceğini gösteriyor. track_io_timingSorguları açıp çalıştırarak bunu doğrulayabilirsiniz.EXPLAIN (ANALYZE, BUFFERS)

Farklı planlar, farklı sıralarda güncellenecek satırlar sunuyor. Biri trip_idsırayla, diğeri ise hangi sırayla fiziksel tabloda temp tablosunda bulunuyorsa.

Güncellenmekte olan tablonun fiziksel sırasının trip_id sütunu ile ilişkilendirildiği görülmekte ve bu sıradaki satırların güncellenmesi, okuma-ileri / ardışık okumalarla verimli IO kalıplarına yol açmaktadır. Temp tablosunun fiziksel sırası birçok rastgele okumaya yol açıyor gibi gözüküyor.

order by trip_idTemp tablosunu oluşturan ifadeye bir ekleyebilirseniz , bu sorunu sizin için çözebilir.

PostgreSQL, UPDATE işlemini planlarken IO siparişinin etkilerini dikkate almaz. (SELECT işlemlerinin aksine, onları hesaba kattığı yer). PostgreSQL daha akıllı olsaydı, ya bir planın daha verimli bir düzen ürettiğini fark ederdi ya da güncelleme ile alt düğümü arasında açık bir sıralama düğümünü arayacaktı ki böylece güncelleme ctid sırayla beslenecekti.

PostgreSQL'in aralıklardaki eşitlik birleşimlerinin seçiciliğini tahmin eden kötü bir iş çıkardığı konusunda haksızsınız. Ancak, bu sadece temel probleminizle teğetsel olarak ilgilidir. Güncellemenizin seçme kısmındaki daha etkili bir sorgu yanlışlıkla daha iyi bir sıraya göre güncellemeye uygun satırları besleyebilir, ancak eğer öyleyse bu çoğunlukla şanssız.


Maalesef değiştiremiyorum track_io_timingve (bir buçuk senedir!) Artık orijinal verilere erişemiyorum. Ancak, teorinizi aynı şema ve benzer boyutta (milyon satır) tabloları oluşturarak ve iki farklı güncelleme çalıştırarak test ettim - bunlardan biri geçici güncelleme tablosunun orijinal tablo gibi sıralandığı ve biri de sıralandığı yarı-rasgele. Ne yazık ki, iki güncelleme kabaca aynı zaman alır, bu da güncelleme tablosunun sıralamasının bu sorguyu etkilemeyeceği anlamına gelir.
abeboparebop

7

Eşitlik tahmininin seçiciliğinin neden tstzrangesütundaki GiST endeksi tarafından bu kadar radikal bir şekilde tahmin edildiğinden tam olarak emin değilim . Bu ilginç görünmekle birlikte, sizin durumunuzla ilgisiz görünüyor.

Senin bu yana UPDATEvar olan tüm 3M satır tuşelere üçte biri (!), Bir indeks hiç yardım gitmiyor . Aksine, tabloya ek olarak dizini aşamalı olarak güncellemek size önemli bir maliyet katacaktır UPDATE.

Sadece basit Sorgu 1 tutun . Basit, radikal çözüm etmektir dizini bırakın önce UPDATE. Başka amaçlar için ihtiyacınız varsa, sonrasından sonra yeniden oluşturun UPDATE. Bu hala büyük sırasında endeksi korumaktan daha hızlı olacaktır UPDATE.

Bir İçin UPDATEtüm satırlar bir üçüncü, muhtemelen sıra tüm diğer endeksleri düşmeye ödeyecek - ve sonra bunları yeniden oluşturun UPDATE. Tek dezavantajı: Ek ayrıcalıklara ve masada özel bir kilite ihtiyacınız var (yalnızca kullanıyorsanız kısa bir süre için CREATE INDEX CONCURRENTLY).

@ ypercube'un GiST endeksi yerine btree kullanma fikri prensip olarak iyi görünüyor. Ama değil (hiçbir endeks başlamak herhangi iyidir) ve tüm satırlar üçte biri için değil sadece (lower(t_range),upper(t_range)), çünkü tstzrangeayrı bir aralık türü değil.

Kesikli aralık tiplerinin çoğu "eşitlik" kavramını basitleştiren kanonik bir şekle sahiptir: kanonik formdaki değerin alt ve üst sınırı onu tanımlar. Dökümantasyon:

Kesikli bir aralık tipi , eleman tipi için istenen adım boyutunun farkında olan bir kanonikleştirme fonksiyonuna sahip olmalıdır . Kanonikleştirme işlevi, özellikle tutarlı bir şekilde kapsayıcı veya münhasır sınırlar olmak üzere, aynı gösterimlere sahip olmak için aralık türündeki eşdeğer değerlerin dönüştürülmesiyle yüklenir. Bir kurallaştırma işlevi belirtilmezse, farklı biçimlendirmelere sahip aralıklar, gerçekte aynı değer kümesini temsil etseler bile, her zaman eşitsiz olarak değerlendirilir.

Dahili aralığı türleri int4range, int8rangeve daterangetüm kullanım düşük bağlanmış ve hariç üst sınırı içeren standart bir formu; bu [),. Bununla birlikte, kullanıcı tanımlı aralık türleri başka kuralları da kullanabilir.

Bu, tstzrangeüst ve alt sınırın kapsayıcılığının eşitlik için göz önünde bulundurulması gereken durumlar için geçerli değildir. Muhtemel bir btree endeksi açık olmalı:

(lower(t_range), upper(t_range), lower_inc(t_range), upper_inc(t_range))

Ve sorgular, WHEREcümlede aynı ifadeleri kullanmak zorunda kalacaktı .

Biri yalnızca döküm değerinin tamamını dizine alma eğiliminde olabilir text: (cast(t_range AS text))- ancak bu ifade, değerlerin IMMUTABLEmetin gösterimi timestamptzgeçerli timezoneayara bağlı olduğundan değildir . IMMUTABLEBir kanonik form üreten bir sarmalayıcı fonksiyona ek adımlar atmanız ve üzerinde işlevsel bir dizin oluşturmanız gerekir.

Ek önlemler / alternatif fikirler

Güncellenen satırlarınızın shape_dist_traveledbirçoğu için zaten aynı değere sahipse tt.shape_dist_traveled(ve UPDATEbenzer tetikleyicilerinizin yan etkilerine güvenmiyorsanız ...), boş güncellemeleri hariç tutarak sorgunuzu daha hızlı yapabilirsiniz:

WHERE ...
AND   shape_dist_traveled IS DISTINCT FROM tt.shape_dist_traveled;

Tabii ki, performans optimizasyonu için tüm genel tavsiyeler geçerlidir. Postgres Wiki iyi bir başlangıç ​​noktasıdır.

VACUUM FULLsizin için zehir olurdu, çünkü bazı ölü gözler (veya ayrılan alan FILLFACTOR) UPDATEperformans için faydalıdır .

Güncellenen birçok satırla ve bunu karşılayabiliyorsanız (eşzamanlı erişim veya diğer bağımlılıklar yoktur), yerinde güncelleme yapmak yerine tamamen yeni bir tablo yazmak daha da hızlı olabilir. Bu cevapla ilgili talimatlar:

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.