Mundarija (22)
- Bu darsda
- 1. Nega bu kerak?
- 2. Partial indeks
- 2.1 Yaratish va hajm
- 2.2 So'rov partial indeksni qachon ishlatadi
- 2.3 Partial unique: Savat bilan noyoblik
- 3. Expression indeks
- 3.1 lower(email)
- 3.2 IMMUTABLE talabi
- 4. Covering indeks va Index Only Scan
- 4.1 Jadvalga bormasdan javob
- 4.2 Visibility map: nega VACUUM kerak
- 4.3 INCLUDE yoki kalitga qo'shish?
- 5. Node qadami: loyihada bular allaqachon bor
- 6. Ko'p uchraydigan xatolar
- 7. Mashqlar
- 1-mashq (oson): to'ldiring
- 2-mashq (o'rta): bitta "asosiy" vazifa
- 3-mashq (qiyin): Index Only Scan qachon ishlaydi
- 8. Real ishda
- Xulosa
- Manbalar
PostgreSQL partial, expression va covering indekslar: Index Only Scan
Qisqacha: Oddiy indeks butun ustunni saqlaydi. Partial indeks faqat kerakli qatorlarni (
WHERE NOT bajarildi) saqlaydi — kanonda 28 MB o'rniga 11 MB. Expression indeks ustunni emas, ifodani (lower(email)) saqlaydi — so'rov ham aynan shu ifoda bilan yozilsa ishlaydi. Covering indeks (INCLUDE) so'rovga kerak bo'lgan qo'shimcha ustunlarni ham saqlaydi — shunda baza jadvalga umuman tegmaydi (Index Only Scan): kanonda 418 sahifa o'rniga 9 sahifa.
Bu darsda
WHEREbilan partial indeks yaratasiz va so'rov uni qachon ishlatishini bilasiz.- Partial unique indeks bilan "faqat faol yozuvlar orasida noyob" qoidasini bazada qo'yasiz.
- Ifoda indeksini yaratasiz va IMMUTABLE xatosini tushunasiz.
INCLUDEbilan covering indeks quriladi va Index Only Scan qachon ishlashini — visibility map bilan — tushuntirasiz.
Oldin bilishingiz kerak: Qaysi ustunga va qanday tartibda indeks, Soft delete, timestamps va tarix jadvallari, MVCC: Postgres ichida versiyalar, Ifodalar va o'rnatilgan funksiyalar.
1. Nega bu kerak?
Tasavvur qiling: vazifalar uchun Telegram eslatma boti yozyapsiz. U har daqiqada bir xil savol beradi: "muddati shu oraliqda tugaydigan, hali bajarilmagan va o'chirilmagan vazifalar qaysi?"
Kanon bazasidagi 2 mln vazifaning 60 % i bajarilgan, yarmida muddat umuman yo'q. Ya'ni bot qidirayotgan qatorlar — jadvalning kichik qismi. muddat ga oddiy indeks qo'ysak, u hamma 2 mln qatorni saqlaydi: bajarilganlarini ham, Savatdagilarini ham. Bot ularni hech qachon so'ramaydi, lekin ular joy egallaydi va har yozishda yangilanadi.
Oldingi darsda indeksni qaysi ustunga qo'yishni o'rgandik. Bu darsda indeksning o'zini so'rovga moslashtiramiz. Uch usul bor:
| Turi | Nimani saqlaydi | Misol |
|---|---|---|
| Partial | Faqat shartga mos qatorlarni | WHERE NOT bajarildi |
| Expression | Ustun emas, ifoda natijasini | lower(email) |
| Covering | Kalitdan tashqari qo'shimcha ustunlarni | INCLUDE (bajarildi) |
O'xshatish: dorixona. Hamma dorilar katalogi — oddiy indeks. "Faqat retseptsiz sotiladiganlar" ro'yxati — partial. "Nomi kichik harflarda" tartiblangan ro'yxat — expression. Ro'yxatning o'zida narx ham yozilgan bo'lsa, javon oldiga bormasdan narxni aytasiz — covering.
Raqamlar ikki manbadan: kanon o'lchovi (PostgreSQL 18.6, Docker, 2 mln vazifa; sxemaning 012-fayldagi holatida, 58-partial.txt) va "biz ishga tushirdik" (PostgreSQL 17.6, Windows 11, kichik jadvallar). Kanonning EXPLAIN ANALYZE chiqishlarida telefonga sig'ishi uchun QUERY PLAN sarlavhasi va cost=… qavslari qisqartirilgan — raqamlar o'zgarmagan.
2. Partial indeks
2.1 Yaratish va hajm
Partial indeks (partial index) — WHERE sharti bilan yaratilgan indeks. Unga faqat shartga mos qatorlar kiradi. Kanon ikki indeksni yonma-yon yaratdi:
CREATE INDEX t58_toliq ON vazifalar (muddat);
CREATE INDEX t58_qisman ON vazifalar (muddat)
WHERE NOT bajarildi AND ochirilgan_vaqt IS NULL;
SELECT indexrelid::regclass,
pg_size_pretty(pg_relation_size(indexrelid))
FROM pg_index
WHERE indexrelid IN ('t58_toliq'::regclass,
't58_qisman'::regclass);Natija:
CREATE INDEX
Time: 1069.114 ms (00:01.069)
CREATE INDEX
Time: 475.266 ms
indexrelid | pg_size_pretty
------------+----------------
t58_toliq | 28 MB
t58_qisman | 11 MB
(2 rows)- To'liq (hamma qator)1.07 s quriladi28 MB
- Partial (bajarilmagan, faol)0.48 s quriladi11 MB
Manba: IlmHamroh kanon o'lchovi, 2026-10-24: PostgreSQL 18.6, Docker (WSL2), 2 mln vazifa
Partial indeks 2.5 marta kichik va 2 marta tez qurildi. Yana bir foyda: bajarilgan vazifa yangilanganda (masalan, izoh qo'shilganda) bu indeksga umuman tegilmaydi — u indeksda yo'q.
2.2 So'rov partial indeksni qachon ishlatadi
Bot so'rovi indeks sharti bilan bir xil shartni o'z ichiga oladi:
EXPLAIN (ANALYZE, BUFFERS)
SELECT id FROM vazifalar
WHERE NOT bajarildi AND ochirilgan_vaqt IS NULL
AND muddat BETWEEN '2025-03-01' AND '2025-03-02';Natija:
Index Scan using t58_qisman on vazifalar (actual time=0.051..6.177 rows=602.00 loops=1)
Index Cond: ((muddat >= '2025-03-01 00:00:00+00'::timestamp with time zone) AND (muddat <= '2025-03-02 00:00:00+00'::timestamp with time zone))
Index Searches: 1
Buffers: shared hit=189 read=416
I/O Timings: shared read=4.653
Planning:
Buffers: shared hit=141 read=2
I/O Timings: shared read=0.086
Planning Time: 0.640 ms
Execution Time: 6.280 ms
(10 rows)Planner partial indeksni tanladi. E'tibor bering: Index Cond da faqat muddat bor. NOT bajarildi va ochirilgan_vaqt IS NULL tekshirilmaydi ham — indeksga faqat shunday qatorlar kirgan.
Endi o'sha shartsiz so'rov:
EXPLAIN SELECT id FROM vazifalar
WHERE muddat BETWEEN '2025-03-01' AND '2025-03-02';Natija:
QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------------------------------
Index Scan using t58_toliq on vazifalar (cost=0.43..3350.41 rows=1362 width=8)
Index Cond: ((muddat >= '2025-03-01 00:00:00+00'::timestamp with time zone) AND (muddat <= '2025-03-02 00:00:00+00'::timestamp with time zone))
(2 rows)To'liq indeks tanlandi. Partial indeksda bajarilgan vazifalar yo'q — u bu savolga to'liq javob bera olmaydi. To'liq indeks o'chirilgach, kanon yana so'radi:
DROP INDEX t58_toliq;
EXPLAIN SELECT id FROM vazifalar
WHERE muddat BETWEEN '2025-03-01' AND '2025-03-02';Natija:
DROP INDEX
QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------------------------------------
Gather (cost=1000.00..53129.20 rows=1362 width=8)
Workers Planned: 2
-> Parallel Seq Scan on vazifalar (cost=0.00..51993.00 rows=568 width=8)
Filter: ((muddat >= '2025-03-01 00:00:00+00'::timestamp with time zone) AND (muddat <= '2025-03-02 00:00:00+00'::timestamp with time zone))
(4 rows)Butun jadval o'qiladi. Qoida: so'rov sharti indeks shartini mantiqan kafolatlashi kerak. Planner buni so'rov matnidan isbotlay olishi kerak — "ehtimol mos keladi" yetmaydi.
Diqqat: Partial indeks sharti doimiy bo'lsin (
NOT bajarildi,ochirilgan_vaqt IS NULL). Agar ilovaWHERE bajarildi = $1deb parametr bilan so'rasa, planner$1qiymatini oldindan bilmasligi mumkin va partial indeksni ishlatmaydi. Parametrli so'rov rejalari — Statistika va planner darsida.
Tekshirib ko'ring: Indeks:
(muddat) WHERE NOT bajarildi. So'rov:WHERE NOT bajarildi AND ustuvorlik = 'yuqori' AND muddat < '2026-01-01'. Indeks ishlatilishi mumkinmi?
Javob
Ha. So'rovda NOT bajarildi bor — demak, kerakli hamma qatorlar indeksda. Qo'shimcha ustuvorlik = 'yuqori' sharti indeksni buzmaydi: baza indeksdan topilgan qatorlarni shu shart bilan filtrlaydi. Muhimi — so'rov indeks shartidan torroq bo'lishi mumkin, kengroq — yo'q.
2.3 Partial unique: Savat bilan noyoblik
Partial indeks UNIQUE bilan ham bo'ladi. Kanon loyihasida u allaqachon bor — soft delete darsida qo'shilgan:
SELECT pg_get_indexdef('vazifalar_egasi_id_matn_key'::regclass);Natija:
pg_get_indexdef
-----------------------------------------------------------------------------------------------------------------------------------------
CREATE UNIQUE INDEX vazifalar_egasi_id_matn_key ON public.vazifalar USING btree (egasi_id, lower(matn)) WHERE (ochirilgan_vaqt IS NULL)
(1 row)Bitta indeksda uchala g'oya: tarkibiy (egasi_id, ...), ifoda (lower(matn)), partial (WHERE ochirilgan_vaqt IS NULL). Ma'nosi: "bitta foydalanuvchining faol vazifalari orasida bir xil matn (registrga befarq) bo'lmasin". Savatdagi vazifa hisobga olinmaydi.
Biz ishga tushirdik (PostgreSQL 17.6, holat 40). Aziz birinchi vazifasini Savatga tashlaydi, keyin xuddi shu matnli yangi vazifa yozadi, keyin katta harflar bilan yana bir marta:
BEGIN;
UPDATE vazifalar SET ochirilgan_vaqt = '2026-10-21 10:00+05'
WHERE id = 1 RETURNING id, matn;
INSERT INTO vazifalar (egasi_id, matn)
SELECT 1, matn FROM vazifalar WHERE id = 1
RETURNING id, matn;
INSERT INTO vazifalar (egasi_id, matn)
SELECT 1, upper(matn) FROM vazifalar WHERE id = 1;
ROLLBACK;Natija:
BEGIN
id | matn
----+--------------------------------------------------
1 | JavaScript kursining 26-qismini boshlash #o'qish
(1 row)
UPDATE 1
id | matn
----+--------------------------------------------------
49 | JavaScript kursining 26-qismini boshlash #o'qish
(1 row)
INSERT 0 1
ERROR: duplicate key value violates unique constraint "vazifalar_egasi_id_matn_key"
DETAIL: Key (egasi_id, lower(matn))=(1, javascript kursining 26-qismini boshlash #o'qish) already exists.
ROLLBACKUch qadam:
- Vazifa 1 Savatga tushdi — endi indeksda yo'q.
- Xuddi shu matnli yangi vazifa (49) qabul qilindi — faol vazifalar orasida takror yo'q.
- Katta harfli nusxa rad etildi:
lower()uni 49 bilan bir xil qildi.
Xato matni: "duplicate key value violates unique constraint" — "takroriy kalit qiymati noyoblik cheklovini buzadi". DETAIL qatori qaysi kalit takrorlanganini ko'rsatadi — ifoda ham (lower(matn)).
Oddiy UNIQUE (egasi_id, matn) cheklovi buni qila olmasdi: u Savatdagi vazifani ham hisoblardi va registrni farqlardi. UNIQUE cheklovi faqat ustunlar bilan ishlaydi; ifoda va WHERE — unique indeks da.
Bitta istisno bor — exclusion constraint (EXCLUDE, Race condition'larni hal qilish darsidagi bron misoli). U ham ifoda va WHERE ni qabul qiladi. Deadlock va serialization failure darsida API'ni ulaganda 014-matn-noyob-kechiktirilgan.sql aynan shu indeksni teng ma'noli cheklov bilan almashtirdi: EXCLUDE USING btree (egasi_id WITH =, lower(matn) WITH =) WHERE (ochirilgan_vaqt IS NULL) DEFERRABLE. Sabab — cheklovni buyruq oxirida tekshirish mumkin, indeksni esa yo'q. Qoida o'sha, faqat xato kodi 23505 o'rniga 23P01; API ikkalasini ham 409 qiladi. Yuqoridagi tajriba holat 40 sxemasida — shuning uchun 23505.
3. Expression indeks
3.1 lower(email)
Ifoda indeksi (expression index) — ustun qiymatini emas, undan hisoblangan ifoda natijasini saqlaydigan indeks. Kanon loyihasida birinchi jadvaldan beri bor: email registrga befarq noyob (Aziz@Example.COM = aziz@example.com), shuning uchun indeks lower(email) ustida.
Kanon ikki so'rovni solishtirdi (20 000 foydalanuvchi):
EXPLAIN (ANALYZE) SELECT id FROM foydalanuvchilar
WHERE lower(email) = 'foydalanuvchi777@example.com';Natija:
Index Scan using foydalanuvchilar_email_key on foydalanuvchilar (actual time=1.859..1.862 rows=1.00 loops=1)
Index Cond: (lower(email) = 'foydalanuvchi777@example.com'::text)
Index Searches: 1
Buffers: shared read=4
I/O Timings: shared read=1.778
Planning:
Buffers: shared hit=46
Planning Time: 0.225 ms
Execution Time: 1.881 ms
(9 rows)EXPLAIN (ANALYZE) SELECT id FROM foydalanuvchilar
WHERE email = 'foydalanuvchi777@example.com';Natija:
Seq Scan on foydalanuvchilar (actual time=1.109..8.201 rows=1.00 loops=1)
Filter: (email = 'foydalanuvchi777@example.com'::text)
Rows Removed by Filter: 19999
Buffers: shared hit=2 read=244 written=10
I/O Timings: shared read=3.994 write=0.319
Planning:
Buffers: shared hit=3
Planning Time: 0.078 ms
Execution Time: 8.224 ms
(9 rows)Birinchisi — 4 sahifa, indeks orqali. Ikkinchisi — 246 sahifa, butun jadval (19 999 qator tashlandi). Indeks lower(email) ni saqlaydi; email ning o'zi uning uchun boshqa narsa. Qoida: so'rovdagi ifoda indeksdagi ifoda bilan bir xil yozilishi kerak.
Shuning uchun ilovada email doim bir xil shaklda qidiriladi: WHERE lower(email) = lower($1). Login kodi email = $1 deb yozilsa — har kirishda butun jadval o'qiladi, katta jadvalda sezilarli.
3.2 IMMUTABLE talabi
Indeksdagi ifoda IMMUTABLE (o'zgarmas) bo'lishi shart: bir xil kirishga har doim, har qanday sozlamada bir xil natija. Aks holda indeksdagi qiymat bir kun to'g'ri, ertasi noto'g'ri bo'lib qolardi.
Misol: "vazifalarni Toshkent kuni bo'yicha" hisobot. Birinchi urinish. Biz ishga tushirdik (PostgreSQL 17.6, holat 40):
CREATE INDEX ON vazifalar (date(yaratilgan_vaqt));
CREATE INDEX ON vazifalar
(to_char(yaratilgan_vaqt, 'YYYY-MM-DD'));Natija:
ERROR: functions in index expression must be marked IMMUTABLE
ERROR: functions in index expression must be marked IMMUTABLE"Indeks ifodasidagi funksiyalar IMMUTABLE deb belgilangan bo'lishi kerak." Nega? yaratilgan_vaqt — timestamptz. Uni sanaga aylantirish sessiya vaqt zonasiga bog'liq (sana va zonalar darsida ko'rgandik): Toshkentda 3-oktabr 02:00 — UTC'da hali 2-oktabr. Bir xil qator ikki sessiyada ikki xil sana beradi.
Yechim — zonani ifodaning ichida qat'iy yozish:
CREATE INDEX ON vazifalar
((yaratilgan_vaqt AT TIME ZONE 'Asia/Tashkent')::date);Natija:
ERROR: syntax error at or near "::"
LINE 2: ((yaratilgan_vaqt AT TIME ZONE 'Asia/Tashkent')::date);
^Bu safar sintaksis xatosi: ifoda indeksida funksiya chaqiruvidan boshqa har qanday ifoda qo'shimcha qavs ichida bo'lishi kerak. lower(email) — funksiya, qavssiz bo'ladi. (… AT TIME ZONE …)::date — funksiya emas, unga yana bitta qavs kerak:
CREATE INDEX vazifalar_kun_idx ON vazifalar
(((yaratilgan_vaqt AT TIME ZONE 'Asia/Tashkent')::date));
SET enable_seqscan = off;
EXPLAIN (COSTS OFF) SELECT id FROM vazifalar
WHERE (yaratilgan_vaqt AT TIME ZONE 'Asia/Tashkent')::date
= '2026-10-03';
EXPLAIN (COSTS OFF) SELECT id FROM vazifalar
WHERE yaratilgan_vaqt::date = '2026-10-03';
RESET enable_seqscan;Natija:
CREATE INDEX
SET
QUERY PLAN
---------------------------------------------------------------------------------------------------
Index Scan using vazifalar_kun_idx on vazifalar
Index Cond: (((yaratilgan_vaqt AT TIME ZONE 'Asia/Tashkent'::text))::date = '2026-10-03'::date)
(2 rows)
QUERY PLAN
----------------------------------------------------------
Seq Scan on vazifalar
Filter: ((yaratilgan_vaqt)::date = '2026-10-03'::date)
(2 rows)
RESETenable_seqscan = off — planner'ga "iloji bo'lsa Seq Scan ishlatma" degan sinov sozlamasi. 48 qatorli jadvalda planner aks holda doim Seq Scan tanlardi (oldingi darsda ko'rdik). Production'da bu sozlamani yoqmang — u faqat "indeks umuman ishlatilishi mumkinmi?" degan savolga javob beradi.
Natijada: zonali ifoda indeksdan foydalandi, ::date li so'rov esa yo'q — ifoda bir xil emas.
Maslahat: Ifoda indeksini yaratgandan keyin
ANALYZE jadvalqiling. Postgres ifoda natijalari uchun alohida statistika yig'adi — usiz planner bunday shartlar qancha qator qaytarishini taxmin bilan baholaydi.
Tekshirib ko'ring: Indeks
lower(email)ustida. Login kodiWHERE email ILIKE $1deb yozilgan. Indeks ishlatiladimi?
Javob
Yo'q. ILIKE — boshqa operator, lower(email) = … emas. B-tree uni ishlata olmaydi. Natija to'g'ri chiqadi, lekin butun jadval o'qiladi. To'g'risi — WHERE lower(email) = lower($1). (ILIKE ham joker belgilar %, _ ni qabul qiladi — login uchun xavfli ham.)
4. Covering indeks va Index Only Scan
4.1 Jadvalga bormasdan javob
Odatda indeks "manzil kitobi": u qator qayerda turganini aytadi, keyin baza jadval sahifasini ochadi. Agar so'rovga kerak bo'lgan hamma ustunlar indeksning o'zida bo'lsa-chi? Unda jadvalga borish shart emas. Bu Index Only Scan — faqat indeksdan o'qish.
Covering indeks (covering index) — so'rovni to'liq "qoplaydigan" indeks. Postgres 11 dan beri buning uchun INCLUDE bor: kalit ustunlaridan tashqari qo'shimcha ustunlar indeks yozuviga "yuk" sifatida qo'shiladi.
Kanon so'rovi: "egasi 77 ning nechta vazifasi bajarilgan?" Kerakli ustunlar — egasi_id (shart) va bajarildi (sanash):
CREATE INDEX t58_cover ON vazifalar (egasi_id)
INCLUDE (bajarildi);
VACUUM vazifalar;
EXPLAIN (ANALYZE, BUFFERS)
SELECT count(*) FILTER (WHERE bajarildi)
FROM vazifalar WHERE egasi_id = 77;Natija:
Aggregate (actual time=0.127..0.127 rows=1.00 loops=1)
Buffers: shared hit=5 read=4
I/O Timings: shared read=0.022
-> Index Only Scan using t58_cover on vazifalar (actual time=0.047..0.095 rows=416.00 loops=1)
Index Cond: (egasi_id = 77)
Heap Fetches: 0
Index Searches: 1
Buffers: shared hit=5 read=4
I/O Timings: shared read=0.022
Planning:
Buffers: shared hit=55
Planning Time: 0.215 ms
Execution Time: 0.147 ms
(13 rows)416 qator 9 sahifadan o'qildi. Heap Fetches: 0 — jadvalga bir marta ham borilmadi. t58_cover o'chirilgandan keyin xuddi shu so'rov:
Aggregate (actual time=1.636..1.638 rows=1.00 loops=1)
Buffers: shared hit=343 read=75
I/O Timings: shared read=0.347
-> Bitmap Heap Scan on vazifalar (actual time=0.106..1.588 rows=416.00 loops=1)
Recheck Cond: (egasi_id = 77)
Heap Blocks: exact=415
Buffers: shared hit=343 read=75
I/O Timings: shared read=0.347
-> Bitmap Index Scan on vazifalar_egasi_id_idx (actual time=0.047..0.048 rows=416.00 loops=1)
Index Cond: (egasi_id = 77)
Index Searches: 1
Buffers: shared hit=2 read=1
I/O Timings: shared read=0.010
Planning:
Buffers: shared hit=9
Planning Time: 0.128 ms
Execution Time: 1.660 ms
(17 rows)Endi 418 sahifa: bajarildi ni bilish uchun 415 ta jadval sahifasi ochildi.
- Covering, Index Only Scan0.15 ms9 sahifa
- Oddiy FK indeksi + jadval1.66 ms418 sahifa
Manba: IlmHamroh kanon o'lchovi, 2026-10-24: PostgreSQL 18.6, Docker (WSL2), 2 mln vazifa
4.2 Visibility map: nega VACUUM kerak
Kanon so'rovdan oldin VACUUM vazifalar qildi. Nega?
MVCC darsida ko'rgandik: jadvalda bitta qatorning bir nechta versiyasi yashaydi, qaysi biri qaysi tranzaksiyaga ko'rinishi xmin/xmax da yozilgan. Indeksda bu ma'lumot yo'q. Indeks "77-egasining qatori shu yerda" deydi, lekin "u hozir tirikmi?" ni bilmaydi.
Buni hal qiladigan narsa — visibility map (ko'rinish xaritasi): har jadval sahifasi uchun bitta belgi "bu sahifadagi hamma qatorlar hammaga ko'rinadi". Belgi qo'yilgan sahifa uchun baza jadvalga bormaydi. Belgisiz sahifa uchun boradi — bu Heap Fetches hisoblagichi. Belgilarni VACUUM qo'yadi (avtomatik — autovacuum).
flowchart TD
A["So'rovga kerak hamma ustunlar<br/>indeksdami?"] -->|yo'q| B["Index Scan / Bitmap:<br/>jadval sahifasi o'qiladi"]
A -->|ha| C["Index Only Scan"]
C --> D{"Sahifa visibility map'da<br/>'hammaga ko'rinadi'?"}
D -->|ha| E["Javob indeksdan"]
D -->|yo'q| F["Jadvalga borib tekshirish<br/>(Heap Fetches +1)"]Diagrammani pastdan o'qing: Index Only Scan ham ba'zan jadvalga boradi. Yangi yozilgan, hali VACUUM ko'rmagan sahifalar uchun.
Biz buni o'zimiz sinadik (PostgreSQL 17.6). Jadvalda autovacuum ataylab o'chirilgan (autovacuum_enabled = off), aks holda u o'zi VACUUM qilib qo'yishi mumkin. TIMING OFF — vaqtsiz, faqat qatorlar soni:
CREATE TABLE t_ios (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
egasi int, holat boolean, matn text
) WITH (autovacuum_enabled = off);
INSERT INTO t_ios (egasi, holat, matn)
SELECT g % 2000, g % 3 = 0, 'matn ' || g
FROM generate_series(1, 400000) g;
CREATE INDEX t_ios_cover ON t_ios (egasi) INCLUDE (holat);
ANALYZE t_ios;
EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) FILTER (WHERE holat) FROM t_ios WHERE egasi = 77;
VACUUM t_ios;
EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) FILTER (WHERE holat) FROM t_ios WHERE egasi = 77;
DROP TABLE t_ios;Natija:
CREATE TABLE
INSERT 0 400000
CREATE INDEX
ANALYZE
QUERY PLAN
------------------------------------------------------------------------
Aggregate (actual rows=1 loops=1)
-> Bitmap Heap Scan on t_ios (actual rows=200 loops=1)
Recheck Cond: (egasi = 77)
Heap Blocks: exact=200
-> Bitmap Index Scan on t_ios_cover (actual rows=200 loops=1)
Index Cond: (egasi = 77)
(6 rows)
VACUUM
QUERY PLAN
----------------------------------------------------------------------------
Aggregate (actual rows=1 loops=1)
-> Index Only Scan using t_ios_cover on t_ios (actual rows=200 loops=1)
Index Cond: (egasi = 77)
Heap Fetches: 0
(4 rows)
DROP TABLEBirinchi reja — VACUUM'dan oldin: covering indeks bor, lekin planner Bitmap Heap Scan tanladi. Ikkinchisi — VACUUM'dan keyin: Index Only Scan, Heap Fetches: 0.
Planner visibility map bo'shligini bilardi: Index Only Scan baribir har qatorni jadvaldan tekshirishga majbur bo'lardi. Shuning uchun u arzonroq Bitmap'ni tanladi. Xulosa: tez-tez yangilanadigan jadvalda Index Only Scan'ning foydasi autovacuum qanchalik tez ishlashiga bog'liq.
4.3 INCLUDE yoki kalitga qo'shish?
(egasi_id) INCLUDE (bajarildi) o'rniga (egasi_id, bajarildi) deb yozsa ham Index Only Scan bo'lardi. Farqi:
| Kalit ustuni | INCLUDE ustuni |
|
|---|---|---|
| Tartiblanadimi | ha | yo'q |
Index Cond da ishlatiladimi |
ha | yo'q (faqat filtr) |
| UNIQUE'ga kiradimi | ha | yo'q |
| Yuqori daraxt tugunlarida | bor | yo'q (faqat barglarda) |
Eng muhim foydasi — UNIQUE bilan. UNIQUE (email) INCLUDE (ism) — noyoblik faqat email bo'yicha, lekin login so'rovi ismni ham indeksdan oladi. UNIQUE (email, ism) esa noyoblik ma'nosini buzardi: bir email ikki xil ism bilan o'tib ketardi.
Covering indeksni har so'rovga qurmang. So'rovga yana bitta ustun kerak bo'lsa (SELECT matn ni qo'shing), Index Only Scan yo'qoladi — 3-mashqda buni ko'rasiz. Katta ustunlarni (matn) INCLUDE ga qo'shish indeksni jadval hajmiga yaqinlashtiradi.
Tekshirib ko'ring: Rejada
Index Only Scan … Heap Fetches: 380(416 qatordan). Bu nimani bildiradi va nima qilish kerak?
Javob
Indeks so'rovni qoplaydi, lekin 380 qator uchun baza jadvalga borib "bu versiya ko'rinadimi?" deb tekshirdi — ularning sahifalari visibility map'da belgilanmagan. Ya'ni Index Only Scan amalda oddiy Index Scan'ga yaqin ishladi. Sabab — yaqinda ko'p yangilanish bo'lgan va VACUUM hali ulgurmagan. VACUUM jadval (yoki autovacuum sozlamalarini tezlashtirish) belgilarni qaytaradi.
5. Node qadami: loyihada bular allaqachon bor
Bu darsda yangi indeks qo'shilmadi. Uchala tur kanon sxemasida avvaldan ishlayapti (holat 64 — API ulangan sxema):
| Indeks | Turi | Qayerdan |
|---|---|---|
foydalanuvchilar_email_key |
expression, unique | birinchi jadval darsi |
vazifalar_egasi_id_matn_excl |
tarkibiy + expression + partial (EXCLUDE) | soft delete darsi, 014 da cheklovga aylandi |
vazifalar_royxat_idx |
tarkibiy + partial | sxema review darsi |
vazifalar_tartib_idx |
tarkibiy + partial | API ulangan dars (013) |
vazifalar_ota_id_idx |
partial (WHERE ota_id IS NOT NULL) |
ALTER TABLE darsi |
Covering g'oyasi API'da ham ishladi. GET /v1/vazifalar?jami=true ro'yxatdan tashqari umumiy sonni ham so'raydi: SELECT count(*) … WHERE v.egasi_id = $1 AND v.ochirilgan_vaqt IS NULL. Bunga kerak hamma narsa vazifalar_tartib_idx da bor: egasi_id — kalitda, ochirilgan_vaqt IS NULL — indeks sharti. Kanon ombor yuborgan SQL'ning rejasini katta bazada (013/014 bilan qayta qurilgan) ko'rdi (reja-64.txt, ikkinchi so'rov):
Aggregate (actual time=0.095..0.096 rows=1.00 loops=1)
Buffers: shared hit=151
-> Index Only Scan using vazifalar_tartib_idx on vazifalar v (actual time=0.009..0.071 rows=479.00 loops=1)
Buffers: shared hit=151
Buffers: shared hit=1
Execution Time: 0.108 msNima bo'ldi? 479 vazifa jadvalga bormasdan, faqat indeksdan sanaldi — INCLUDE siz ham, chunki kerakli ustun kalitning o'zida. Eslatma boti esa kursda keyinroq quriladi — t58_qisman o'lchovdan keyin o'chirildi. Bot paydo bo'lganda indeks so'rov bilan birga, alohida SQL faylda qo'shiladi.
Sizning qadamingiz: \d vazifalar chiqishidagi "Indexes:" bo'limini o'qing va har indeksning yonida turini (partial — WHERE, expression — lower(...), unique) belgilang.
6. Ko'p uchraydigan xatolar
| Xato | Nima bo'ladi | To'g'risi |
|---|---|---|
| So'rovda partial shart yo'q | Partial indeks ishlatilmaydi | Shartni so'rovga aynan qo'shing |
email = $1, indeks lower(email) da |
Seq Scan | lower(email) = lower($1) |
date(timestamptz) ga indeks |
must be marked IMMUTABLE |
AT TIME ZONE 'Asia/Tashkent' bilan |
| Ifoda qavssiz | syntax error at or near "::" |
Ifodani yana bir qavsga oling |
UNIQUE (…) WHERE … cheklov sifatida |
Sintaksis xatosi | CREATE UNIQUE INDEX … WHERE … |
| Index Only Scan kutilgan, lekin Bitmap | Visibility map bo'sh | VACUUM, autovacuum'ni tekshiring |
7. Mashqlar
Mashqlar kurs/mashqlar/26/58-partial/ da. SQL yechimlar holat 40 bazasida tekshirilgan (biz ishga tushirdik, PostgreSQL 17.6).
1-mashq (oson): to'ldiring
- Faqat shartga mos qatorlarni saqlaydigan indeks —
- Indeks ifodasidagi funksiya bo'lishi shart bo'lgan belgi —
- Kalitdan tashqari qo'shimcha ustun qo'shadigan kalit so'z —
- Jadvalga bormasdan, faqat indeksdan o'qish — Index Scan
- Index Only Scan'da jadvalga borishlar soni — Heap
Yechim
- partial —
CREATE INDEX … WHERE …. - IMMUTABLE — bir xil kirish, har doim bir xil natija.
- INCLUDE — ustun indeks yozuviga yuk sifatida qo'shiladi, tartiblanmaydi.
- Only — visibility map'da belgilangan sahifalar uchun.
- Fetches — VACUUM'dan keyin 0 ga yaqinlashadi.
2-mashq (o'rta): bitta "asosiy" vazifa
Ilovaga "asosiy vazifa" (ro'yxat tepasiga qadaladigan) imkoniyati kerak: har foydalanuvchida ko'pi bilan bitta. Buni bazada kafolatlang. Tranzaksiyada asosiy boolean ustunini qo'shing, partial unique indeks yarating va sinang: Aziz (1) va Dilnoza (2) bittadan asosiy vazifa belgilasin — o'tsin; Azizning ikkinchisi — rad etilsin. Oxirida ROLLBACK.
Yechim
BEGIN;
ALTER TABLE vazifalar
ADD COLUMN asosiy boolean NOT NULL DEFAULT false;
CREATE UNIQUE INDEX vazifalar_asosiy_key
ON vazifalar (egasi_id) WHERE asosiy;
UPDATE vazifalar SET asosiy = true WHERE id = 2;
UPDATE vazifalar SET asosiy = true
WHERE id = (SELECT min(id) FROM vazifalar WHERE egasi_id = 2);
SELECT egasi_id, count(*) FROM vazifalar
WHERE asosiy GROUP BY 1 ORDER BY 1;
-- Azizning ikkinchi asosiy vazifasi
UPDATE vazifalar SET asosiy = true WHERE id = 8;
ROLLBACK;Natija:
egasi_id | count
----------+-------
1 | 1
2 | 1
(2 rows)
ERROR: duplicate key value violates unique constraint "vazifalar_asosiy_key"
DETAIL: Key (egasi_id)=(1) already exists.Indeksga faqat asosiy = true qatorlar kiradi, kaliti — egasi_id. Shuning uchun bitta egasida ikkita true — takror kalit. false lar indeksda yo'q, ular cheksiz bo'lishi mumkin. Indeks juda kichik: har foydalanuvchiga bitta yozuv. Ilova "boshqasini asosiy qilish" ni bitta tranzaksiyada qiladi: avval eskisini false, keyin yangisini true.
3-mashq (qiyin): Index Only Scan qachon ishlaydi
400 000 qatorli jadval yarating (WITH (autovacuum_enabled = off) — autovacuum aralashmasin), (egasi) INCLUDE (holat) indeksini qo'ying va uch rejani EXPLAIN (COSTS OFF) bilan oling: (1) VACUUM'dan oldin, (2) keyin, (3) so'rovga max(matn) qo'shilganda. Har natijani tushuntiring.
Yechim
CREATE TABLE t58_ios (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
egasi int, holat boolean, matn text
) WITH (autovacuum_enabled = off);
INSERT INTO t58_ios (egasi, holat, matn)
SELECT g % 2000, g % 3 = 0, 'matn ' || g
FROM generate_series(1, 400000) g;
CREATE INDEX t58_ios_cover ON t58_ios (egasi) INCLUDE (holat);
ANALYZE t58_ios;
EXPLAIN (COSTS OFF)
SELECT count(*) FILTER (WHERE holat) FROM t58_ios WHERE egasi = 77;
VACUUM t58_ios;
EXPLAIN (COSTS OFF)
SELECT count(*) FILTER (WHERE holat) FROM t58_ios WHERE egasi = 77;
EXPLAIN (COSTS OFF)
SELECT count(*) FILTER (WHERE holat), max(matn)
FROM t58_ios WHERE egasi = 77;
DROP TABLE t58_ios;Natija:
QUERY PLAN
------------------------------------------------
Aggregate
-> Bitmap Heap Scan on t58_ios
Recheck Cond: (egasi = 77)
-> Bitmap Index Scan on t58_ios_cover
Index Cond: (egasi = 77)
(5 rows)
QUERY PLAN
------------------------------------------------------
Aggregate
-> Index Only Scan using t58_ios_cover on t58_ios
Index Cond: (egasi = 77)
(3 rows)
QUERY PLAN
------------------------------------------------
Aggregate
-> Bitmap Heap Scan on t58_ios
Recheck Cond: (egasi = 77)
-> Bitmap Index Scan on t58_ios_cover
Index Cond: (egasi = 77)
(5 rows)- VACUUM yo'q — visibility map bo'sh. Index Only Scan har qatorni baribir jadvaldan tekshirardi; planner arzonroq Bitmap'ni tanladi.
- VACUUM sahifalarni "hammaga ko'rinadi" deb belgiladi — Index Only Scan.
matnindeksda yo'q — jadvalga borish shart, Index Only Scan imkonsiz.
ANALYZE alohida chaqirildi: autovacuum o'chiq bo'lgani uchun statistika o'zi yig'ilmaydi.
8. Real ishda
- Soft delete bor har loyihada
WHERE deleted_at IS NULLli partial unique indeks — standart naqsh (Rails, Laravel, Prisma loyihalarida ham). - Login —
lower(email)(yokicitextturi) indeksi; email'ni saqlashdan oldin kichik harfga o'tkazish ham keng tarqalgan. - Navbat jadvallari —
WHERE holat = 'kutmoqda'partial indeks: millionlab bajarilgan ish orasida yangilarini tez topish (SKIP LOCKED navbat darsidagi jadval uchun ham mos). - Intervyuda: "Partial index qachon kerak?", "Index Only Scan nega ba'zan jadvalga boradi?", "
INCLUDEva kalit ustuni farqi?"
Xulosa
- Partial indeks (
WHERE …) faqat kerakli qatorlarni saqlaydi — kichik va tez; so'rov uning shartini aynan o'z ichiga olishi kerak. - Partial unique indeks — "faqat faol yozuvlar orasida noyob" qoidasi; oddiy
UNIQUEcheklovi buni qila olmaydi. - Expression indeks ifoda natijasini saqlaydi; so'rov ifodasi bir xil bo'lsin; funksiya IMMUTABLE bo'lsin, ifoda qo'shimcha qavsda.
- Covering indeks (
INCLUDE) — Index Only Scan: kanonda 418 → 9 sahifa. - Index Only Scan visibility map'ga tayanadi — VACUUM'siz u jadvalga qaytadi.
Keyingi dars: Indeks turlari: GIN, GiST, BRIN va pg_trgm — B-tree yordam bera olmaydigan qidiruvlar: ILIKE '%…%' (529 → 13 ms), massiv va JSONB ichidan qidirish, 32 kB'lik BRIN indeksi.
Manbalar
- PostgreSQL 18 hujjati — Partial Indexes: https://www.postgresql.org/docs/18/indexes-partial.html
- PostgreSQL 18 hujjati — Indexes on Expressions: https://www.postgresql.org/docs/18/indexes-expressional.html
- PostgreSQL 18 hujjati — Index-Only Scans and Covering Indexes: https://www.postgresql.org/docs/18/indexes-index-only-scans.html
- PostgreSQL 18 hujjati — Visibility Map: https://www.postgresql.org/docs/18/storage-vm.html
- IlmHamroh kanon o'lchovi (PostgreSQL 18.6, 2 mln vazifa, 2026-10-24); IMMUTABLE xatolari, partial unique tajribasi, visibility map tajribasi va mashqlar — biz ishga tushirdik (PostgreSQL 17.6, Windows 11, i5-12500H).
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!