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 . Postgres jest zmuszony przekonwertować to na LEFT JOIN
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
:
- Przykładowe zapytanie pokazujące błąd szacowania kardynalności w PostgreSQL
- SQL INNER JOIN w wielu tabelach o składni WHERE
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.