Sorgular mantıksal olarak benzerse planlar neden farklıdır?


19

Yedi Haftada Yedi Veritabanından 3.Gün'ün ilk ödev sorusunu cevaplamak için iki işlev yazdım .

İstediğiniz bir film başlığını veya aktörün adını girebileceğiniz saklı bir yordam oluşturun ve aktörün oynadığı filmlere veya benzer türlere sahip filmlere göre ilk beş öneri döndürür.

İlk denemem doğru ama yavaş. Sonuç döndürmek 2000 ms'ye kadar sürebilir.

CREATE OR REPLACE FUNCTION suggest_movies(IN query text, IN result_limit integer DEFAULT 5)
  RETURNS TABLE(movie_id integer, title text) AS
$BODY$
WITH suggestions AS (

  SELECT
    actors.name AS entity_term,
    movies.movie_id AS suggestion_id,
    movies.title AS suggestion_title,
    1 AS rank
  FROM actors
  INNER JOIN movies_actors ON (actors.actor_id = movies_actors.actor_id)
  INNER JOIN movies ON (movies.movie_id = movies_actors.movie_id)

  UNION ALL

  SELECT
    searches.title AS entity_term,
    suggestions.movie_id AS suggestion_id,
    suggestions.title AS suggestion_title,
    RANK() OVER (PARTITION BY searches.movie_id ORDER BY cube_distance(searches.genre, suggestions.genre)) AS rank
  FROM movies AS searches
  INNER JOIN movies AS suggestions ON
    (searches.movie_id <> suggestions.movie_id) AND
    (cube_enlarge(searches.genre, 2, 18) @> suggestions.genre)
)
SELECT suggestion_id, suggestion_title
FROM suggestions
WHERE entity_term = query
ORDER BY rank, suggestion_id
LIMIT result_limit;
$BODY$
LANGUAGE sql;

İkinci denemem doğru ve hızlı. Filtreyi CTE'den birliğin her bir parçasına doğru iterek optimize ettim.

Bu sorguyu dış sorgudan kaldırdım:

WHERE entity_term = query

Bu satırı ilk iç sorguya ekledim:

WHERE actors.name = query

Bu satırı ikinci iç sorguya ekledim:

WHERE movies.title = query

İkinci fonksiyon aynı sonucu döndürmek için yaklaşık 10 ms sürer.

Veritabanında işlev tanımları dışında hiçbir şey farklı değildir.

PostgreSQL neden bu iki mantıksal eşdeğer sorgu için farklı planlar üretiyor?

EXPLAIN ANALYZEİlk fonksiyonunun planı aşağıdaki gibidir:

                                                                                       Limit  (cost=7774.18..7774.19 rows=5 width=44) (actual time=1738.566..1738.567 rows=5 loops=1)
   CTE suggestions
     ->  Append  (cost=332.56..7337.19 rows=19350 width=285) (actual time=7.113..1577.823 rows=383024 loops=1)
           ->  Subquery Scan on "*SELECT* 1"  (cost=332.56..996.80 rows=11168 width=33) (actual time=7.113..22.258 rows=11168 loops=1)
                 ->  Hash Join  (cost=332.56..885.12 rows=11168 width=33) (actual time=7.110..19.850 rows=11168 loops=1)
                       Hash Cond: (movies_actors.movie_id = movies.movie_id)
                       ->  Hash Join  (cost=143.19..514.27 rows=11168 width=18) (actual time=4.326..11.938 rows=11168 loops=1)
                             Hash Cond: (movies_actors.actor_id = actors.actor_id)
                             ->  Seq Scan on movies_actors  (cost=0.00..161.68 rows=11168 width=8) (actual time=0.013..1.648 rows=11168 loops=1)
                             ->  Hash  (cost=80.86..80.86 rows=4986 width=18) (actual time=4.296..4.296 rows=4986 loops=1)
                                   Buckets: 1024  Batches: 1  Memory Usage: 252kB
                                   ->  Seq Scan on actors  (cost=0.00..80.86 rows=4986 width=18) (actual time=0.009..1.681 rows=4986 loops=1)
                       ->  Hash  (cost=153.61..153.61 rows=2861 width=19) (actual time=2.768..2.768 rows=2861 loops=1)
                             Buckets: 1024  Batches: 1  Memory Usage: 146kB
                             ->  Seq Scan on movies  (cost=0.00..153.61 rows=2861 width=19) (actual time=0.003..1.197 rows=2861 loops=1)
           ->  Subquery Scan on "*SELECT* 2"  (cost=6074.48..6340.40 rows=8182 width=630) (actual time=1231.324..1528.188 rows=371856 loops=1)
                 ->  WindowAgg  (cost=6074.48..6258.58 rows=8182 width=630) (actual time=1231.324..1492.106 rows=371856 loops=1)
                       ->  Sort  (cost=6074.48..6094.94 rows=8182 width=630) (actual time=1231.307..1282.550 rows=371856 loops=1)
                             Sort Key: searches.movie_id, (cube_distance(searches.genre, suggestions_1.genre))
                             Sort Method: external sort  Disk: 21584kB
                             ->  Nested Loop  (cost=0.27..3246.72 rows=8182 width=630) (actual time=0.047..909.096 rows=371856 loops=1)
                                   ->  Seq Scan on movies searches  (cost=0.00..153.61 rows=2861 width=315) (actual time=0.003..0.676 rows=2861 loops=1)
                                   ->  Index Scan using movies_genres_cube on movies suggestions_1  (cost=0.27..1.05 rows=3 width=315) (actual time=0.016..0.277 rows=130 loops=2861)
                                         Index Cond: (cube_enlarge(searches.genre, 2::double precision, 18) @> genre)
                                         Filter: (searches.movie_id <> movie_id)
                                         Rows Removed by Filter: 1
   ->  Sort  (cost=436.99..437.23 rows=97 width=44) (actual time=1738.565..1738.566 rows=5 loops=1)
         Sort Key: suggestions.rank, suggestions.suggestion_id
         Sort Method: top-N heapsort  Memory: 25kB
         ->  CTE Scan on suggestions  (cost=0.00..435.38 rows=97 width=44) (actual time=1281.905..1738.531 rows=43 loops=1)
               Filter: (entity_term = 'Die Hard'::text)
               Rows Removed by Filter: 382981
 Total runtime: 1746.623 ms

EXPLAIN ANALYZEİkinci sorgu planı aşağıdaki gibidir:

 Limit  (cost=43.74..43.76 rows=5 width=44) (actual time=1.231..1.234 rows=5 loops=1)
   CTE suggestions
     ->  Append  (cost=4.86..43.58 rows=5 width=391) (actual time=1.029..1.141 rows=43 loops=1)
           ->  Subquery Scan on "*SELECT* 1"  (cost=4.86..20.18 rows=2 width=33) (actual time=0.047..0.047 rows=0 loops=1)
                 ->  Nested Loop  (cost=4.86..20.16 rows=2 width=33) (actual time=0.047..0.047 rows=0 loops=1)
                       ->  Nested Loop  (cost=4.58..19.45 rows=2 width=18) (actual time=0.045..0.045 rows=0 loops=1)
                             ->  Index Scan using actors_name on actors  (cost=0.28..8.30 rows=1 width=18) (actual time=0.045..0.045 rows=0 loops=1)
                                   Index Cond: (name = 'Die Hard'::text)
                             ->  Bitmap Heap Scan on movies_actors  (cost=4.30..11.13 rows=2 width=8) (never executed)
                                   Recheck Cond: (actor_id = actors.actor_id)
                                   ->  Bitmap Index Scan on movies_actors_actor_id  (cost=0.00..4.30 rows=2 width=0) (never executed)
                                         Index Cond: (actor_id = actors.actor_id)
                       ->  Index Scan using movies_pkey on movies  (cost=0.28..0.35 rows=1 width=19) (never executed)
                             Index Cond: (movie_id = movies_actors.movie_id)
           ->  Subquery Scan on "*SELECT* 2"  (cost=23.31..23.40 rows=3 width=630) (actual time=0.982..1.081 rows=43 loops=1)
                 ->  WindowAgg  (cost=23.31..23.37 rows=3 width=630) (actual time=0.982..1.064 rows=43 loops=1)
                       ->  Sort  (cost=23.31..23.31 rows=3 width=630) (actual time=0.963..0.971 rows=43 loops=1)
                             Sort Key: searches.movie_id, (cube_distance(searches.genre, suggestions_1.genre))
                             Sort Method: quicksort  Memory: 28kB
                             ->  Nested Loop  (cost=4.58..23.28 rows=3 width=630) (actual time=0.808..0.916 rows=43 loops=1)
                                   ->  Index Scan using movies_title on movies searches  (cost=0.28..8.30 rows=1 width=315) (actual time=0.025..0.027 rows=1 loops=1)
                                         Index Cond: (title = 'Die Hard'::text)
                                   ->  Bitmap Heap Scan on movies suggestions_1  (cost=4.30..14.95 rows=3 width=315) (actual time=0.775..0.844 rows=43 loops=1)
                                         Recheck Cond: (cube_enlarge(searches.genre, 2::double precision, 18) @> genre)
                                         Filter: (searches.movie_id <> movie_id)
                                         Rows Removed by Filter: 1
                                         ->  Bitmap Index Scan on movies_genres_cube  (cost=0.00..4.29 rows=3 width=0) (actual time=0.750..0.750 rows=44 loops=1)
                                               Index Cond: (cube_enlarge(searches.genre, 2::double precision, 18) @> genre)
   ->  Sort  (cost=0.16..0.17 rows=5 width=44) (actual time=1.230..1.231 rows=5 loops=1)
         Sort Key: suggestions.rank, suggestions.suggestion_id
         Sort Method: top-N heapsort  Memory: 25kB
         ->  CTE Scan on suggestions  (cost=0.00..0.10 rows=5 width=44) (actual time=1.034..1.187 rows=43 loops=1)
 Total runtime: 1.410 ms

Yanıtlar:


21

CTE'ler için otomatik yüklem itme yok

PostgreSQL 9.3, CTE'ler için kesin aşağı itme yapmaz .

Aşağı itme işlemini belirleyen bir optimize edici, yan tümceleri iç sorgulara taşıyabilir. Amaç, alakasız verileri mümkün olduğunca erken filtrelemektir. Yeni sorgu mantıksal olarak eşdeğer olduğu sürece, motor yine de tüm ilgili verileri getirir, bu nedenle doğru sonucu yalnızca daha hızlı üretir.

Çekirdek geliştirici Tom Lane, pgsql-performans posta listesindeki mantıksal denkliği belirleme zorluğuna işaret ediyor .

CTE'ler aynı zamanda optimizasyon çitleri olarak kabul edilir; bu, CTE yazılabilir bir sorgu içerdiğinde anlambilimi akılda tutmak için bir optimizer sınırlaması değildir.

Optimize edici, salt okunur CTE'leri yazılabilir olanlardan ayırmaz, bu nedenle planları düşünürken aşırı muhafazakardır. 'Çit' tedavisi, optimizasyonun CTE içindeki nerede yan tümcesini hareket ettirmesini durdurur, ancak bunu yapmanın güvenli olduğunu görebiliriz.

PostgreSQL ekibinin CTE optimizasyonunu geliştirmesini bekleyebiliriz, ancak şimdilik iyi performans elde etmek için yazma tarzınızı değiştirmeniz gerekiyor.

Performans için yeniden yazma

Soru zaten daha iyi bir plan elde etmenin bir yolunu gösteriyor. Filtre koşulunun çoğaltılması, temelde itmeli itmenin etkisini kodlar.

Her iki planda da, motor sonuç satırlarını bir çalışma masasına kopyalar, böylece onları sıralayabilir. Çalışma masası büyüdükçe, sorgu yavaşlar.

İlk plan, temel tablolardaki tüm satırları çalışma masasına kopyalar ve sonucu bulmak için tarar. İşleri daha da yavaşlatmak için, tüm çalışma tezgahını taraması gerekir, çünkü dizini yoktur.

Bu saçma bir miktar gereksiz iş. Temel tablolardaki tahmini 19350 satırdan yalnızca 5 eşleşen satır olduğunda, cevabı bulmak için temel tablolardaki tüm verileri iki kez okur.

İkinci plan, eşleşen satırları bulmak için dizinleri kullanır ve bunları yalnızca çalışma masasına kopyalar. Endeks, verileri bizim için etkili bir şekilde filtreledi.

On sayfa 85 SQL Sanat, Stéphane Faroult kullanıcı beklentilerinin hatırlatmaktadır.

Çok büyük ölçüde, son kullanıcılar sabrını bekledikleri sıra sayısına göre ayarlar: bir iğne istediklerinde samanlığın boyutuna çok az dikkat ederler.

İkinci plan iğne ile ölçeklenir, bu nedenle kullanıcılarınızı mutlu etme olasılığı daha yüksektir.

Sürdürülebilirlik için yeniden yazma

Yeni sorguyu korumak daha zordur, çünkü bir filtre epksresyonunu değiştirerek bir kusur ekleyebilir, diğerini değiştiremezsiniz.

Her şeyi sadece bir kez yazıp yine de iyi bir performans elde etmek harika olmaz mıydı?

Yapabiliriz. Optimize edici, alt sorgular için aşağı itme işlemini belirler.

Daha basit bir örneği açıklamak daha kolaydır.

CREATE TABLE a (c INT);

CREATE TABLE b (c INT);

CREATE INDEX a_c ON a(c);

CREATE INDEX b_c ON b(c);

INSERT INTO a SELECT 1 FROM generate_series(1, 1000000);

INSERT INTO b SELECT 2 FROM a;

INSERT INTO a SELECT 3;

Bu, her biri dizinlenmiş bir sütuna sahip iki tablo oluşturur. Birlikte bir milyon 1s, bir milyon 2s ve bir tane içerirler 3.

İğneyi 3bu sorgulardan birini kullanarak bulabilirsiniz .

-- CTE
EXPLAIN ANALYZE
WITH cte AS (
  SELECT c FROM a
  UNION ALL
  SELECT c FROM b
)
SELECT c FROM cte WHERE c = 3;

-- Subquery
EXPLAIN ANALYZE
SELECT c
FROM (
  SELECT c FROM a
  UNION ALL
  SELECT c FROM b
) AS subquery
WHERE c = 3;

CTE için plan yavaş. Motor üç tablo tarar ve yaklaşık dört milyon satırı okur. Yaklaşık 1000 milisaniye sürer.

CTE Scan on cte  (cost=33275.00..78275.00 rows=10000 width=4) (actual time=471.412..943.225 rows=1 loops=1)
  Filter: (c = 3)
  Rows Removed by Filter: 2000000
  CTE cte
    ->  Append  (cost=0.00..33275.00 rows=2000000 width=4) (actual time=0.011..409.573 rows=2000001 loops=1)
          ->  Seq Scan on a  (cost=0.00..14425.00 rows=1000000 width=4) (actual time=0.010..114.869 rows=1000001 loops=1)
          ->  Seq Scan on b  (cost=0.00..18850.00 rows=1000000 width=4) (actual time=5.530..104.674 rows=1000000 loops=1)
Total runtime: 948.594 ms

Alt sorgu planı hızlıdır. Motor her dizini arar. Milisaniyeden daha az zaman alır.

Append  (cost=0.42..8.88 rows=2 width=4) (actual time=0.021..0.038 rows=1 loops=1)
  ->  Index Only Scan using a_c on a  (cost=0.42..4.44 rows=1 width=4) (actual time=0.020..0.021 rows=1 loops=1)
        Index Cond: (c = 3)
        Heap Fetches: 1
  ->  Index Only Scan using b_c on b  (cost=0.42..4.44 rows=1 width=4) (actual time=0.016..0.016 rows=0 loops=1)
        Index Cond: (c = 3)
        Heap Fetches: 0
Total runtime: 0.065 ms

Etkileşimli bir sürüm için bkz. SQLFiddle .


0

Planlar Postgres 12'de aynı

Soru Postgres 9.3 hakkında sordu. Beş yıl sonra, bu sürüm eski, ama ne değişti?

PostgreSQL 12 şimdi böyle CTE'leri sıralıyor.

IN ile sorguları (Ortak tablo ifadeleri)

Ortak tablo ifadeleri (diğer adıyla WITHsorgular) artık a) özyinelemesiz, b) herhangi bir yan etkisi bulunmadığında ve c) bir sorgunun sonraki bölümlerinde yalnızca bir kez başvuruda bulunulduğunda sorguda otomatik olarak satır içine alınabilir. Bu WITH, PostgreSQL 8.4 yantümcesinin getirilmesinden beri var olan bir "optimizasyon çitini" kaldırır

Gerekirse, bir WITH sorgusunu MATERIALIZED deyimini kullanarak gerçekleştirmeye zorlayabilirsiniz, örn.

WITH c AS MATERIALIZED ( SELECT * FROM a WHERE a.x % 4 = 0 ) SELECT * FROM c JOIN d ON d.y = a.x;
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.