IlmHamroh
JavaScript Full-stack/26-qism. SQL va PostgreSQL58/64-dars21 daqiqa
Mundarija (22)

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

  • WHERE bilan 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.
  • INCLUDE bilan 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:

sql
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:

text
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)
muddat indeksi: to'liq va partial
  • 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:

sql
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:

text
 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:

sql
EXPLAIN SELECT id FROM vazifalar
WHERE muddat BETWEEN '2025-03-01' AND '2025-03-02';

Natija:

text
                                                                    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:

sql
DROP INDEX t58_toliq;
EXPLAIN SELECT id FROM vazifalar
WHERE muddat BETWEEN '2025-03-01' AND '2025-03-02';

Natija:

text
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 ilova WHERE bajarildi = $1 deb parametr bilan so'rasa, planner $1 qiymatini 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:

sql
SELECT pg_get_indexdef('vazifalar_egasi_id_matn_key'::regclass);

Natija:

text
                                                             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:

sql
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:

text
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.
ROLLBACK

Uch qadam:

  1. Vazifa 1 Savatga tushdi — endi indeksda yo'q.
  2. Xuddi shu matnli yangi vazifa (49) qabul qilindi — faol vazifalar orasida takror yo'q.
  3. 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):

sql
EXPLAIN (ANALYZE) SELECT id FROM foydalanuvchilar
WHERE lower(email) = 'foydalanuvchi777@example.com';

Natija:

text
 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)
sql
EXPLAIN (ANALYZE) SELECT id FROM foydalanuvchilar
WHERE email = 'foydalanuvchi777@example.com';

Natija:

text
 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):

sql
CREATE INDEX ON vazifalar (date(yaratilgan_vaqt));
CREATE INDEX ON vazifalar
  (to_char(yaratilgan_vaqt, 'YYYY-MM-DD'));

Natija:

text
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:

sql
CREATE INDEX ON vazifalar
  ((yaratilgan_vaqt AT TIME ZONE 'Asia/Tashkent')::date);

Natija:

text
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:

sql
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:

text
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)

RESET

enable_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 jadval qiling. 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 kodi WHERE email ILIKE $1 deb 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):

sql
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:

text
 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:

text
 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.

Egasi 77: nechta bajarilgan? O'qilgan sahifalar
  • 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:

sql
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:

text
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 TABLE

Birinchi 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):

text
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 ms

Nima 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
sql
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:

text
 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
sql
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:

text
                   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)
  1. VACUUM yo'q — visibility map bo'sh. Index Only Scan har qatorni baribir jadvaldan tekshirardi; planner arzonroq Bitmap'ni tanladi.
  2. VACUUM sahifalarni "hammaga ko'rinadi" deb belgiladi — Index Only Scan.
  3. matn indeksda 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 NULL li partial unique indeks — standart naqsh (Rails, Laravel, Prisma loyihalarida ham).
  • Login — lower(email) (yoki citext turi) 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?", "INCLUDE va 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 UNIQUE cheklovi 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

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
PostgreSQL partial, expression va covering indekslar: Index Only Scan — IlmHamroh