Mam taki stół:
CREATE TABLE products (
id serial PRIMARY KEY,
category_ids integer[],
published boolean NOT NULL,
score integer NOT NULL,
title varchar NOT NULL);
Produkt może należeć do wielu kategorii. category_ids
kolumna zawiera listę identyfikatorów wszystkich kategorii produktów.
Typowe zapytanie wygląda następująco (zawsze szuka pojedynczej kategorii):
SELECT * FROM products WHERE published
AND category_ids @> ARRAY[23465]
ORDER BY score DESC, title
LIMIT 20 OFFSET 8000;
Aby przyspieszyć, używam następującego indeksu:
CREATE INDEX idx_test1 ON products
USING GIN (category_ids gin__int_ops) WHERE published;
Ten bardzo pomaga, chyba że w jednej kategorii jest zbyt wiele produktów. Szybko odfiltrowuje produkty należące do tej kategorii, ale następnie istnieje operacja sortowania, która musi zostać wykonana w sposób trudny (bez indeksu).
Zainstalowałem btree_gin
rozszerzenie pozwalające mi na zbudowanie wielokolumnowego indeksu GIN w następujący sposób:
CREATE INDEX idx_test2 ON products USING GIN (
category_ids gin__int_ops, score, title) WHERE published;
Ale Postgres nie chce tego używać do sortowania . Nawet gdy usunę DESC
specyfikator w zapytaniu.
Wszelkie alternatywne podejścia do optymalizacji zadania są bardzo mile widziane.
Dodatkowe informacje:
- PostgreSQL 9.4, z rozszerzeniem intarray
- całkowita liczba produktów wynosi obecnie 260 tys., ale oczekuje się znacznego wzrostu (do 10 mln, jest to platforma e-commerce dla wielu najemców)
- produkty według kategorii 1..10000 (może wzrosnąć do 100 tys.), średnia jest poniżej 100, ale te kategorie z dużą liczbą produktów zwykle przyciągają znacznie więcej zapytań
Poniższy plan zapytań uzyskano z mniejszego systemu testowego (4680 produktów w wybranej kategorii, łącznie 200 000 produktów w tabeli):
Limit (cost=948.99..948.99 rows=1 width=72) (actual time=82.330..82.341 rows=20 loops=1)
-> Sort (cost=948.37..948.99 rows=245 width=72) (actual time=80.231..81.337 rows=4020 loops=1)
Sort Key: score, title
Sort Method: quicksort Memory: 928kB
-> Bitmap Heap Scan on products (cost=13.90..938.65 rows=245 width=72) (actual time=1.919..16.044 rows=4680 loops=1)
Recheck Cond: ((category_ids @> '{292844}'::integer[]) AND published)
Heap Blocks: exact=3441
-> Bitmap Index Scan on idx_test2 (cost=0.00..13.84 rows=245 width=0) (actual time=1.185..1.185 rows=4680 loops=1)
Index Cond: (category_ids @> '{292844}'::integer[])
Planning time: 0.202 ms
Execution time: 82.404 ms
Uwaga 1 : 82 ms może nie wyglądać tak przerażająco, ale dlatego, że bufor sortowania pasuje do pamięci. Po wybraniu wszystkich kolumn z tabeli produktów ( SELECT * FROM ...
w rzeczywistości jest ich około 60), Sort Method: external merge Disk: 5696kB
czas wykonania ulega podwojeniu. Dotyczy to tylko 4680 produktów.
Punkt działania nr 1 (pochodzi z uwagi nr 1): Aby zmniejszyć ślad pamięci operacji sortowania, a tym samym nieco ją przyspieszyć, rozsądnie byłoby najpierw pobrać, posortować i ograniczyć identyfikatory produktów, a następnie pobrać pełne rekordy:
SELECT * FROM products WHERE id IN (
SELECT id FROM products WHERE published AND category_ids @> ARRAY[23465]
ORDER BY score DESC, title LIMIT 20 OFFSET 8000
) ORDER BY score DESC, title;
To sprowadza nas z powrotem do Sort Method: quicksort Memory: 903kB
~ 80 ms dla 4680 produktów. Nadal może być powolny, gdy liczba produktów wzrośnie do 100 tys.
źródło
score
może być NULL, ale nadal sortujesz wedługscore DESC
, niescore DESC NULLS LAST
. Jedno lub drugie wydaje się niewłaściwe ...score
faktycznie NIE jest NULL - poprawiłem definicję tabeli.Odpowiedzi:
Przeprowadziłem wiele eksperymentów i oto moje ustalenia.
GIN i sortowanie
Indeks GIN obecnie (od wersji 9.4) nie może pomóc w zamówieniu .
work_mem
Dzięki Chris za wskazanie tego parametru konfiguracyjnego . Domyślnie wynosi 4 MB, a jeśli Twój zestaw rekordów jest większy, zwiększenie
work_mem
do odpowiedniej wartości (można ją znaleźć wEXPLAIN ANALYSE
) może znacznie przyspieszyć operacje sortowania.Uruchom ponownie serwer, aby zmiany zaczęły obowiązywać, a następnie sprawdź dwukrotnie:
Oryginalne zapytanie
Zapełniłem bazę danych 650 000 produktów, a niektóre kategorie zawierały do 40 000 produktów. Uprościłem trochę zapytanie, usuwając
published
klauzulę:Jak widzimy,
work_mem
nie wystarczyło, więc mieliśmySort Method: external merge Disk: 29656kB
(liczba tutaj jest przybliżona, potrzebuje nieco więcej niż 32 MB na szybkie sortowanie w pamięci).Zmniejsz ślad pamięci
Nie wybieraj pełnych rekordów do sortowania, używaj identyfikatorów, stosuj sortowanie, przesunięcie i ograniczenie, a następnie załaduj tylko 10 rekordów, których potrzebujemy:
Uwaga
Sort Method: quicksort Memory: 7396kB
. Wynik jest znacznie lepszy.DOŁĄCZ i dodatkowy indeks B-drzewa
Zgodnie z radą Chrisa stworzyłem dodatkowy indeks:
Najpierw próbowałem dołączyć w ten sposób:
Plan zapytań różni się nieznacznie, ale wynik jest taki sam:
Grając z różnymi przesunięciami i liczbą produktów, nie mogłem zmusić PostgreSQL do korzystania z dodatkowego indeksu B-drzewa.
Więc poszedłem klasycznie i stworzyłem tabelę połączeń :
Nadal nie używa indeksu B-drzewa, zestaw wyników nie pasował
work_mem
, a zatem słabe wyniki.Ale w niektórych okolicznościach, mając dużą liczbę produktów i mały offset PostgreSQL decyduje się teraz na użycie indeksu B-drzewa:
Jest to w rzeczywistości dość logiczne, ponieważ indeks B-drzewa tutaj nie daje bezpośredniego wyniku, służy jedynie jako przewodnik dla skanowania sekwencyjnego.
Porównajmy z zapytaniem GIN:
Wynik GIN jest znacznie lepszy. Sprawdziłem różne kombinacje liczby produktów i przesunięć, w żadnym wypadku podejście do tabeli połączeń nie było lepsze .
Moc prawdziwego indeksu
Aby PostgreSQL mógł w pełni wykorzystywać indeks do sortowania, wszystkie
WHERE
parametry zapytania orazORDER BY
parametry muszą znajdować się w jednym indeksie B-drzewa. Aby to zrobić, skopiowałem pola sortowania z produktu do tabeli połączeń:I to jest najgorszy scenariusz z dużą liczbą produktów w wybranej kategorii i dużym offsetem. Gdy offset = 300 czas wykonania wynosi zaledwie 0,5 ms.
Niestety utrzymanie takiej tabeli połączeń wymaga dodatkowego wysiłku. Można to osiągnąć za pomocą indeksowanych widoków zmaterializowanych, ale jest to przydatne tylko wtedy, gdy dane są aktualizowane rzadko, ponieważ odświeżenie takiego zmaterializowanego widoku jest dość ciężką operacją.
Pozostaję więc do tej pory z indeksem GIN, ze zwiększonym
work_mem
i zmniejszonym zapytaniem o wielkość pamięci.źródło
work_mem
nastawienia w postgresql.conf. Załaduj ponownie wystarczy. I ostrzegam przedwork_mem
zbyt wysokim globalnym ustawieniem w środowisku wielu użytkowników (również niezbyt niskim). Jeśli masz jakieś zapytania wymagające więcejwork_mem
, ustaw je wyżej tylko dla sesjiSET
- lub tylko dla transakcjiSET LOCAL
. Zobacz: dba.stackexchange.com/a/48633/3684Oto kilka krótkich wskazówek, które mogą pomóc poprawić wydajność. Zacznę od najłatwiejszej wskazówki, która z twojej strony jest prawie bez wysiłku, i przejdę do trudniejszej wskazówki po pierwszej.
1.
work_mem
Widzę więc od razu, że rodzaj zgłoszony w twoim planie wyjaśniania
Sort Method: external merge Disk: 5696kB
zużywa mniej niż 6 MB, ale rozlewa się na dysk. Musisz zwiększyćwork_mem
ustawienie w swoimpostgresql.conf
pliku, aby było wystarczająco duże, aby sortowanie mieściło się w pamięci.EDYCJA: Ponadto, po dalszej inspekcji, widzę, że po użyciu indeksu do sprawdzenia,
catgory_ids
które pasują do twoich kryteriów, skanowanie indeksu bitmap jest zmuszone stać się „stratne” i musi ponownie sprawdzić stan podczas odczytywania wierszy z odpowiednich stron stosu . Zapoznaj się z tym postem na postgresql.org w celu uzyskania lepszego wyjaśnienia niż ja. : P Najważniejsze jest to, że twój poziomwork_mem
jest zdecydowanie za niski. Jeśli nie dostroiłeś ustawień domyślnych na serwerze, nie będzie on działał dobrze.Ta poprawka nie zajmie Ci dużo czasu. Jedna zmiana na
postgresql.conf
i już nie ma! Więcej wskazówek znajdziesz na tej stronie dostrajania wydajności .2. Zmiana schematu
Tak więc w projekcie schematu podjąłeś decyzję o denormalizacji
category_ids
tablicy na liczbę całkowitą, co następnie zmusza cię do użycia indeksu GIN lub GIST w celu uzyskania szybkiego dostępu. Z mojego doświadczenia wynika, że wybór indeksu GIN będzie szybszy dla odczytów niż GIST, więc w takim przypadku dokonałeś właściwego wyboru. Jednak GIN jest nieposortowanym indeksem; myśleć bardziej jak klucz-wartość, gdzie predykaty równość to łatwe do sprawdzenia, ale operacji, takich jakWHERE >
,WHERE <
lubORDER BY
nie są ułatwione przez indeks.Przyzwoite podejście polegałoby na znormalizowaniu projektu przy użyciu tabeli mostów / tabeli połączeń , służącej do określania relacji wiele do wielu w bazach danych.
W takim przypadku masz wiele kategorii i zestaw odpowiednich liczb całkowitych
category_id
oraz wiele produktów i odpowiadających im liczb całkowitychproduct_id
. Zamiast kolumny w tabeli produktów, która jest tablicą liczb całkowitychcategory_id
s, usuń tę kolumnę tablicy ze schematu i utwórz tabelę jakoNastępnie możesz wygenerować indeksy B-drzewa na dwóch kolumnach tabeli mostu,
Tylko moja skromna opinia, ale te zmiany mogą mieć dla ciebie dużą różnicę.
work_mem
Przynajmniej wypróbuj tę zmianę.Powodzenia!
EDYTOWAĆ:
Zbuduj dodatkowy indeks ułatwiający sortowanie
Tak więc, jeśli z czasem Twoja linia produktów się rozszerzy, niektóre zapytania mogą zwrócić wiele wyników (tysiące, dziesiątki tysięcy?), Ale które nadal mogą być tylko niewielkim podzbiorem całej linii produktów. W takich przypadkach sortowanie może być nawet dość kosztowne, jeśli jest wykonywane w pamięci, ale do ułatwienia sortowania można użyć odpowiednio zaprojektowanego indeksu.
Zobacz oficjalną dokumentację PostgreSQL opisującą indeksy i ORDER BY .
Jeśli utworzysz indeks pasujący do twoich
ORDER BY
wymagańnastępnie Postgres zoptymalizuje i zdecyduje, czy użycie indeksu lub wykonanie jawnego sortowania będzie bardziej opłacalne. Pamiętaj, że nie ma gwarancji, że Postgres będzie korzystać z indeksu; będzie dążyć do optymalizacji wydajności i wybierać między użyciem indeksu lub jawnym sortowaniem. Jeśli utworzysz ten indeks, monitoruj go, aby sprawdzić, czy jest on wystarczająco używany, aby uzasadnić jego utworzenie, i upuść go, jeśli większość twoich działań jest wykonywana jawnie.
Nadal jednak w tym momencie Twoje zaangażowanie w „największy huk za grosze” będzie prawdopodobnie zużywać więcej
work_mem
, ale są przypadki, w których indeks mógłby wspierać sortowanie.źródło
work_mem
konfiguracji miała na celu usunięcie problemu „sortowania na dysku”, a także problemu ze sprawdzaniem stanu. W miarę wzrostu liczby produktów może być potrzebny dodatkowy indeks do sortowania. Wyjaśnij moje zmiany powyżej.