2011-03-18 6 views
7

Oto prosty przykład dla mojego problemu:Powolne DOŁĄCZ frazę OR w WHERE

CREATE TABLE test1 (id SERIAL, key TEXT UNIQUE, value TEXT); 
CREATE TABLE test2 (id SERIAL, key TEXT UNIQUE, value TEXT); 

INSERT INTO test1 (key, value) 
SELECT i::TEXT, 'ABC' || i::TEXT 
FROM generate_series(0, 1000000) AS i; 

INSERT INTO test2 (key, value) 
SELECT i::TEXT, 'ABC' || (i+1000)::TEXT 
FROM generate_series(0, 600000) AS i; 

INSERT INTO test2 (key, value) 
SELECT i::TEXT, 'ABC' || (i+1000)::TEXT 
FROM generate_series(1000000, 1200000) AS i; 

CREATE INDEX test1_key ON test1 (key); 
CREATE INDEX test1_value ON test1 (value); 
CREATE INDEX test2_key ON test2 (key); 
CREATE INDEX test2_value ON test2 (value); 

VACUUM FULL VERBOSE ANALYZE test1; 
VACUUM FULL VERBOSE ANALYZE test2; 

To zapytanie obecnie używam, ale który trwa dłużej niż 6 sekund.

EXPLAIN ANALYZE 
SELECT test1.key AS key1, test1.value AS value1, 
     test2.key AS key2, test2.value AS value2 
FROM test1 
LEFT OUTER JOIN test2 ON (test1.key = test2.key) 
WHERE test1.value = 'ABC1234' OR test2.value = 'ABC1234'; 

key1 | value1 | key2 | value2 
------+---------+------+--------- 
234 | ABC234 | 234 | ABC1234 
1234 | ABC1234 | 1234 | ABC2234 
(2 rows) 

                 QUERY PLAN 
---------------------------------------------------------------------------------------------------------------------------- 
Hash Left Join (cost=27344.05..79728.10 rows=2 width=32) (actual time=5428.635..6097.098 rows=2 loops=1) 
    Hash Cond: (test1.key = test2.key) 
    Filter: ((test1.value = 'ABC1234'::text) OR (test2.value = 'ABC1234'::text)) 
    -> Seq Scan on test1 (cost=0.00..16321.01 rows=1000001 width=15) (actual time=0.009..1057.315 rows=1000001 loops=1) 
    -> Hash (cost=13047.02..13047.02 rows=800002 width=17) (actual time=2231.964..2231.964 rows=800002 loops=1) 
     Buckets: 65536 Batches: 2 Memory Usage: 14551kB 
     -> Seq Scan on test2 (cost=0.00..13047.02 rows=800002 width=17) (actual time=0.010..980.232 rows=800002 loops=1) 
Total runtime: 6109.042 ms 
(8 rows) 

W obu tabelach tylko bardzo niewiele zestawów danych spełni wymagania, ale wydaje się, że fakt ten nie jest przestrzegany. mogę zamiast użyć kwerendy tak:

EXPLAIN ANALYZE 
SELECT coalesce(test1.key, test3.key1) AS key1, coalesce(test1.value, test3.value1) AS value1, 
     coalesce(test2.key, test3.key2) AS key2, coalesce(test2.value, test3.value2) AS value2 
FROM (SELECT test1.key AS key1, test1.value AS value1, 
      test2.key AS key2, test2.value AS value2 
     FROM (SELECT key, value FROM test1 WHERE value = 'ABC1234') AS test1 
     FULL JOIN (SELECT key, value FROM test2 WHERE value = 'ABC1234') AS test2 
     ON (test1.key = test2.key)) AS test3 
LEFT OUTER JOIN test1 ON (test1.key = test3.key2) 
LEFT OUTER JOIN test2 ON (test2.key = test3.key1) 
WHERE test1.key IS NOT NULL; 

key1 | value1 | key2 | value2 
------+---------+------+--------- 
1234 | ABC1234 | 1234 | ABC2234 
234 | ABC234 | 234 | ABC1234 
(2 rows) 

                   QUERY PLAN 
------------------------------------------------------------------------------------------------------------------------------------------ 
Nested Loop Left Join (cost=0.00..33.56 rows=1 width=64) (actual time=0.075..0.083 rows=1 loops=1) 
    -> Nested Loop (cost=0.00..25.19 rows=1 width=47) (actual time=0.066..0.072 rows=1 loops=1) 
     -> Nested Loop Left Join (cost=0.00..16.80 rows=1 width=32) (actual time=0.051..0.054 rows=1 loops=1) 
       -> Index Scan using test2_value_key on test2 (cost=0.00..8.41 rows=1 width=17) (actual time=0.026..0.027 rows=1 loops=1) 
        Index Cond: (value = 'ABC1234'::text) 
       -> Index Scan using test1_key on test1 (cost=0.00..8.38 rows=1 width=15) (actual time=0.020..0.020 rows=0 loops=1) 
        Index Cond: (public.test1.key = public.test2.key) 
        Filter: (public.test1.value = 'ABC1234'::text) 
     -> Index Scan using test1_key on test1 (cost=0.00..8.38 rows=1 width=15) (actual time=0.011..0.013 rows=1 loops=1) 
       Index Cond: ((public.test1.key IS NOT NULL) AND (public.test1.key = public.test2.key)) 
    -> Index Scan using test2_key on test2 (cost=0.00..8.36 rows=1 width=17) (actual time=0.001..0.001 rows=0 loops=1) 
     Index Cond: (public.test2.key = public.test1.key) 
Total runtime: 0.139 ms 

następujące zapytanie jest prostsze, ale wciąż zbyt powolny:

EXPLAIN ANALYZE 
SELECT test1.key AS key1, test1.value AS value1, 
     test2.key AS key2, test2.value AS value2 
FROM test1 
LEFT OUTER JOIN test2 ON (test1.key = test2.key) 
WHERE test1.value = 'ABC1234' 
    OR EXISTS (SELECT 1 FROM test2 t WHERE t.key = test1.key AND t.value = 'ABC1234'); 

key1 | value1 | key2 | value2 
------+---------+------+--------- 
1234 | ABC1234 | 1234 | ABC2234 
234 | ABC234 | 234 | ABC1234 
(2 rows) 

                   QUERY PLAN 
---------------------------------------------------------------------------------------------------------------------------------------- 
Merge Left Join (cost=0.00..8446826.32 rows=500001 width=32) (actual time=615.706..1651.370 rows=2 loops=1) 
    Merge Cond: (test1.key = test2.key) 
    -> Index Scan using test1_key on test1 (cost=0.00..8398983.25 rows=500001 width=15) (actual time=28.449..734.567 rows=2 loops=1) 
     Filter: ((value = 'ABC1234'::text) OR (alternatives: SubPlan 1 or hashed SubPlan 2)) 
     SubPlan 1 
      -> Index Scan using test2_key on test2 t (cost=0.00..8.36 rows=1 width=0) (never executed) 
       Index Cond: (key = $0) 
       Filter: (value = 'ABC1234'::text) 
     SubPlan 2 
      -> Index Scan using test2_value on test2 t (cost=0.00..8.37 rows=1 width=7) (actual time=0.376..0.380 rows=1 loops=1) 
       Index Cond: (value = 'ABC1234'::text) 
    -> Index Scan using test2_key on test2 (cost=0.00..39593.05 rows=800002 width=17) (actual time=0.019..498.456 rows=348894 loops=1) 
Total runtime: 1651.453 ms 
(13 rows) 


Więc moje pytanie brzmi: Czy jest proste zapytanie, które doprowadzi do podobnego planu szybkiego wykonywania, takiego jak drugie zapytanie lub może do indeksu lub jakiejś wskazówki dla planisty.

(wiem na ten przykład to byłoby uzasadnione tylko jeden stół z obu wartości w nim. Jednak w rzeczywistości stoliki są bardziej skomplikowane i schemat tabeli nie może być zmieniony tak łatwo.)


PostgreSQL Version: 9.0.3 
shared_buffers = 64MB 
effective_cache_size = 32MB 
work_mem = 16MB 
maintenance_work_mem = 32MB 
temp_buffers = 8MB 
wal_buffers= 1MB 


EDIT: Jak sugeruje z Kipotlov tutaj jest wersja UNION. Dlaczego zwykłe zapytanie OR nie wybiera tak dobrego planu?

EXPLAIN ANALYZE 
SELECT test1.key AS key1, test1.value AS value1, 
     test2.key AS key2, test2.value AS value2 
FROM test1 
LEFT OUTER JOIN test2 ON (test1.key = test2.key) 
WHERE test1.value = 'ABC1234' 
UNION 
SELECT test1.key AS key1, test1.value AS value1, 
     test2.key AS key2, test2.value AS value2 
FROM test1 
LEFT OUTER JOIN test2 ON (test1.key = test2.key) 
WHERE test2.value = 'ABC1234'; 

key1 | value1 | key2 | value2 
------+---------+------+--------- 
1234 | ABC1234 | 1234 | ABC2234 
234 | ABC234 | 234 | ABC1234 
(2 rows) 

                    QUERY PLAN 
------------------------------------------------------------------------------------------------------------------------------------------------ 
Unique (cost=33.64..33.66 rows=2 width=32) (actual time=0.114..0.119 rows=2 loops=1) 
    -> Sort (cost=33.64..33.64 rows=2 width=32) (actual time=0.111..0.113 rows=2 loops=1) 
     Sort Key: public.test1.key, public.test1.value, public.test2.key, public.test2.value 
     Sort Method: quicksort Memory: 17kB 
     -> Append (cost=0.00..33.63 rows=2 width=32) (actual time=0.046..0.097 rows=2 loops=1) 
       -> Nested Loop Left Join (cost=0.00..16.81 rows=1 width=32) (actual time=0.044..0.050 rows=1 loops=1) 
        -> Index Scan using test1_value_key on test1 (cost=0.00..8.44 rows=1 width=15) (actual time=0.023..0.024 rows=1 loops=1) 
          Index Cond: (value = 'ABC1234'::text) 
        -> Index Scan using test2_key on test2 (cost=0.00..8.36 rows=1 width=17) (actual time=0.014..0.016 rows=1 loops=1) 
          Index Cond: (public.test1.key = public.test2.key) 
       -> Nested Loop (cost=0.00..16.80 rows=1 width=32) (actual time=0.036..0.041 rows=1 loops=1) 
        -> Index Scan using test2_value_key on test2 (cost=0.00..8.41 rows=1 width=17) (actual time=0.019..0.020 rows=1 loops=1) 
          Index Cond: (value = 'ABC1234'::text) 
        -> Index Scan using test1_key on test1 (cost=0.00..8.38 rows=1 width=15) (actual time=0.013..0.015 rows=1 loops=1) 
          Index Cond: (public.test1.key = public.test2.key) 
Total runtime: 0.173 ms 
(16 rows) 
+4

Czy próbowałeś użyć 2 zapytań i "UNION"? Pierwsze zapytanie z pierwszą klauzulą ​​where (test1.value) i drugą z drugą klauzulą ​​where (test2.value). Nie wiem, czy to będzie szybsze czy nie. – Kipotlov

+2

@Kipotlov Dodałem zapytanie UNION. Jest tak szybki, jak drugie zapytanie, ale nie jestem pewien, czy mogę go dostosować do mojego prawdziwego problemu. Jakieś pomysły, dlaczego normalne zapytanie OR preferuje sekwencyjne skanowanie? –

Odpowiedz

6

Przede wszystkim dziękuję za bardzo szczegółowe pytanie. Rzadko zdarza się, aby przed pytaniem znaleźć osoby, które badały swój problem w takich szczegółach.

Myślałem o tym i problem wydaje się, że PostgreSQL chce dołączyć do wszystkich wierszy, ponieważ każdy nie pasujący wiersz z testu 1 może być potencjalnie dopasowany w test2 - i na odwrót.

Rozwiązanie zmusza planistę do wykonania zapytania w dwóch krokach. Jednym z podejść jest duże zapytanie UNION, którego już próbujesz - zmusić do rozważenia każdego wyrażenia w osobnym zapytaniu.

Innym podejściem zmusza do planowania, aby znaleźć pasujące klucze pierwszy, a następnie wykonać przyłączyć, więc nie może być mowy o dwuznaczność:

EXPLAIN ANALYZE 
SELECT test1.key AS key1, test1.value AS value1, 
     test2.key AS key2, test2.value AS value2 
FROM (
    SELECT key FROM test1 WHERE value='ABC1234' 
    UNION SELECT key FROM test2 WHERE value='ABC1234' 
) AS matching_keys 
INNER JOIN test1 USING (key) 
LEFT OUTER JOIN test2 USING (key); 

Nested Loop Left Join (cost=16.84..34.44 rows=2 width=32) (actual time=0.211..0.280 rows=2 loops=1) 
    -> Nested Loop (cost=16.84..33.65 rows=2 width=15) (actual time=0.175..0.212 rows=2 loops=1) 
     -> Unique (cost=16.84..16.85 rows=2 width=6) (actual time=0.132..0.136 rows=2 loops=1) 
       -> Sort (cost=16.84..16.85 rows=2 width=6) (actual time=0.131..0.132 rows=2 loops=1) 
        Sort Key: public.test1.key 
        Sort Method: quicksort Memory: 25kB 
        -> Append (cost=0.00..16.83 rows=2 width=6) (actual time=0.058..0.110 rows=2 loops=1) 
          -> Index Scan using test1_value on test1 (cost=0.00..8.42 rows=1 width=6) (actual time=0.056..0.058 rows=1 loops=1) 
           Index Cond: (value = 'ABC1234'::text) 
          -> Index Scan using test2_value on test2 (cost=0.00..8.39 rows=1 width=7) (actual time=0.046..0.047 rows=1 loops=1) 
           Index Cond: (value = 'ABC1234'::text) 
     -> Index Scan using test1_key on test1 (cost=0.00..8.38 rows=1 width=15) (actual time=0.032..0.033 rows=1 loops=2) 
       Index Cond: (key = public.test1.key) 
    -> Index Scan using test2_key on test2 (cost=0.00..0.38 rows=1 width=17) (actual time=0.028..0.029 rows=1 loops=2) 
     Index Cond: (public.test1.key = key) 
Total runtime: 0.390 ms 
(16 rows) 

Znowu UNION służy rolę OR. Niestety to podejście nadal działa źle w przypadku zapytań takich jak value>'ABC1234'.Możesz go nieco poprawić, podskakując: work_mem. Brak mi tutaj.


chodzi o ostatnie pytanie:

Dlaczego normalny lub zapytanie nie wybrać taki dobry plan?

Ponieważ program do planowania PostgreSQL obecnie nie ma możliwości podziału wyrażeń OR'ed na oddzielne zapytania UNION. Jest kilka zastrzeżeń, które sprawiają, że jest trudniejsze, niż mogłoby się wydawać.

Planista PostgreSQL jest już dość rozbudowany, ale do tej pory nie było dużego priorytetu, korzystając z optymalizacji, które są już możliwe dzięki ręcznemu przepisywaniu SQL.

+2

Nie jestem pewien, czy ta optymalizacja jest zawsze możliwa przez ręczne przepisywanie. Na przykład, jeśli '?' Jest wartością dostarczoną przez użytkownika, a warunek to "wartość

+1

Zaktualizowałem moją odpowiedź z innym podejściem. Niestety, niewiele lepiej. – intgr

1

Nie wiem, która droga jest lepsza lub szybsza.

Ale pierwszą rzeczą, którą zauważam jest to, że: Masz dwie tabele z każdą kolumną klucza (UNIQUE) w każdej z nich. Następnie otrzymujesz dane z dwóch tabel dla tego samego klucza.

Chodzi mi o to, dlaczego nie przyłączysz się do dwóch stołów na początku, więc musisz tylko dostać się z jednego stołu?

+1

Moje prawdziwe dwie tabele mają więcej kolumn i nie są połączone tylko jedną kolumną. Po prostu uprościłem moją sprawę, wciąż otrzymując te same wyniki. Nie mogę tak łatwo połączyć moich prawdziwych dwóch tabel z jednym. –