Mám stůl s 7,2 milionu tuplů, který vypadá takto:
table public.methods
column | type | attributes
--------+-----------------------+----------------------------------------------------
id | integer | not null DEFAULT nextval('methodkey'::regclass)
hash | character varying(32) | not null
string | character varying | not null
method | character varying | not null
file | character varying | not null
type | character varying | not null
Indexes:
"methods_pkey" PRIMARY KEY, btree (id)
"methodhash" btree (hash)
Nyní chci vybrat některé hodnoty, ale dotaz je neuvěřitelně pomalý:
db=# explain
select hash, string, count(method)
from methods
where hash not in
(select hash from nostring)
group by hash, string
order by count(method) desc;
QUERY PLAN
----------------------------------------------------------------------------------------
Sort (cost=160245190041.10..160245190962.07 rows=368391 width=182)
Sort Key: (count(methods.method))
-> GroupAggregate (cost=160245017241.77..160245057764.73 rows=368391 width=182)
-> Sort (cost=160245017241.77..160245026451.53 rows=3683905 width=182)
Sort Key: methods.hash, methods.string
-> Seq Scan on methods (cost=0.00..160243305942.27 rows=3683905 width=182)
Filter: (NOT (SubPlan 1))
SubPlan 1
-> Materialize (cost=0.00..41071.54 rows=970636 width=33)
-> Seq Scan on nostring (cost=0.00..28634.36 rows=970636 width=33)
Sloupec hash
je hash md5 string
a má index. Takže si myslím, že můj problém je, že celá tabulka je tříděna podle id a ne podle hashe, takže chvíli to nejprve setřídíme a poté seskupíme?
Tabulka nostring
obsahuje pouze seznam hashů, které nechci mít. Ale potřebuji obě tabulky, aby měly všechny hodnoty. Není tedy možné je odstranit.
další informace: žádný ze sloupců nemůže být null (opraveno jako v definici tabulky) a i'm pomocí postgresql 9.2.
Hodnota LEFT JOIN
V @ dezso odpověď by měla být dobrá. Index však bude stěží užitečný (sám o sobě), protože dotaz musí přesto přečíst celou tabulku - výjimkou jsou skenování pouze indexu v Postgresu 9.2+ a příznivé podmínky, viz níže.
SELECT m.hash, m.string, count(m.method) AS method_ct
FROM methods m
LEFT JOIN nostring n USING (hash)
WHERE n.hash IS NULL
GROUP BY m.hash, m.string
ORDER BY count(m.method) DESC;
Na dotaz spusťte EXPLAIN ANALYZE
. Několikrát vyloučit efekty výplaty a šum. Porovnejte nejlepší výsledky.
Vytvořte index s více sloupci, který odpovídá vašemu dotazu:
CREATE INDEX methods_cluster_idx ON methods (hash, string, method);
Počkejte? Poté, co jsem řekl, index by nepomohl? Potřebujeme to k CLUSTER
tabulce:
CLUSTER methods USING methods_cluster_idx;
ANALYZE methods;
Znovu spusťte EXPLAIN ANALYZE
. Rychleji? To by mělo být.
CLUSTER
je jednorázová operace, která přepíše celou tabulku v pořadí podle použitého indexu. Je to také efektivně VACUUM FULL
. Pokud si chcete být jisti, spustili byste předběžný test pouze s VACUUM FULL
, Abyste zjistili, co lze k tomu připsat.
Pokud vaše tabulka vidí mnoho operací zápisu, účinek se časem sníží. Naplánujte CLUSTER
mimo pracovní dobu, chcete-li efekt obnovit. Jemné doladění závisí na konkrétním případu použití. Manuál o CLUSTER
.
CLUSTER
je poměrně hrubý nástroj, potřebuje exkluzivní zámek na stole. Pokud si to nemůžete dovolit, zvažte pg_repack
, který může udělat totéž bez výhradního zámku. Více v této pozdější odpovědi:
Pokud je procento NULL
hodnot ve sloupci method
vysoké (více než ~ 20 procent, v závislosti na u skutečných velikostí řádků) by měl částečný index pomoci:
CREATE INDEX methods_foo_idx ON methods (hash, string)
WHERE method IS NOT NULL;
(Vaše pozdější aktualizace ukazuje, že vaše sloupce jsou NOT NULL
, Takže se nepoužijí.)
Pokud používáte PostgreSQL 9.2 nebo novější (jako @deszo komentoval ) uvedené indexy mohou být užitečné bez CLUSTER
, pokud plánovač může použít skenování pouze indexů . Použitelné pouze za příznivých podmínek: Indexem nesmí být pokryty žádné operace zápisu, které by ovlivnily mapu viditelnosti od posledního VACUUM
a všech sloupců v dotazu. V podstatě to mohou používat tabulky jen pro čtení, zatímco silně psané tabulky jsou omezené. Více podrobností v Postgres Wiki.
Výše uvedený částečný index by v tomto případě mohl být ještě užitečnější.
Pokud , na druhé straně jsou ve sloupci žádné NULL
hodnoty method
, měli byste
1..) Definujte NOT NULL
A
2..) Použijte count(*)
namísto count(method)
, což je o něco rychlejší a to samé při absenci NULL
hodnot.
Pokud musíte tento dotaz volat často a tabulka je pouze pro čtení, vytvořte MATERIALIZED VIEW
.
Exotický jemný bod: Tabulka se jmenuje nostring
, zdá se však, že obsahuje hash. Vyloučením hashů namísto řetězců existuje šance, že vyloučíte více řetězců, než bylo zamýšleno. Extrémně nepravděpodobné, ale možné.
Vítejte v DBA.SE!
Můžete zkusit přeformulovat svůj dotaz takto:
SELECT m.hash, string, count(method)
FROM
methods m
LEFT JOIN nostring n ON m.hash = n.hash
WHERE n.hash IS NULL
GROUP BY hash, string
ORDER BY count(method) DESC;
nebo jiná možnost:
SELECT m.hash, string, count(method)
FROM
methods m
WHERE NOT EXISTS (SELECT hash FROM nostring WHERE hash = m.hash)
GROUP BY hash, string
ORDER BY count(method) DESC;
NOT IN
je typický dřez výkonu, protože je obtížné s ním používat index.
To může být dále vylepšeno indexy. Index na nostring.hash
vypadá užitečné. Ale nejprve: co teď dostanete? (Bylo by lepší vidět výstup EXPLAIN ANALYZE
vzhledem k tomu, že samotné náklady neříkají dobu, kterou operace trvalo.)
Protože hash je md5, můžete se jej pravděpodobně pokusit převést na číslo: můžete jej uložit jako číslo nebo vytvořit funkční index, který vypočítá toto číslo v neměnné funkci.
Ostatní lidé již vytvořili funkci pl/pgsql, která převádí (část) hodnoty md5 z textu na řetězec. Viz https://stackoverflow.com/questions/9809381/hashing-a-string-to-a-numeric-value-in-postgressql pro příklad
Věřím, že při skenování indexu trávíte opravdu hodně času porovnáváním řetězců. Pokud se vám podaří tuto hodnotu uložit jako číslo, pak by to mělo být opravdu opravdu rychlejší.
Hodně jsem narazil na tento problém, a objevil jednoduchý 2-dílný trik.
Vytvořte index podřetězce na hodnotě hash: (7 je obvykle dobrá délka)
create index methods_idx_hash_substring ON methods(substring(hash,1,7))
Nechte vaše hledání/spojení zahrnovat shodu podřetězců, takže plánovač dotazů je naznačen, aby používal index:
starý: WHERE hash = :kwarg
nový: WHERE (hash = :kwarg) AND (substring(hash,1,7) = substring(:kwarg,1,7))
Také byste měli mít index na surovém hash
.
výsledkem (obvykle) je, že plánovač nejprve konzultuje index podřetězce a vyřadí většinu řádků. pak odpovídá hashe 32 znaků s odpovídajícím indexem (nebo tabulkou). tento přístup pro mě snížil 800ms dotazů na 4.