PostgreSQL
 sql >> Baza danych >  >> RDS >> PostgreSQL

Dlaczego niewielka zmiana wyszukiwanego hasła tak bardzo spowalnia zapytanie?

Dlaczego?

Powód jest to:

Szybkie zapytanie:

->  Hash Left Join  (cost=1378.60..2467.48 rows=15 width=79) (actual time=41.759..85.037 rows=1129 loops=1)
      ...
      Filter: (unaccent(((((COALESCE(p.abrev, ''::character varying))::text || ' ('::text) || (COALESCE(p.prenome, ''::character varying))::text) || ')'::text)) ~~* (...)

Powolne zapytanie:

->  Hash Left Join  (cost=1378.60..2467.48 rows=1 width=79) (actual time=35.084..80.209 rows=1129 loops=1)
      ...
      Filter: (unaccent(((((COALESCE(p.abrev, ''::character varying))::text || ' ('::text) || (COALESCE(p.prenome, ''::character varying))::text) || ')'::text)) ~~* unacc (...)

Rozszerzenie wzorca wyszukiwania o inną postać powoduje, że Postgres przyjmuje jeszcze mniej trafień. (Zazwyczaj jest to rozsądne oszacowanie.) Postgres oczywiście nie ma wystarczająco dokładnych statystyk (żadnych, właściwie nie czytaj dalej), aby spodziewać się takiej samej liczby trafień, jaką naprawdę otrzymujesz.

Powoduje to przejście na inny plan zapytań, który jest jeszcze mniej optymalny dla rzeczywistego liczba trafień rows=1129 .

Rozwiązanie

Zakładając, że aktualny Postgres 9.5 nie został zadeklarowany.

Jednym ze sposobów poprawy sytuacji jest utworzenie indeksu wyrażenia na wyrażeniu w predykacie. To sprawia, że ​​Postgres gromadzi statystyki dla rzeczywistego wyrażenia, co może pomóc w zapytaniu nawet jeśli sam indeks nie jest używany w zapytaniu . Bez indeksu brak statystyk dla wyrażenia w ogóle. A jeśli zostanie to zrobione poprawnie, indeks może zostać użyty do zapytania, to nawet znacznie lepiej. Ale jest wiele problemów z Twoim obecnym wyrażeniem:

unaccent(TEKST(coalesce(p.abrev,'')||' ('||coalesce(p.prenome,'')||')')) lubie unaccent('%vicen%' )

Rozważ to zaktualizowane zapytanie w oparciu o niektóre założenia o nieujawnionych definicjach tabel:

SELECT e.id
     , (SELECT count(*) FROM imgitem
        WHERE tabid = e.id AND tab = 'esp') AS imgs -- count(*) is faster
     , e.ano, e.mes, e.dia
     , e.ano::text || to_char(e.mes2, 'FM"-"00')
                   || to_char(e.dia,  'FM"-"00') AS data    
     , pl.pltag, e.inpa, e.det, d.ano anodet
     , format('%s (%s)', p.abrev, p.prenome) AS determinador
     , d.tax
     , coalesce(v.val,v.valf)   || ' ' || vu.unit  AS altura
     , coalesce(v1.val,v1.valf) || ' ' || vu1.unit AS dap
     , d.fam, tf.nome família, d.gen, tg.nome AS gênero, d.sp
     , ts.nome AS espécie, d.inf, e.loc, l.nome localidade, e.lat, e.lon
FROM      pess    p                        -- reorder!
JOIN      det     d   ON d.detby   = p.id  -- INNER JOIN !
LEFT JOIN tax     tf  ON tf.oldfam = d.fam
LEFT JOIN tax     tg  ON tg.oldgen = d.gen
LEFT JOIN tax     ts  ON ts.oldsp  = d.sp
LEFT JOIN tax     ti  ON ti.oldinf = d.inf  -- unused, see @joop's comment
LEFT JOIN esp     e   ON e.det     = d.id
LEFT JOIN loc     l   ON l.id      = e.loc
LEFT JOIN var     v   ON v.esp     = e.id AND v.key  = 265
LEFT JOIN varunit vu  ON vu.id     = v.unit
LEFT JOIN var     v1  ON v1.esp    = e.id AND v1.key = 264
LEFT JOIN varunit vu1 ON vu1.id    = v1.unit
LEFT JOIN pl          ON pl.id     = e.pl
WHERE f_unaccent(p.abrev)   ILIKE f_unaccent('%' || 'vicenti' || '%') OR
      f_unaccent(p.prenome) ILIKE f_unaccent('%' || 'vicenti' || '%');

Główne punkty

Dlaczego f_unaccent() ? Ponieważ unaccent() nie można zindeksować. Przeczytaj to:

Użyłem opisanej tam funkcji, aby umożliwić wykonanie następującego (zalecane!) wielokolumnowego trygramu funkcjonalnego GIN indeks :

CREATE INDEX pess_unaccent_nome_trgm_idx ON pess
USING gin (f_unaccent(pess) gin_trgm_ops, f_unaccent(prenome) gin_trgm_ops);

Jeśli nie znasz indeksów trygramów, przeczytaj najpierw to:

I prawdopodobnie:

Pamiętaj, aby uruchomić najnowszą wersję Postgresa (obecnie 9.5). Wprowadzono znaczne ulepszenia indeksów GIN. Będziesz zainteresowany ulepszeniami w pg_trgm 1.2, które mają zostać wydane wraz z nadchodzącym Postgresem 9.6:

Przygotowane zestawienia są powszechnym sposobem wykonywania zapytań z parametrami (zwłaszcza z tekstem z danych wejściowych użytkownika). Postgres musi znaleźć plan, który działa najlepiej dla dowolnego parametru. Dodaj symbole wieloznaczne jako stałe do wyszukiwanego hasła w ten sposób:

f_unaccent(p.abrev) ILIKE f_unaccent('%' || 'vicenti' || '%')

('vicenti' zostanie zastąpiony parametrem). Postgres wie, że mamy do czynienia ze wzorcem, który nie jest zakotwiczony ani z lewej, ani z prawej strony – co pozwoliłoby na zastosowanie różnych strategii. Powiązana odpowiedź zawierająca więcej szczegółów:

Lub może ponownie zaplanować zapytanie dla każdego wyszukiwanego terminu (ewentualnie używając dynamicznego SQL w funkcji). Ale upewnij się, że czas planowania nie pochłania żadnego możliwego wzrostu wydajności.

GDZIE warunek na kolumnach w pess jest sprzeczne z LEFT JOIN . Postgres jest zmuszony przekonwertować to na INNER JOIN . Co gorsza, sprzężenie pojawia się późno w drzewie sprzężenia. A ponieważ Postgres nie może zmienić kolejności twoich przyłączeń (patrz poniżej), może to stać się bardzo kosztowne. Przenieś tabelę na pierwszą pozycja w OD klauzula o wczesnej eliminacji wierszy. Po LEFT JOIN s z definicji nie eliminują żadnych wierszy. Ale przy tak wielu tabelach ważne jest, aby przenieść złączenia, które mogą mnożyć się wiersze do końca.

Dołączasz do 13 stołów, 12 z nich z LEFT JOIN co pozostawia 12! możliwe kombinacje - lub 11! * 2! jeśli weźmiemy ten LEFT JOIN pod uwagę, że tak naprawdę jest to INNER JOIN . To za wiele, aby Postgres ocenił wszystkie możliwe permutacje w celu uzyskania najlepszego planu zapytań. Przeczytaj o join_collapse_limit :

Domyślne ustawienie join_collapse_limit to 8 , co oznacza, że ​​Postgres nie będzie próbował zmienić kolejności tabel w Twoim FROM klauzula i kolejność tabel jest istotna .

Jednym ze sposobów obejścia tego problemu byłoby podzielenie części krytycznej dla wydajności na CTE jak @joop skomentował . Nie ustawiaj join_collapse_limit znacznie wyższe lub czasy planowania zapytań obejmujące wiele połączonych tabel ulegną pogorszeniu.

O Twojej połączonej dacie o nazwie dane :

cast(cast(e.ano as varchar(4))||'-'||right('0'||cast(e.mes as varchar(2)),2)||' -'|| right('0'||cast(e.dia as varchar(2)),2) as varchar(10)) jako dane

Zakładając budujesz z trzech kolumn numerycznych dla roku, miesiąca i dnia, które są zdefiniowane NOT NULL , użyj tego zamiast:

e.ano::text || to_char(e.mes2, 'FM"-"00')
            || to_char(e.dia,  'FM"-"00') AS data

Informacje o FM modyfikator wzorca szablonu:

Ale tak naprawdę powinieneś przechowywać datę jako typ danych date na początek.

Również uproszczona:

format('%s (%s)', p.abrev, p.prenome) AS determinador

Nie przyspieszy zapytania, ale jest znacznie czystsze. Zobacz format() .

Na koniec wszystkie zwykłe porady dotyczące optymalizacji wydajności dotyczy:

Jeśli wszystko dobrze zrobisz, powinieneś zobaczyć znacznie szybsze zapytania dla wszystkich wzory.



  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Elegancka grupa PostgreSQL autorstwa Ruby on Rails / ActiveRecord

  2. Utwórz kopię zapasową/przywróć zadokowaną bazę danych PostgreSQL

  3. Tablica PHP do tablicy postgres

  4. Używanie row_to_json() z połączeniami zagnieżdżonymi

  5. Jak powiązać wyniki PGSeaerch ze stroną indeksową w zagnieżdżonych zasobach?