Už jste to použili více než 6000 XNUMXkrát, ale jedna užitečná funkce, která možná zůstala nepovšimnuta, je strukturální stopy, které vypadají nějak takto:
Poslouchejte je a vaše požadavky budou „hladké a hedvábné“. 🙂
Ale vážně, v mnoha situacích je požadavek pomalý a náročný na zdroje jsou typické a lze je rozpoznat podle struktury a dat plánu.
V tomto případě nemusí každý jednotlivý vývojář sám hledat možnost optimalizace, spoléhat se pouze na své zkušenosti – můžeme mu říci, co se zde děje, co by mohlo být důvodem a jak přistupovat k řešení. To jsme udělali.
Pojďme se na tyto případy podívat blíže – jak jsou definovány a k jakým doporučením vedou.
Abyste se lépe ponořili do tématu, můžete si nejprve poslechnout odpovídající blok od moje zpráva na PGConf.Russia 2020a teprve poté přejděte k podrobné analýze každého příkladu:
#1: index „podřazení“
Když se objeví
Ukažte poslední fakturu pro klienta "LLC Kolokolchik".
CREATE TABLE tbl AS
SELECT
generate_series(1, 100000) pk -- 100K "фактов"
, (random() * 1000)::integer fk_cli; -- 1K разных внешних ключей
CREATE INDEX ON tbl(fk_cli); -- индекс для foreign key
SELECT
*
FROM
tbl
WHERE
fk_cli = 1 -- отбор по конкретной связи
ORDER BY
pk DESC -- хотим всего одну "последнюю" запись
LIMIT 1;
I na tak primitivním vzorku - 8.5krát rychlejší a 33krát méně čtení. Čím více „faktů“ pro každou hodnotu máte, tím je efekt zjevnější fk.
Podotýkám, že takový index bude fungovat jako „prefixový“ index o nic horší než dříve pro jiné dotazy s fk, kde seřadit podle pk nebylo a není (můžete si o tom přečíst více v mém článku o hledání neúčinných indexů). Včetně bude poskytovat normální explicitní podpora cizích klíčů na tomto poli.
#2: průnik indexu (BitmapAnd)
Když se objeví
Zobrazit všechny smlouvy pro klienta „LLC Kolokolchik“, uzavřené jménem „NAO Buttercup“.
Jak identifikovat
-> BitmapAnd
-> Bitmap Index Scan
-> Bitmap Index Scan
Doporučení
vytvořit složený index o pole z obou původních nebo rozšířit jedno ze stávajících o pole z druhého.
Příklad:
CREATE TABLE tbl AS
SELECT
generate_series(1, 100000) pk -- 100K "фактов"
, (random() * 100)::integer fk_org -- 100 разных внешних ключей
, (random() * 1000)::integer fk_cli; -- 1K разных внешних ключей
CREATE INDEX ON tbl(fk_org); -- индекс для foreign key
CREATE INDEX ON tbl(fk_cli); -- индекс для foreign key
SELECT
*
FROM
tbl
WHERE
(fk_org, fk_cli) = (1, 999); -- отбор по конкретной паре
Výplata je zde menší, protože Bitmap Heap Scan je docela efektivní sám o sobě. Ale stejně 7krát rychlejší a 2.5krát méně čtení.
#3: Sloučit indexy (BitmapOr)
Když se objeví
Zobrazte prvních 20 nejstarších „nás“ nebo nepřiřazených požadavků ke zpracování, přičemž vaše má prioritu.
Jak identifikovat
-> BitmapOr
-> Bitmap Index Scan
-> Bitmap Index Scan
Doporučení
Chcete-li použít UNION [VŠE] zkombinovat poddotazy pro každý z bloků NEBO podmínek.
Příklad:
CREATE TABLE tbl AS
SELECT
generate_series(1, 100000) pk -- 100K "фактов"
, CASE
WHEN random() < 1::real/16 THEN NULL -- с вероятностью 1:16 запись "ничья"
ELSE (random() * 100)::integer -- 100 разных внешних ключей
END fk_own;
CREATE INDEX ON tbl(fk_own, pk); -- индекс с "вроде как подходящей" сортировкой
SELECT
*
FROM
tbl
WHERE
fk_own = 1 OR -- свои
fk_own IS NULL -- ... или "ничьи"
ORDER BY
pk
, (fk_own = 1) DESC -- сначала "свои"
LIMIT 20;
(
SELECT
*
FROM
tbl
WHERE
fk_own = 1 -- сначала "свои" 20
ORDER BY
pk
LIMIT 20
)
UNION ALL
(
SELECT
*
FROM
tbl
WHERE
fk_own IS NULL -- потом "ничьи" 20
ORDER BY
pk
LIMIT 20
)
LIMIT 20; -- но всего - 20, больше и не надо
Využili jsme toho, že všech 20 požadovaných záznamů bylo okamžitě přijato v prvním bloku, takže druhý, s „dražším“ Bitmap Heap Scan, se ani neprovedl - nakonec 22x rychlejší, 44x méně čtení!
Zpravidla vzniká, když chcete „připojit další filtr“ k již existujícímu požadavku.
„A ty nemáš stejný, ale s perleťovými knoflíky? " film "Diamantové rameno"
Například úpravou výše uvedené úlohy zobrazte prvních 20 nejstarších „kritických“ požadavků na zpracování, bez ohledu na jejich účel.
Jak identifikovat
-> Seq Scan | Bitmap Heap Scan | Index [Only] Scan [Backward]
&& 5 × rows < RRbF -- отфильтровано >80% прочитанного
&& loops × RRbF > 100 -- и при этом больше 100 записей суммарно
Doporučení
Vytvořte [více] specializované index s podmínkou WHERE nebo do indexu zahrnout další pole.
Pokud je stav filtru pro vaše účely "statický" - tzn neznamená expanzi seznam hodnot v budoucnu - je lepší použít index WHERE. Do této kategorie dobře zapadají různé booleovské/výčtové stavy.
Pokud je podmínka filtrování může nabývat různých významů, pak je lepší rozšířit index o tato pole – jako v situaci s BitmapAnd výše.
Příklad:
CREATE TABLE tbl AS
SELECT
generate_series(1, 100000) pk -- 100K "фактов"
, CASE
WHEN random() < 1::real/16 THEN NULL
ELSE (random() * 100)::integer -- 100 разных внешних ключей
END fk_own
, (random() < 1::real/50) critical; -- 1:50, что заявка "критичная"
CREATE INDEX ON tbl(pk);
CREATE INDEX ON tbl(fk_own, pk);
SELECT
*
FROM
tbl
WHERE
critical
ORDER BY
pk
LIMIT 20;
Jak vidíte, filtrování z plánu úplně zmizelo a požadavek se stal 5x rychleji.
#5: řídký stůl
Když se objeví
Různé pokusy o vytvoření vlastní fronty zpracování úloh, kdy velké množství aktualizací/mazání záznamů v tabulce vede k situaci velkého počtu „mrtvých“ záznamů.
Jak identifikovat
-> Seq Scan | Bitmap Heap Scan | Index [Only] Scan [Backward]
&& loops × (rows + RRbF) < (shared hit + shared read) × 8
-- прочитано больше 1KB на каждую запись
&& shared hit + shared read > 64
Doporučení
Pravidelně provádějte ručně VAKUUM [PLNÉ] nebo dosáhnout přiměřeně častého školení autovakuum doladěním jeho parametrů vč pro konkrétní stůl.
Zdá se, že jsme málo četli a vše bylo indexováno a nikoho jsme nadbytečně nefiltrovali – ale přesto čteme podstatně více stránek, než bychom chtěli.
Jak identifikovat
-> Index [Only] Scan [Backward]
&& loops × (rows + RRbF) < (shared hit + shared read) × 8
-- прочитано больше 1KB на каждую запись
&& shared hit + shared read > 64
Doporučení
Podívejte se pozorně na strukturu použitého indexu a klíčová pole uvedená v dotazu – s největší pravděpodobností část indexu není nastavena. S největší pravděpodobností budete muset vytvořit podobný index, ale bez prefixových polí resp naučit se opakovat jejich hodnoty.
Příklad:
CREATE TABLE tbl AS
SELECT
generate_series(1, 100000) pk -- 100K "фактов"
, (random() * 100)::integer fk_org -- 100 разных внешних ключей
, (random() * 1000)::integer fk_cli; -- 1K разных внешних ключей
CREATE INDEX ON tbl(fk_org, fk_cli); -- все почти как в #2
-- только вот отдельный индекс по fk_cli мы уже посчитали лишним и удалили
SELECT
*
FROM
tbl
WHERE
fk_cli = 999 -- а fk_org не задано, хотя стоит в индексе раньше
LIMIT 20;
Všechno se zdá být v pořádku, dokonce i podle indexu, ale je to nějak podezřelé - pro každý z 20 přečtených záznamů jsme museli odečíst 4 stránky dat, 32 kB na záznam - není to tučné? A název indexu tbl_fk_org_fk_cli_idx k zamyšlení.
Jednorázové zpracování (třídění nebo unifikace) velkého množství záznamů se nevejde do paměti k tomu určené.
Jak identifikovat
-> *
&& temp written > 0
Doporučení
Pokud množství paměti použité operací výrazně nepřekračuje zadanou hodnotu parametru work_mem, vyplatí se to opravit. Můžete okamžitě v konfiguraci pro všechny, nebo můžete projít SET [LOCAL] pro konkrétní požadavek/transakci.
Příklad:
SHOW work_mem;
-- "16MB"
SELECT
random()
FROM
generate_series(1, 1000000)
ORDER BY
1;
Čekalo se na uzamčení vyvolané konkurenčním požadavkem nebo byly nedostatečné hardwarové prostředky CPU/hypervizoru.
Jak identifikovat
-> *
&& (shared hit / 8K) + (shared read / 1K) < time / 1000
-- RAM hit = 64MB/s, HDD read = 8MB/s
&& time > 100ms -- читали мало, но слишком долго
Doporučení
Použijte externí monitorovací systém server kvůli blokování nebo abnormální spotřebě zdrojů. Již jsme hovořili o naší verzi organizace tohoto procesu pro stovky serverů zde и zde.