14 β Indeks strategiyasi¶
β¬ οΈ Oldingi: 13 β Anti-naqshlar: nima qilmaslik kerak Β· π README Β· Keyingi: 15 β Sxema va so'rov performansi (EXPLAIN ANALYZE) β‘οΈ
Bu bobda: indeks endi shunchaki "qidiruvni tezlashtiruvchi narsa" emas β uni DIZAYN qarori sifatida ko'ramiz. PostgreSQL ning beshta indeks turini (B-tree, Hash, GiST, GIN, BRIN) va har birini qachon ishlatishni o'rganamiz; kompozit indeksda ustun tartibini to'g'ri tanlashni, qisman (partial), ifoda (expression) va covering (INCLUDE) indekslarni loyihalashni; indeksning haqiqiy narxini, ortiqcha indeksni topishni va FK ustuniga indeks qo'yishni ko'ramiz. Hammasini PG 18 da
EXPLAIN (ANALYZE)bilan o'z ko'zimiz bilan tekshiramiz.
0. Bu bob SQL kitobidan nimasi bilan farq qiladi¶
SQL kitobining 21-bobida indeks nima ekanini, CREATE INDEX sintaksisini, B-tree daraxtini va chap prefiks qoidasini ko'rgansiz. U yerda asosiy savol edi: "indeks qanday ishlaydi va qanday yaratiladi?"
Bu bobning savoli boshqacha: "qaysi indeksni, qaysi ustunga, qaysi tartibda, qaysi tur bilan loyihalash kerak?" Bu β sxema dizaynining bir qismi. Yomon tanlangan indeks jadvalingizni sekinlashtiradi, joyni isrof qiladi va sizni "indeks bor-ku, nega ishlamayapti?" degan tuzoqqa soladi.
Shuning uchun bu yerda biz CREATE INDEX sintaksisini qayta o'rgatmaymiz β uni ishlatamiz. Diqqat β qarorda: nima uchun aynan shu indeks.
Falsafa: indeks β bepul tezlik emas. Har bir indeks o'qishni tezlashtiradi, lekin yozishni (INSERT/UPDATE/DELETE) sekinlashtiradi va disk joy egallaydi. Indeks dizayni β bu o'qish va yozish o'rtasidagi savdolashuv (trade-off). "Har ehtimolga" indeks qo'yish β anti-naqsh.
Bu bobning hamma misollari PostgreSQL 18.4 da, 100 000 qatorli buyurtma jadvalida haqiqatan ishga tushirilgan. Reja:
CREATE SCHEMA IF NOT EXISTS ch14;
SET search_path = ch14;
CREATE TABLE buyurtma (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
mijoz_id int NOT NULL,
holat text NOT NULL,
summa numeric(12,2) NOT NULL,
email text NOT NULL,
teglar text[] NOT NULL, -- ko'p qiymatli: massiv
xususiyat jsonb NOT NULL, -- {"rang": "...", "olcham": "..."}
yaratilgan timestamptz NOT NULL,
ochirilgan timestamptz -- soft delete: NULL = faol
);
-- 100 000 qatorni generate_series bilan yasaymiz:
INSERT INTO buyurtma (mijoz_id, holat, summa, email, teglar, xususiyat, yaratilgan, ochirilgan)
SELECT
(random()*20000)::int + 1,
(ARRAY['yangi','tolangan','jonatilgan','yetkazilgan','bekor'])[(random()*4)::int + 1],
(random()*5000)::numeric(12,2),
'User' || g || '@shop.uz',
ARRAY[ (ARRAY['aksiya','vip','tezkor','sovga','optom'])[(random()*4)::int+1],
(ARRAY['aksiya','vip','tezkor','sovga','optom'])[(random()*4)::int+1] ],
jsonb_build_object('rang', (ARRAY['qizil','kok','yashil'])[(random()*2)::int+1],
'olcham', (ARRAY['S','M','L','XL'])[(random()*3)::int+1]),
timestamptz '2024-01-01' + (g || ' minutes')::interval,
CASE WHEN random() < 0.05 THEN timestamptz '2025-01-01' ELSE NULL END
FROM generate_series(1, 100000) AS g;
ANALYZE buyurtma; -- statistikani yangilash (planner uchun MUHIM)
generate_series(1, 100000) β bu PG ning bir qatordan 100 000 qator yasash usuli (SQL kitobidagi WITH RECURSIVE ga teng, lekin ancha qulay). ANALYZE β planner to'g'ri qaror chiqarishi uchun jadval statistikasini yangilaydi; uni unutsangiz, plan noto'g'ri bo'lishi mumkin.
Bizning jadvalda atayin har xil shaklli ustunlar bor: tartiblangan vaqt (
yaratilgan-> BRIN uchun), massiv va JSONB (teglar,xususiyat-> GIN uchun), email (expressionindeks uchun),ochirilgan(partial indeks uchun). Har bir indeks turini shu jadvalda namoyish qilamiz.
1. Seq scan vs index scan β nima uchun umuman indeks kerak¶
Avval indekssiz holatga qaraylik. Bitta mijozning buyurtmalarini topamiz:
PG 18 chiqishi (qisqartirilgan, vaqtlar taxminan):
Seq Scan on buyurtma (actual rows=4.00 loops=1)
Filter: (mijoz_id = 12345)
Rows Removed by Filter: 99996
Buffers: shared hit=2138
Diqqat qiling: Seq Scan β butun jadvalni ketma-ket o'qidi, Rows Removed by Filter: 99996 β 4 ta kerakli qatorni topish uchun 99 996 qatorni tashlab yubordi. Buffers: shared hit=2138 β 2138 ta sahifa o'qildi.
Endi B-tree indeks qo'yamiz:
CREATE INDEX idx_buyurtma_mijoz ON buyurtma(mijoz_id);
EXPLAIN (ANALYZE, COSTS OFF) SELECT * FROM buyurtma WHERE mijoz_id = 12345;
Bitmap Heap Scan on buyurtma (actual rows=4.00 loops=1)
Recheck Cond: (mijoz_id = 12345)
Heap Blocks: exact=4
Buffers: shared hit=4 read=2
-> Bitmap Index Scan on idx_buyurtma_mijoz (actual rows=4.00 loops=1)
Index Cond: (mijoz_id = 12345)
Mana farq: Buffers 2138 dan 6 ga tushdi. PG endi butun jadvalni emas, faqat indeksni va kerakli 4 sahifani o'qiyapti. Million qatorli jadvalda bu farq "yarim soniya" va "ko'z ochib yumguncha" o'rtasidagi farqdir.
Bitmap Heap Scan β PG bir nechta qatorni topish kerak bo'lganda tanlaydigan rejim: avval indeksdan qatorlar joyini "bitmap" ga yig'adi, keyin jadvaldan o'qiydi. Bitta qator uchun esa to'g'ridan-to'g'ri Index Scan ishlatiladi. Rejalarni o'qishni keyingi 15-bobda chuqurroq ko'ramiz.
Dizayn xulosasi: indeks katta jadvalda WHERE/JOIN/ORDER BY da tez-tez ishlatiladigan ustun uchun kerak. Kichik jadvalda (bir necha ming qator) PG ko'pincha indeksdan ko'ra seq scan ni afzal ko'radi β va to'g'ri qiladi, chunki butun jadvalni o'qish baribir arzon.
2. Indeks turlari: qaysi birini qachon¶
PostgreSQL bir nechta indeks turini taklif qiladi. Aksariyat dasturchilar faqat B-tree ni biladi β lekin to'g'ri tur tanlash ba'zan o'nlab baravar farq qiladi.
2.1 B-tree β standart tanlov (90% holat)¶
CREATE INDEX da tur ko'rsatmasangiz, PG B-tree yaratadi. U deyarli hamma narsani qoplaydi:
- Tenglik:
WHERE x = 5 - Oraliq:
WHERE x > 5,BETWEEN,<,>= ORDER BY x(saralangan natija β indeks allaqachon tartiblangan)IS NULL, chap prefiks (kompozit)
PK, FK, UNIQUE, sana, narx, ism β bularning hammasi B-tree. Qoida: shubha bo'lsa, B-tree.
2.2 Hash β faqat tenglik¶
Hash indeks faqat = ni qo'llaydi β oraliq ham, ORDER BY ham yo'q. Nazariy jihatdan juda uzun matn yoki UUID tengligida B-tree dan biroz tejamliroq. Amalda esa B-tree odatda yetarli va ko'p qirrali, shuning uchun Hash kam ishlatiladi. Bilib qo'ying β lekin sukut bo'yicha B-tree ni tanlang.
2.3 GIN β ko'p qiymatli ustun (JSONB, array, full-text)¶
Bitta katakda ko'p element bo'lsa (massiv, JSONB hujjat, matn so'zlari), B-tree ojiz β u butun qiymatni yaxlit kalit deb biladi, ichidagi elementni topa olmaydi. Bu yerda GIN (Generalized Inverted iNdex) kerak.
Massiv ichida qidiramiz β "vip" tegi bor buyurtmalar:
CREATE INDEX idx_buyurtma_teglar ON buyurtma USING gin (teglar);
EXPLAIN (ANALYZE, COSTS OFF) SELECT count(*) FROM buyurtma WHERE teglar @> ARRAY['vip'];
Aggregate (actual rows=1.00 loops=1)
-> Bitmap Heap Scan on buyurtma (actual rows=43776.00 loops=1)
Recheck Cond: (teglar @> '{vip}'::text[])
-> Bitmap Index Scan on idx_buyurtma_teglar (actual rows=43776.00 loops=1)
Index Cond: (teglar @> '{vip}'::text[])
@> β "o'z ichiga oladi" operatori; GIN aynan shunga moslangan. JSONB uchun ham xuddi shunday:
CREATE INDEX idx_buyurtma_xususiyat ON buyurtma USING gin (xususiyat);
EXPLAIN (ANALYZE, COSTS OFF)
SELECT count(*) FROM buyurtma WHERE xususiyat @> '{"rang": "qizil"}';
Aggregate (actual rows=1.00 loops=1)
-> Bitmap Heap Scan on buyurtma (actual rows=24921.00 loops=1)
Recheck Cond: (xususiyat @> '{"rang": "qizil"}'::jsonb)
-> Bitmap Index Scan on idx_buyurtma_xususiyat (actual rows=24921.00 loops=1)
Dizayn eslatmasi: JSONB ga GIN qo'yishdan oldin so'rang β "bu ma'lumot haqiqatan JSONB bo'lishi kerakmi, yoki oddiy ustunlar yaxshiroqmi?" (13-bobdagi EAV anti-naqshini eslang). Agar JSONB to'g'ri qaror bo'lsa va ichidan qidirsangiz β GIN majburiy. Faqat bitta yo'l (
xususiyat->>'rang') bo'yicha qidirsangiz, expression indeks (3.3) yengilroq.
2.4 BRIN β ulkan, tabiiy tartiblangan jadval (vaqt!)¶
yaratilgan ustuni vaqt bo'yicha o'sib boradi (yangi buyurtma β har doim oxirgi vaqt). Bunday tabiiy tartiblangan ustun uchun BRIN (Block Range INdex) bor β u har bir disk bloki uchun faqat "min-max" oralig'ini saqlaydi, shuning uchun ulkan jadvalda ham juda kichik bo'ladi:
CREATE INDEX idx_buyurtma_sana_brin ON buyurtma USING brin (yaratilgan);
SELECT pg_size_pretty(pg_relation_size('idx_buyurtma_sana_brin')) AS brin,
pg_size_pretty(pg_relation_size('idx_buyurtma_lower_email')) AS btree;
24 kB vs 3992 kB β BRIN B-tree dan ~160 baravar kichik! Va u oraliq so'rovni quvvatlaydi:
EXPLAIN (ANALYZE, COSTS OFF)
SELECT count(*) FROM buyurtma WHERE yaratilgan BETWEEN '2024-02-01' AND '2024-02-05';
Aggregate (actual rows=1.00 loops=1)
-> Bitmap Heap Scan on buyurtma (actual rows=5761.00 loops=1)
Recheck Cond: ((yaratilgan >= ...) AND (yaratilgan <= ...))
Rows Removed by Index Recheck: 12202
-> Bitmap Index Scan on idx_buyurtma_sana_brin (actual rows=3840.00 loops=1)
Rows Removed by Index Recheck: 12202 β BRIN "taxminiy": u blokni "ehtimol mos" deb belgilaydi, keyin PG aniqlab tekshiradi. Shuning uchun BRIN faqat tartiblangan ustunda foydali. Agar ustun tartibsiz (random) bo'lsa, har blokda min-max butun diapazonni qoplaydi va indeks foydasiz. BRIN ning ideal o'rni β vaqt ustunli ulkan log/o'lchov jadvallari (millionlab+ qator).
2.5 GiST β geometrik, oraliq, "kesishmaslik"¶
GiST geometrik ma'lumot (PostGIS), range turlari va "eng yaqin" qidiruv uchun. Eng amaliy dizayn ishlatilishi β EXCLUDE constraint (11-bobda ko'rgan xona band-bo'sh, vaqt oralig'i kesishmasligi). U yerdagi EXCLUDE USING gist (...) aynan GiST indeksiga tayanadi. Full-text qidiruvni ham (kichik hajmda) quvvatlaydi.
2.6 SP-GiST β maxsus, notekis taqsimotli¶
SP-GiST notekis taqsimlangan, daraxtsimon ma'lumot uchun: IP prefiks (inet), telefon prefiks, fazoviy nuqtalar (quadtree). Kam uchraydi β shunday ehtiyoj paydo bo'lsa, "bunaqasi ham bor" deb eslang. Aksariyat loyihada hech qachon kerak bo'lmaydi.
| Tur | Qachon | Operatorlar |
|---|---|---|
| B-tree | standart: tenglik, oraliq, ORDER BY | = < > BETWEEN IS NULL |
| Hash | faqat tenglik (kam ishlatiladi) | = |
| GiST | geometrik, range, EXCLUDE, yaqinlik | && @> <-> |
| GIN | JSONB, array, full-text (ko'p qiymatli) | @> ? && @@ |
| BRIN | ulkan + tabiiy tartiblangan (vaqt) | < > BETWEEN |
| SP-GiST | notekis taqsimot (IP, prefiks) | = << @> |
MySQL farqi: InnoDB faqat B-tree ni (va
FULLTEXT,SPATIALmaxsus turlarini) qo'llaydi; GIN/BRIN/GiST PG ga xos. MySQL da JSON ustunidan tez qidirish uchun odatda generated column + B-tree ishlatiladi.
3. Kompozit, qisman va ifoda indekslari β dizayn nozikliklari¶
3.1 Kompozit indeks: ustun TARTIBI qaror¶
Bir nechta ustunni qamragan indeksda ustunlar tartibi β eng ko'p xato qilinadigan joy. (mijoz_id, yaratilgan) va (yaratilgan, mijoz_id) β bu ikki boshqa indeks, bir-birini almashtira olmaydi.
Indeks avval mijoz_id bo'yicha, har guruh ichida yaratilgan bo'yicha saralangan. Demak chap prefiks qoidasi (SQL kitobida ko'rgan): indeks faqat chapdan boshlab uzluksiz ustunlardan foydalanadi.
Chap ustun bor β indeks tig'iz ishlaydi:
EXPLAIN (ANALYZE, COSTS OFF)
SELECT * FROM buyurtma WHERE mijoz_id = 999 AND yaratilgan > '2024-06-01';
Index Scan using idx_buyurtma_mijoz_sana on buyurtma (actual rows=0.00 loops=1)
Index Cond: ((mijoz_id = 999) AND (yaratilgan > '2024-06-01'...))
Buffers: shared read=3
Faqat 3 ta sahifa β mukammal. Endi faqat ikkinchi ustun (chap prefiks yo'q):
EXPLAIN (ANALYZE, COSTS OFF)
SELECT count(*) FROM buyurtma WHERE yaratilgan = '2024-03-15 10:00:00+00';
Aggregate (actual rows=1.00 loops=1)
-> Index Only Scan using idx_buyurtma_mijoz_sana on buyurtma (actual rows=0.00 loops=1)
Index Cond: (yaratilgan = '2024-03-15 15:00:00+05'...)
Buffers: shared hit=12 read=383
Diqqat: PG indeksdan foydalandi-yu, lekin Buffers ... read=383 β butun indeksni o'qishga majbur bo'ldi! Chunki mijoz_id (chap ustun) cheklanmagan, qatorlar yaratilgan bo'yicha global saralanmagan β PG har guruhni titkilashga majbur. Bu chap prefiks qoidasining haqiqiy isboti: indeks bor, ammo samarasi yo'qoldi.
Tartib qoidalari (dizayn):
- Tenglik avval, oraliq keyin.
WHERE a = ? AND b > ?uchun(a, b)β to'g'ri. Tenglik ustunini chapga qo'ying, indeks aniq nuqtaga "sakraydi", keyin oraliqni skanlaydi. - Yolg'iz tez ishlatilgan ustun chapga. Agar
mijoz_idba'zan yolg'iz, ba'zanyaratilganbilan birga kelsa βmijoz_idni chapga qo'ying; u har ikki holatda ham ishlaydi. - Selektivlik. Ko'p xil qiymatli (yuqori selektiv) ustun odatda foydaliroq, lekin tenglik/oraliq qoidasi undan ustun turadi.
3.2 Qisman (partial) indeks β faqat kerakli qatorlar¶
12-bobdagi soft delete naqshini eslang: ochirilgan IS NULL = faol qator. Ko'pchilik so'rov faqat faol qatorlar bilan ishlaydi. Nega o'chirilgan 5% qatorni ham indekslash kerak? β Kerak emas.
CREATE INDEX idx_buyurtma_faol_holat ON buyurtma(holat) WHERE ochirilgan IS NULL;
SELECT pg_size_pretty(pg_relation_size('idx_buyurtma_mijoz')) AS toliq_btree,
pg_size_pretty(pg_relation_size('idx_buyurtma_faol_holat')) AS partial;
Indeks 664 kB, to'liq B-tree esa 1168 kB β kichikroq, tezroq, yangilash ham arzonroq. So'rov shartida WHERE predikatiga mos kelsa, PG bu indeksdan foydalanadi:
Bitmap Heap Scan on buyurtma (actual rows=11866.00 loops=1)
Recheck Cond: ((holat = 'yangi'::text) AND (ochirilgan IS NULL))
-> Bitmap Index Scan on idx_buyurtma_faol_holat (actual rows=11866.00 loops=1)
Index Cond: (holat = 'yangi'::text)
Qachon partial indeks:
- Soft delete:
WHERE ochirilgan IS NULL(faol qatorlar). - Holat filtri:
WHERE holat = 'faol'β agar so'rovlarning ko'pi faqat faol holatni qidirsa. - "Faqat 1% qatorga tegishli" hodisalar:
WHERE muammoli = trueβ kichik indeks, hammasini topadi. - UNIQUE bilan birga:
CREATE UNIQUE INDEX ... (email) WHERE ochirilgan IS NULLβ "faol foydalanuvchilar orasida email noyob, o'chirilganlar hisobga olinmaydi" degan biznes qoidasini majburlaydi.
3.3 Ifoda (expression) indeks β funksiya natijasiga indeks¶
WHERE lower(email) = '...' (registr-sezgir bo'lmagan qidiruv) β bu yerda oddiy indeks ishlamaydi, chunki indeks email ni saqlaydi, lower(email) ni emas (13-bobdagi "ustunga funksiya" tuzog'ini eslang). Avval indekssiz:
Seq Scan β yomon. Ifoda indeksi aynan funksiya natijasini indekslaydi:
CREATE INDEX idx_buyurtma_lower_email ON buyurtma(lower(email));
EXPLAIN (ANALYZE, COSTS OFF) SELECT * FROM buyurtma WHERE lower(email) = 'user5000@shop.uz';
Bitmap Heap Scan on buyurtma (actual rows=1.00 loops=1)
Recheck Cond: (lower(email) = 'user5000@shop.uz'::text)
-> Bitmap Index Scan on idx_buyurtma_lower_email (actual rows=1.00 loops=1)
Index Cond: (lower(email) = 'user5000@shop.uz'::text)
So'rovdagi ifoda indeksdagi ifodaga aynan mos kelishi kerak (lower(email)). Boshqa misollar: (date_trunc('day', yaratilgan)), (xususiyat->>'rang') (JSONB dan bitta yo'lni indekslash β to'liq GIN dan yengilroq), ((narx * miqdor)).
3.4 Covering (INCLUDE) indeks β index-only scan¶
Oddiy indeks bilan PG ikki qadam qiladi: (1) indeksdan qator joyini topadi, (2) jadval (heap) dan qolgan ustunlarni o'qiydi. Agar kerakli hamma ustun indeksning ichida bo'lsa, ikkinchi qadam keraksiz β bu Index Only Scan.
PG 18 da INCLUDE bilan ustunlarni indeksning bargiga "yuk" (payload) sifatida qo'shamiz:
CREATE INDEX idx_buyurtma_mijoz_inc ON buyurtma(mijoz_id) INCLUDE (summa, holat);
EXPLAIN (ANALYZE, COSTS OFF)
SELECT mijoz_id, summa, holat FROM buyurtma WHERE mijoz_id = 777;
Index Only Scan aniq ko'rinishi uchun (raqobatchi indeks o'chirilgan holatda):
Index Only Scan using idx_buyurtma_mijoz_inc on buyurtma (actual rows=3.00 loops=1)
Index Cond: (mijoz_id = 777)
Heap Fetches: 3
Index Only Scan + Heap Fetches β jadvalga deyarli bormadi (Heap Fetches past bo'lsa zo'r; VACUUM dan keyin 0 ham bo'lishi mumkin). summa va holat indeksdan o'qildi.
INCLUDE vs kalitga qo'shish farqi: INCLUDE (summa, holat) ustunlari indeksda saralanmaydi, ular faqat barg sahifada saqlanadi β shuning uchun ular bo'yicha qidirib bo'lmaydi, lekin ularni o'qib bo'ladi. Agar (mijoz_id, summa, holat) deb yozsangiz, hammasi kalit bo'ladi, indeks kattaroq va summa/holat bo'yicha saralash imkonini ham beradi. Faqat o'qish kerak bo'lsa β INCLUDE yengilroq.
Dizayn eslatmasi: covering indeks faqat eng "issiq" (juda tez-tez chaqiriladigan) so'rov uchun. INCLUDE ustunlari indeksni kattalashtiradi va yozishni sekinlashtiradi. "Bu so'rov soatiga ming marta chaqiriladi va doim shu uch ustunni qaytaradi" β ana shunda INCLUDE oqlanadi.
4. Indeksning narxi va ortiqcha indekslar¶
4.1 Har indeks β yozishga soliq¶
Indeks bepul emas. Har bir INSERT, har bir indekslangan ustunni o'zgartiruvchi UPDATE, har bir DELETE β barcha tegishli indekslarni ham yangilaydi. 5 ta indeksli jadvalga INSERT β bu 1 ta jadval yozuvi + 5 ta indeks yangilanishi.
| Narx | Tafsilot |
|---|---|
| Yozish sekinroq | har INSERT/UPDATE/DELETE indekslarni yangilaydi |
| Disk joy | indeks alohida tuzilma; ba'zan jadvaldan ham katta (bizda B-tree email = ~4 MB) |
| VACUUM/maintenance | ko'proq indeks = ko'proq tozalash ishi |
| Planner sekinroq | juda ko'p indeks plannerni chalg'itadi va rejalashtirish vaqtini oshiradi |
Shuning uchun kam yozadigan, ko'p o'qiladigan ustunga indeks qo'ying. Kam o'qilib, ko'p yoziladigan ustunga indeks β sof zarar.
4.2 Ortiqcha va dublikat indeksni topish¶
Vaqt o'tib loyihada indekslar yig'ilib qoladi: kimdir (a) qo'ydi, keyin yana kimdir (a, b) qo'shdi β endi (a) ortiqcha, chunki (a, b) uning chap prefiksini ham qoplaydi. Yoki ikkita bir xil indeks ikki marta yaratilgan.
Ishlatilmagan indeksni topish β pg_stat_user_indexes orqali skan sonini ko'ramiz:
SELECT indexrelid::regclass AS indeks,
idx_scan AS skan_soni,
pg_size_pretty(pg_relation_size(indexrelid)) AS hajm
FROM pg_stat_user_indexes
WHERE schemaname = 'ch14'
ORDER BY idx_scan;
indeks | skan_soni | hajm
--------------------------+-----------+---------
buyurtma_pkey | 0 | 2208 kB
idx_buyurtma_xususiyat | 1 | 560 kB
...
skan_soni = 0 (uzoq vaqt ishlagan tizimda) β bu indeks hech ishlatilmagan, nomzod o'chirishga. (Ehtiyot: yaqinda qo'shilgan yoki noyob hodisalar uchun indeks 0 ko'rinishi mumkin β statistika yetarli yig'ilganini tekshiring.)
Dublikat (bir xil) indeksni topish:
SELECT indexrelid::regclass AS indeks,
pg_size_pretty(pg_relation_size(indexrelid)) AS hajm
FROM pg_index i
JOIN pg_stat_user_indexes s USING (indexrelid)
WHERE schemaname = 'ch14'
AND (indrelid, indkey::text, indclass::text, indexprs IS NULL, indpred IS NULL) IN (
SELECT indrelid, indkey::text, indclass::text, indexprs IS NULL, indpred IS NULL
FROM pg_index GROUP BY 1,2,3,4,5 HAVING count(*) > 1)
ORDER BY 1;
Agar idx_dup_a va idx_dup_b ikkalasi ham (holat) ustunida bo'lsa, bu so'rov ikkalasini ham qaytaradi β bittasini o'chiring.
4.3 FK ustuniga indeks β PG AVTOMATIK QO'YMAYDI!¶
Bu eng ko'p uchraydigan yashirin xato. PG PRIMARY KEY va UNIQUE ga avtomatik indeks qo'yadi, lekin FOREIGN KEY ustuniga indeks qo'ymaydi (MySQL/InnoDB esa qo'yadi β bu farqni biling!).
CREATE TABLE qator (
id int PRIMARY KEY,
buyurtma_id bigint REFERENCES buyurtma(id), -- FK
nom text
);
SELECT indexname FROM pg_indexes WHERE tablename = 'qator';
Indekssiz FK ikki muammo keltiradi:
- Bola jadvalidan qidirish sekin:
SELECT * FROM qator WHERE buyurtma_id = 50-> Seq Scan. - Eng yashirin va xavfli: ota jadvaldan qator o'chirilganda (
DELETE FROM buyurtma WHERE id = 50), PG bola jadvalida "shu ID ga bog'liq qator bormi?" deb tekshirishi kerak. Indeks bo'lmasa β har o'chirishda bola jadvalining to'liq skani. Katta bola jadvalida bu DELETE ni o'lik darajada sekinlashtiradi.
Qoida: har bir FK ustuniga indeks qo'ying (agar u allaqachon kompozit indeksning chap prefiksi bo'lmasa):
Bu shu qadar muhimki, ko'p jamoalar buni dizayn nazoratiga kiritadi: "FK qo'shganda, indeksini ham qo'shasan". 13-bobdagi "indekssiz FK" anti-naqshini ham qarang.
5. Indeks dizayni β amaliy yondashuv¶
Indeksni "oldindan hamma narsaga" qo'ymang. Ketma-ketlik:
- Avval o'lcha. Sxema va misol ma'lumot bilan boshlang, asosiy so'rovlarni
EXPLAIN (ANALYZE)bilan ko'ring. Sekin joyni toping, keyin indeks qo'ying. - Default majburiy minimum: har PK (avtomatik), har FK (qo'lda!), har UNIQUE qoida (avtomatik), eng tez-tez ishlatiladigan WHERE/JOIN ustunlari.
- Tur to'g'ri tanlangan: oddiy ustun -> B-tree; JSONB/array -> GIN; ulkan vaqt jadvali -> BRIN; geometrik/EXCLUDE -> GiST.
- Kompozitda tartib: tenglik avval, oraliq keyin; yolg'iz tez ishlatilgan ustun chapga.
- Nozik vositalar kerakli joyda: filtrlangan so'rov -> partial; funksiya bo'yicha qidiruv -> expression; juda issiq o'qish -> covering INCLUDE.
- Davriy audit:
pg_stat_user_indexesbilan ishlatilmagan/dublikat indekslarni topib o'chiring.
Ekspert maslahati: indeks dizayni β bir martalik ish emas, doimiy jarayon. So'rov naqshlari o'zgaradi, ma'lumot o'sadi. Kvartalda bir marta indekslarni qayta ko'rib chiqing: qaysilari ishlatilmayapti, qaysi yangi so'rovga indeks kerak. "Indeks qo'shish" qanchalik oson bo'lsa, "keraksizini o'chirish" shunchalik unutiladi.
Tekshiruvni tozalaymiz:
Mashqlar¶
Oson¶
- Tur tanlash β tezkor. Quyidagi ustunlarning har biri uchun qaysi indeks turi mos: (a)
narx numeric(oraliq qidiruv), (b)teglar text[](ichidan qidirish), (c)hodisa_vaqti timestamptz(ulkan log jadvali, oraliq), (d)sozlamalar jsonb(kalit bo'yicha qidirish). Sababini bir jumlada yozing. - Nega indeks ishlamadi.
CREATE INDEX idx_email ON foydalanuvchi(email)bor, lekinWHERE lower(email) = 'a@b.uz'Seq Scan beradi. Nega? Tuzating. - Avtomatik indeks.
id serial PRIMARY KEY,email text UNIQUE,tuman_id int REFERENCES tuman(id)ustunli jadvalda PG qaysi ustunlarga indeksni AVTOMATIK qo'yadi, qaysiga YO'Q? O'zingiz qaysi indeksni qo'lda qo'shasiz? - Kompozit tartib β birinchi qadam. "Berilgan mijozning so'nggi buyurtmalari" (
WHERE mijoz_id = ? ORDER BY yaratilgan DESC) uchun(mijoz_id, yaratilgan)yoki(yaratilgan, mijoz_id)β qaysi tartib to'g'ri? Nega?
O'rta¶
- Selektivlik.
jins textustunida atigi 2 xil qiymat ('erkak','ayol') bor, har biri ~50%. Bunga oddiy B-tree indeks foyda beradimi? Qaysi vaziyatda partial indeks bu yerda mantiqliroq bo'ladi? - Partial indeksni loyihala.
buyurtma(holat)da 5 xil holat bor, lekin so'rovlarning 90% i faqatholat = 'yangi'ni qidiradi (qayta ishlash navbati). Eng tejamli indeksni loyihalang va nega to'liq indeksdan yaxshiroq ekanini tushuntiring. - Covering indeks. API endpointi har so'rovda
SELECT mahsulot_id, narx, ombor_qoldigi FROM ombor WHERE mahsulot_id = ?ni qaytaradi va soatiga o'n minglab marta chaqiriladi. Index-only scan beradigan indeksni loyihalang. Kalit va INCLUDE ni qanday taqsimlaysiz? - Ortiqcha indeksni top. Jadvalda
idx_a (mijoz_id),idx_ab (mijoz_id, sana),idx_b (sana)indekslari bor. Qaysi biri ortiqcha bo'lishi mumkin? Qaysi so'rov naqshidaidx_bbaribir kerak bo'lib qoladi? - GIN qachon.
maqola(matn text, teglar text[])jadvalida ikki so'rov bor: (a)WHERE teglar @> ARRAY['postgresql'], (b)WHERE matn ILIKE '%indeks%'. Har biriga qaysi indeks (yoki indeks emas)? Tushuntiring. - Tenglik-oraliq tartibi.
WHERE tuman_id = 5 AND yaratilgan BETWEEN ? AND ?uchun ikki nomzod:(tuman_id, yaratilgan)va(yaratilgan, tuman_id). Qaysi biri tezroq va nega? "Tenglik avval" qoidasini qo'llang.
Qiyin¶
- BRIN qaror. 500 million qatorli
sensor_olchov(qurilma_id int, vaqt timestamptz, qiymat numeric)jadvali bor; ma'lumot vaqt bo'yicha ketma-ket yoziladi. Vaqt oralig'i bo'yicha so'rovlar uchun BRIN yoki B-tree? Hajm va tezlik trade-off ini taqqoslang. Endiqurilma_idrandom tartibda kelsa β unga BRIN ishlaydimi? - Indeks auditi loyihasi. Loyihada
buyurtmajadvalida 9 ta indeks bor, INSERT lar sekinlashgan.pg_stat_user_indexesdan qanday foydalanib ishlatilmagan va dublikat indekslarni aniqlaysiz? Qaysi indekslarni o'chirish xavfsiz, qaysilari "0 skan" bo'lsa ham qoldirish kerak (maslahat: UNIQUE va FK-quvvatlovchi indekslar)? - To'liq indeks strategiyasi loyihala. Marketplace
buyurtma(id, xaridor_id, sotuvchi_id, holat, summa, yaratilgan, ochirilgan, xususiyat jsonb)jadvali uchun quyidagi so'rovlar bor: (a) xaridorning faol buyurtmalari sana bo'yicha; (b) sotuvchining "yangi" holatdagi buyurtmalari; (c)xususiyat->>'kategoriya'bo'yicha qidirish; (d) email registr-sezgir bo'lmagan qidiruv (email ustuni qo'shilgan deb hisoblang). Har bir so'rov uchun indeksni (turi, ustunlari, tartibi, partial/expression/INCLUDE) loyihalang va minimal indekslar to'plamini taklif qiling. - Index-only scan buzilishi. Covering indeks qo'ydingiz, lekin
EXPLAINdaIndex Only Scano'rnigaHeap Fetchesjuda yuqori chiqyapti. Nega bunday bo'ladi (maslahat: visibility map, yangi yozilgan qatorlar) va qaysi maintenance buyrug'i buni tuzatadi? - Yozish vs o'qish trade-off.
hodisa_log(id, foydalanuvchi_id, tur, vaqt, tafsilot jsonb)jadvaliga soatiga millionlab qator yoziladi (append-only), o'qish kam (kunlik hisobot). Indeks strategiyasini qanday loyihalaysiz β ko'p indeks yoki kam? Qaysi turlar? Yozish tezligini saqlash uchun nimani qurbon qilasiz?
Yechimlar¶
Yechim β 1
- (a)
narx numeric-> B-tree. Oraliq (<,>,BETWEEN) va saralash β bu B-tree ning asosiy ishi. - (b)
teglar text[]-> GIN. Massiv ichidan element qidirish (@>,&&) faqat GIN bilan tez; B-tree butun massivni yaxlit kalit deb biladi. - (c)
hodisa_vaqti(ulkan log) -> BRIN. Vaqt tabiiy tartiblangan, jadval ulkan; BRIN juda kichik bo'ladi va oraliqni quvvatlaydi. (Kichik jadvalda esa oddiy B-tree.) - (d)
sozlamalar jsonb-> GIN. JSONB ichidan kalit/qiymat bo'yicha qidirish (@>,?) GIN ni talab qiladi. (Agar faqat bitta yo'l bo'yicha qidirilsa β expression indeks yengilroq.)
Yechim β 2
Indeks email qiymatini saqlaydi, lekin so'rov lower(email) natijasini qidiradi β bu boshqa qiymat, shuning uchun indeks yaramaydi (PG har qatorda lower() ni hisoblashga majbur -> Seq Scan). Tuzatish β ifoda (expression) indeks:
Endi indeksdagi ifoda so'rovdagi ifodaga aynan mos keladi. (Muqobil: email ni doim kichik harfda saqlash yoki citext turi ishlatish.)
Yechim β 3
PG avtomatik qo'yadi:
- id serial PRIMARY KEY -> avtomatik unique B-tree indeks.
- email text UNIQUE -> avtomatik unique B-tree indeks.
PG qo'ymaydi:
- tuman_id int REFERENCES tuman(id) -> FOREIGN KEY ga indeks YO'Q!
Qo'lda qo'shish kerak:
Aks holda tuman dan qator o'chirilganda PG butun foydalanuvchi jadvalini skanlaydi (FK butunligini tekshirish uchun) va WHERE tuman_id = ? so'rovi sekin bo'ladi.
Yechim β 4
(mijoz_id, yaratilgan) to'g'ri.
mijoz_idβ tenglik sharti (= ?), shuning uchun chapga: indeks aniq mijozga "sakraydi".- O'sha guruh ichida qatorlar
yaratilganbo'yicha allaqachon saralangan, demakORDER BY yaratilgan DESCham indeksdan bepul olinadi (PG indeksni teskari o'qiydi) β qo'shimcha saralash qadami kerak emas.
(yaratilgan, mijoz_id) bo'lsa, chap ustun yaratilgan cheklanmagan -> chap prefiks ishlamaydi, indeks samarasiz.
Yechim β 5
Oddiy B-tree indeks jins ga deyarli foyda bermaydi: har bir qiymat qatorlarning ~50% ini qamraydi, shuning uchun PG baribir jadvalning yarmini o'qishga majbur β bu Seq Scan dan ko'p farq qilmaydi (hatto sekinroq bo'lishi mumkin). Bu past selektivlik.
Partial indeks mantiqli bo'ladigan vaziyat: qiymatlardan biri kam uchraganda. Masalan holat = 'muammoli' qatorlar atigi 0.5% bo'lsa:
Endi indeks kichik (faqat 0.5% qator) va "muammoli buyurtmalar" so'rovi juda tez. Asosiy tushuncha: indeks foydasi kam uchraydigan qatorlarni ajratishda, ko'pchilik qatorni qamrashda emas.
Yechim β 6
So'rovlarning 90% i holat = 'yangi' bo'lgani uchun, eng tejamli yo'l β shu shartni indeksning o'ziga qotirish (partial):
To'liq buyurtma(holat) indeksidan afzalligi:
- Kichikroq: faqat 'yangi' qatorlarni indekslaydi (masalan 20% qator -> indeks ~5x kichik).
- Tezroq yangilanadi: boshqa holatga o'tgan qator indeksdan tushib qoladi β INSERT/UPDATE arzonroq.
WHERE holat = 'yangi' ORDER BY yaratilganso'rovini mukammal quvvatlaydi.
So'rov shartidagi holat = 'yangi' indeks predikatiga mos kelishi shart, aks holda PG bu indeksni ishlatmaydi.
Yechim β 7
Index-only scan uchun barcha qaytariladigan ustunlar indeksda bo'lishi kerak. Qidiruv kaliti mahsulot_id, qolgan ikki ustun (narx, ombor_qoldigi) β INCLUDE (yuk):
- Kalit =
mahsulot_id: WHERE bo'yicha qidiriladi, saralanishi kerak. - INCLUDE =
narx, ombor_qoldigi: faqat o'qiladi, qidirilmaydi -> kalitga emas, yukka.
EXPLAIN da Index Only Scan ko'rinadi: jadvalga (heap) bormaydi, bitta o'qish bilan bitadi. Soatiga o'n minglab marta chaqirilgani uchun bu INCLUDE narxini (kattaroq indeks) oqlaydi.
Yechim β 8
idx_a (mijoz_id)β ortiqcha.idx_ab (mijoz_id, sana)uning chap prefiksini qoplaydi:WHERE mijoz_id = ?ikkala indeksdan ham foydalanadi.idx_ani o'chirish mumkin (indeks biroz kattaroq bo'ladi, lekin bitta indeks yetadi).idx_b (sana)β KERAK bo'lib qoladi.idx_abning chap ustunimijoz_id, shuning uchunWHERE sana = ?(mijozsiz)idx_abdan foydalana olmaydi (chap prefiks yo'q). Agar shunday so'rov bo'lsa,idx_bzarur.
Xulosa: idx_a ni o'chirish xavfsiz; idx_ab va idx_b ni "sana yolg'iz qidiriladimi?" degan savol asosida qoldirish kerak.
Yechim β 9
- (a)
WHERE teglar @> ARRAY['postgresql']-> GIN. Massiv ichidan element qidirish;@>operatori GIN bilan tez.
- (b)
WHERE matn ILIKE '%indeks%'-> oddiy indeks ishlamaydi (boshi va oxiri ochiq%...%). To'g'ri yechim β full-text qidiruv (GIN +to_tsvector) yokipg_trgmkengaytmasi bilan trigram GIN:
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE INDEX idx_maqola_matn_trgm ON maqola USING gin (matn gin_trgm_ops);
Oddiy LIKE '%...%' ga B-tree umuman yordam bermaydi β bu SQL kitobida ko'rgan "boshi noma'lum" tuzog'i.
Yechim β 10
(tuman_id, yaratilgan) tezroq.
tuman_idβ tenglik (= 5): indeks aynan shu tuman guruhiga sakraydi.- O'sha guruh ichida qatorlar
yaratilganbo'yicha saralangan ->BETWEENoralig'i tig'iz, uzluksiz diapazon bo'lib o'qiladi.
(yaratilgan, tuman_id) bo'lsa: indeks avval vaqt bo'yicha saralangan, tuman_id har vaqt nuqtasida tarqoq. BETWEEN diapazonidagi hamma qatorni o'qib, keyin tuman_id = 5 ni filtrlash kerak β ko'p ortiqcha ish.
Qoida: tenglik shartidagi ustun (tuman_id) β chapga, oraliq shartidagi ustun (yaratilgan) β o'ngga.
Yechim β 11
Vaqt oralig'i so'rovi uchun β BRIN.
- 500 mln qator vaqt bo'yicha ketma-ket yozilgan -> har disk bloki tor min-max vaqt oralig'iga ega -> BRIN juda samarali.
- Hajm: BRIN bunday jadvalda bir necha MB; ekvivalent B-tree o'nlab GB bo'lishi mumkin. Bu disk va xotira tejamkorligi katta.
- Tezlik: oraliq so'rovda BRIN mos bloklarni tezda topadi (keyin aniqlab tekshiradi). Aniq bitta qatorni topishda B-tree tezroq, lekin bu yerda vazifa β oraliq.
qurilma_id random kelsa -> BRIN ishlamaydi. BRIN faqat qiymat disk tartibiga korrelyatsiya qilganda foydali. qurilma_id aralash kelsa, har blokda min-max butun diapazonni qoplaydi va BRIN hech narsani ajrata olmaydi. qurilma_id ga oddiy B-tree (yoki (qurilma_id, vaqt) kompozit) kerak.
Yechim β 12
Qadamlar:
- Ishlatilmagan indeks β skan soni 0:
SELECT indexrelid::regclass, idx_scan, pg_size_pretty(pg_relation_size(indexrelid))
FROM pg_stat_user_indexes WHERE relname = 'buyurtma' ORDER BY idx_scan;
idx_scan = 0 (statistika yetarli yig'ilgandan keyin) β o'chirishga nomzod.
- Dublikat indeks β bir xil ustun/ifoda/predikatli indekslarni
pg_indexni guruhlash orqali toping (bobdagi 4.2 so'rovi).
O'chirish xavfsiz: ikki nusxadagi indeksning bittasi; hech qachon ishlatilmagan oddiy qo'shimcha indekslar; boshqa indeks chap prefiksi sifatida qoplaydigan ortiqcha indeks.
0 skan bo'lsa ham QOLDIRISH kerak:
- UNIQUE constraint ni quvvatlovchi indeks β u qidiruv uchun emas, butunlik uchun; o'chirsangiz unikallik buziladi.
- FK ni quvvatlovchi indeks β idx_scan da ko'rinmasligi mumkin, lekin ota jadvaldan DELETE/UPDATE da ishlatiladi; o'chirsangiz cascade operatsiyalar sekinlashadi.
- Yaqinda qo'shilgan indeks β statistika hali yig'ilmagan bo'lishi mumkin.
Yechim β 13
So'rovlarni indekslarga moslab:
-- (a) xaridorning faol buyurtmalari sana bo'yicha (soft delete partial + kompozit)
CREATE INDEX idx_buyurtma_xaridor_faol ON buyurtma(xaridor_id, yaratilgan)
WHERE ochirilgan IS NULL;
-- (b) sotuvchining "yangi" holatdagi buyurtmalari (partial, holat indeksga qotirilgan)
CREATE INDEX idx_buyurtma_sotuvchi_yangi ON buyurtma(sotuvchi_id, yaratilgan)
WHERE holat = 'yangi' AND ochirilgan IS NULL;
-- (c) xususiyat->>'kategoriya' bo'yicha qidirish (faqat bitta yo'l -> expression yengil)
CREATE INDEX idx_buyurtma_kategoriya ON buyurtma((xususiyat->>'kategoriya'));
-- (agar JSONB ning ko'p yo'li bo'yicha qidirilsa -> GIN: USING gin (xususiyat))
-- (d) email registr-sezgir bo'lmagan qidiruv (expression)
CREATE INDEX idx_buyurtma_email_lower ON buyurtma(lower(email));
Tahlil:
- (a) va (b) β (id_ustun, yaratilgan) tartibi: tenglik avval (xaridor_id/sotuvchi_id), oraliq/saralash keyin (yaratilgan); partial bilan faol/yangi qatorlarga cheklangan -> kichik va tez.
- (c) β bitta JSONB yo'l bo'yicha qidirish uchun expression indeks GIN dan yengil va arzon; ko'p yo'l kerak bo'lsa GIN.
- (d) β funksiya natijasini indekslash (expression).
Minimal to'plam: har so'rov uchun aniq bittadan indeks; ortiqcha "har ehtimolga" indeks yo'q. PK (id) avtomatik. Agar xaridor_id/sotuvchi_id mustaqil FK bo'lsa va ota jadvaldan tez-tez DELETE bo'lsa β partialsiz to'liq FK indeksini ham qo'shish kerak bo'lishi mumkin (partial indeks FK butunlik tekshiruvini to'liq qoplamaydi).
Yechim β 14
Index Only Scan haqiqatan "only" bo'lishi uchun PG qatorning ko'rinishini (visible/o'chirilmagan emasligini) bilishi kerak. Bu ma'lumot indeksda emas, visibility map (VM) da saqlanadi. Agar qator yaqinda yozilgan/yangilangan va VM hali yangilanmagan bo'lsa, PG har qator uchun jadvalga (heap) borib ko'rinishni tekshiradi -> Heap Fetches yuqori, foyda yo'qoladi.
Tuzatish β VACUUM (visibility map ni yangilaydi):
VACUUM dan keyin VM "bu bloklardagi hamma qator ko'rinadi" deb belgilaydi, PG jadvalga bormaydi, Heap Fetches 0 ga tushadi. Shuning uchun ko'p yoziladigan jadvalda index-only scan foydasi autovacuum sozlamasiga bog'liq.
Yechim β 15
Bu yozish-og'ir, o'qish-kam ssenariy β strategiya: indeksni minimal saqlash.
- Har indeks INSERT ni sekinlashtiradi. Soatiga millionlab yozuvda har qo'shimcha indeks katta soliq. Shuning uchun "har ehtimolga" indeks YO'Q.
- Faqat haqiqatan kerakli indeks: kunlik hisobot vaqt bo'yicha bo'lsa ->
vaqtga BRIN (kichik, yozishga deyarli ta'sir qilmaydi, append-only ga ideal). B-tree o'rniga BRIN β chunki BRIN yozishda ancha arzon. - PK ni yengil saqlang: agar
idfaqat noyoblik uchun bo'lsa,bigint identity(B-tree PK) yetarli; UUID PK random bo'lsa yozishni va indeksni shishiradi (uuidv7()ketma-ket bo'lgani uchun afzalroq). - JSONB ga GIN β faqat zarur bo'lsa. GIN yozishda qimmat; agar
tafsilotdan kam qidirilsa, GIN qo'ymang yoki kechroq, hisobot vaqtida vaqtinchalik qo'ying.
Nimani qurbon qilamiz: ad-hoc o'qish tezligini. Kam o'qiladigan ustunlarga indeks qo'ymaymiz β kerak bo'lsa hisobot sekinroq ishlaydi, lekin yozish tez qoladi. Agar hisobot juda muhim bo'lsa β alohida analitik nusxa/materialized view ga ko'chiring (15-bob, 21-bob), asosiy yozuv jadvalini indekssiz tez saqlang.
β¬ οΈ Oldingi: 13 β Anti-naqshlar: nima qilmaslik kerak Β· π README Β· Keyingi: 15 β Sxema va so'rov performansi (EXPLAIN ANALYZE) β‘οΈ