Mundarija (32)
- Bu darsda
- 1. Nega bu kerak?
- 2. Oddiy view
- 2.1 View — saqlangan so'rov
- 2.2 View EXPLAIN'da yo'qoladi
- 2.3 SELECT * view'da muzlaydi
- 3. View orqali yozish
- 3.1 Avtomatik yangilanadigan view
- 3.2 Tuzoq: qator view'dan "chiqib ketadi"
- 3.3 WITH CHECK OPTION
- 3.4 Agregatli view yangilanmaydi
- 4. Hujumchi nigohi: security_barrier
- 4.1 View xavfsizlik devori sifatida
- 4.2 Tajriba
- 4.3 Himoya qoidalari
- 5. Materialized view
- 5.1 Kanon: foydalanuvchi statistikasi
- 5.2 O'qish
- 5.3 MV eskiradi
- 5.4 Ikki xil REFRESH va ularning qulflari
- 5.5 Unique indeks sharti va WITH NO DATA
- 5.6 MV — jadval kabi
- 5.7 Qachon va kim yangilaydi
- 6. Ko'p uchraydigan xatolar
- 7. Mashqlar
- 1-mashq (oson)
- 2-mashq (o'rta)
- 3-mashq (qiyin)
- 8. Loyiha qadami
- 9. Real ishda
- Xulosa
- Manbalar
PostgreSQL view va materialized view: updatable view, security_barrier, REFRESH CONCURRENTLY
Qisqacha: View — nom berilgan saqlangan so'rov: unga jadval kabi murojaat qilinadi, lekin har safar asl jadvaldan o'qiladi. Materialized view — natijasi diskka yozilgan view: o'qish tez, lekin ma'lumot eskiradi va
REFRESHbilan yangilanadi.REFRESH … CONCURRENTLYo'qishni to'smaydi, faqat unique indeks bo'lsa ishlaydi. Xavfsizlik uchun ishlatiladigan viewsecurity_barriersiz yashirin qatorlarni sizdirishi mumkin.
Bu darsda
- View EXPLAIN'da "erib ketishini" (inlining) va
SELECT *view'ning ustunlarini muzlatishini ko'rasiz. - View orqali
UPDATEqilasiz vaWITH CHECK OPTIONnima uchun kerakligini tushuntirasiz. security_barriersiz view qanday sizishini o'z tajribangizda ko'rasiz.- Materialized view yaratasiz,
REFRESHvaREFRESH … CONCURRENTLYning qulflarini solishtirasiz. vazifalarga foydalanuvchi statistikasi uchun materialized view qo'shamiz (holat 04).
Oldin bilishingiz kerak: Ilg'or constraint'lar va generated ustunlar, CTE (WITH), Denormalizatsiya va hisoblangan maydonlar, Qulflar: row, table va advisory.
1. Nega bu kerak?
O'tgan darsda qoidalarni bazaga topshirdik. Endi so'rovlarni. Ikki vaziyat.
Birinchi: WHERE ochirilgan_vaqt IS NULL — "Savatda bo'lmagan" sharti. U API'ning deyarli har so'rovida bor. Bitta joyda unutildi — o'chirilgan vazifa ro'yxatda chiqdi. 26-qismda shuning uchun faol_vazifalar ko'rinishi (view) yaratilgan edi: shart bir marta yoziladi, hamma uni nom bilan ishlatadi.
Ikkinchi: frontend'dagi "Statistika" sahifasi. Har foydalanuvchi uchun: jami vazifalar, bajarilganlar, foiz, muddati o'tganlar. Bu — butun vazifalar jadvalini guruhlash (Agregatlar). Sahifani har ochganda millionlab qatorni qayta sanash shart emas: statistika bir necha daqiqa eski bo'lsa ham hech narsa bo'lmaydi.
O'xshatish: choyxonadagi menyu taxtasi. Oddiy view — ofitsiant: har so'ralganda oshxonaga borib, "bugun nima bor?" deb so'rab keladi — doim yangi, lekin har safar yo'l. Materialized view — devordagi taxta: ertalab yozilgan, bir qarashda o'qiladi, lekin osh tugasa ham taxtada turadi, toki kimdir o'chirib qayta yozmaguncha.
flowchart LR
S["SELECT ... FROM view"] --> V["View so'rovi ochiladi"] --> J["vazifalar jadvali"]
M["SELECT ... FROM MV"] --> D["MV ning o'z qatorlari"]
R["REFRESH (fon ishi)"] --> J
R --> DNimaga qarang: view'dan o'qish har safar asl jadvalga boradi; MV'dan o'qish faqat o'z qatorlariga — jadvalga faqat REFRESH boradi.
2. Oddiy view
2.1 View — saqlangan so'rov
Denormalizatsiya darsida view'ni bir gap bilan ko'rgan edik. Ichiga qaraymiz. Bu darsdagi so'rovlarni holat 04 bazasida, o'z PostgreSQL 17.6 klasterimizda ishga tushirdik (kanon tajribasi (IlmHamroh, 2026-10, PostgreSQL 18.6) bilan mos; farq bo'lsa — aytamiz):
SELECT pg_get_viewdef('faol_vazifalar', true); pg_get_viewdef
----------------------------------
SELECT id, +
egasi_id, +
matn, +
bajarildi, +
yaratilgan_vaqt, +
ustuvorlik, +
muddat, +
bajarilgan_vaqt, +
ota_id, +
izohlar_soni, +
yangilangan_vaqt, +
ochirilgan_vaqt, +
versiya, +
tartib +
FROM vazifalar +
WHERE ochirilgan_vaqt IS NULL;
(1 row)pg_get_viewdef — view qaysi so'rov ekanini qaytaradi (+ — psql'da qatorning davomi). E'tibor bering: 26-qismda view SELECT * bilan yaratilgan edi, lekin bazada * yo'q — aniq ustunlar ro'yxati. Bu muhim, pastda ko'ramiz.
2.2 View EXPLAIN'da yo'qoladi
View alohida "jadval" emas. Planner so'rovni view ta'rifi bilan birlashtirib, bitta so'rovga aylantiradi — bu inlining (ichiga ochish):
EXPLAIN (COSTS OFF)
SELECT id FROM faol_vazifalar WHERE egasi_id = 1; QUERY PLAN
----------------------------------------------------------
Seq Scan on vazifalar
Filter: ((ochirilgan_vaqt IS NULL) AND (egasi_id = 1))
(2 rows)Rejada faol_vazifalar umuman yo'q — faqat vazifalar va ikkala shart birga: view'niki (ochirilgan_vaqt IS NULL) va bizniki (egasi_id = 1). Demak oddiy view tezlik bermaydi va olib ham ketmaydi: u qulaylik va xavfsizlik vositasi, kesh emas. (8 qatorli jadvalda Seq Scan — indeks kerak emas, EXPLAIN ANALYZE'ni o'qish.)
2.3 SELECT * view'da muzlaydi
SELECT * view yaratilgan paytdagi ustunlarga aylanadi. Jadvalga keyin ustun qo'shilsa, view uni ko'rmaydi. Va teskarisi: view ishlatayotgan ustunni o'chirib bo'lmaydi. Biz sinadik:
BEGIN;
CREATE VIEW v_teglar AS SELECT * FROM teglar;
ALTER TABLE teglar ADD COLUMN rang text;
SELECT * FROM v_teglar ORDER BY id LIMIT 1;
ALTER TABLE teglar DROP COLUMN nom;
ROLLBACK;BEGIN
CREATE VIEW
ALTER TABLE
id | egasi_id | nom
----+----------+--------
1 | 1 | #bozor
(1 row)
ERROR: cannot drop column nom of table teglar because other objects depend on it
DETAIL: view v_teglar depends on column nom of table teglar
HINT: Use DROP ... CASCADE to drop the dependent objects too.
ROLLBACKrangqo'shildi, lekin view'da yo'q — viewid, egasi_id, nombo'lib qolgan.nomni o'chirish rad etildi: "boshqa obyektlar unga bog'liq". HINTCASCADEtaklif qiladi — lekin u view'ni ham o'chiradi.
Amaliyot: real loyiha sxemasi darsidagi topilma shu edi: shuning uchun kanon migratsiyalari vazifalar ga ustun qo'shganda faol_vazifalar ni qayta yaratadi (CREATE OR REPLACE VIEW faqat oxiriga ustun qo'sha oladi). Migratsiyada view'lar — "unutilgan bog'liqlik"ning birinchi manbai.
Tekshirib ko'ring: Oddiy view ichidagi so'rov sekin bo'lsa, view'dan o'qish tezroq bo'ladimi?
Javob
Yo'q. View inline qilinadi — planner xuddi siz so'rovni to'g'ridan yozgandek bajaradi. Tezlashtirish uchun indeks yoki materialized view kerak. Ba'zan view hatto sekinlashtiradi: ichida keraksiz JOIN yoki ORDER BY bo'lsa, tashqi so'rov ularni ham bajaradi.
3. View orqali yozish
3.1 Avtomatik yangilanadigan view
faol_vazifalar — bitta jadvaldan oddiy SELECT … WHERE. Bunday view avtomatik yangilanadigan (updatable): unga INSERT, UPDATE, DELETE yozsangiz, Postgres buni asl jadvalga o'tkazadi. Shartlari (hujjat bo'yicha, "CREATE VIEW"): FROM da bitta jadval yoki shunday view; GROUP BY, DISTINCT, agregat, UNION, LIMIT yo'q.
BEGIN;
UPDATE faol_vazifalar
SET bajarildi = true, bajarilgan_vaqt = '2026-10-24 10:00+05'
WHERE id = 1
RETURNING id, bajarildi, versiya;
ROLLBACK;BEGIN
id | bajarildi | versiya
----+-----------+---------
1 | t | 2
(1 row)
UPDATE 1
ROLLBACKAzizning 1-vazifasi bajarildi. vazifalar dagi trigger'lar ham ishladi — versiya oshdi (Optimistic concurrency). View orqali yozish asl jadvalga yozishning o'zi.
3.2 Tuzoq: qator view'dan "chiqib ketadi"
View orqali shunday o'zgarish ham mumkin: qatorni view shartidan chiqarib yuborish. Kanon topilmasi:
BEGIN;
UPDATE faol_vazifalar
SET ochirilgan_vaqt = '2026-10-24 10:00+05'
WHERE id = 3 RETURNING id;
SELECT count(*) FROM faol_vazifalar WHERE id = 3;
ROLLBACK;BEGIN
id
----
3
(1 row)
UPDATE 1
count
-------
0
(1 row)
ROLLBACK"Faol vazifalar" ko'rinishi orqali vazifani Savatga tashladik — va u o'sha view'dan yo'qoldi. Ba'zan bu kerakli xulq. Lekin view "faqat faol vazifalar bilan ishlash" uchun berilgan bo'lsa — bu teshik.
3.3 WITH CHECK OPTION
WITH CHECK OPTION — view orqali yozilgan har yangi qator view shartiga mos kelishini talab qiladi:
BEGIN;
CREATE VIEW t_faol_tekshir AS
SELECT * FROM vazifalar WHERE ochirilgan_vaqt IS NULL
WITH CASCADED CHECK OPTION;
UPDATE t_faol_tekshir SET ochirilgan_vaqt = '2026-10-24 10:00+05'
WHERE id = 3;
ROLLBACK;BEGIN
CREATE VIEW
ERROR: new row violates check option for view "t_faol_tekshir"
DETAIL: Failing row contains (3, 5, Mijozga taklifnoma yuborish #ish, t, 2026-10-01 05:00:00+00, orta, null, 2026-10-01 07:00:00+00, null, 0, 2026-10-26 08:39:20.938+00, 2026-10-24 05:00:00+00, 2, 1).
ROLLBACK"Yangi qator t_faol_tekshir view'ining tekshiruv shartini buzadi". DETAIL dagi yangilangan_vaqt — trigger qo'ygan joriy vaqt; bu ustun har ishga tushirishda boshqacha bo'ladi (kanonda 2026-10-24 14:46:25.548+00).
CASCADED va LOCAL farqi view boshqa view ustiga qurilganda chiqadi: CASCADED — pastdagi hamma view shartlari ham tekshiriladi, LOCAL — faqat shu view'niki. Sukut — CASCADED. Xavfsizlik nuqtai nazaridan doim CASCADED.
3.4 Agregatli view yangilanmaydi
BEGIN;
CREATE VIEW t_soni AS
SELECT egasi_id, count(*) n FROM vazifalar GROUP BY egasi_id;
UPDATE t_soni SET n = 0;
ROLLBACK;BEGIN
CREATE VIEW
ERROR: cannot update view "t_soni"
DETAIL: Views containing GROUP BY are not automatically updatable.
HINT: To enable updating the view, provide an INSTEAD OF UPDATE trigger or an unconditional ON UPDATE DO INSTEAD rule.
ROLLBACKXabar aniq: "GROUP BY li view'lar avtomatik yangilanmaydi". HINT ikki yo'l ko'rsatadi: INSTEAD OF trigger yoki RULE. INSTEAD OF trigger — "view'ga yozish o'rniga shu funksiyani bajar" (Triggerlar darsida). RULE — eski mexanizm, yangi kodda ishlatilmaydi.
Tekshirib ko'ring:
faol_vazifalarorqaliINSERTqilinsa,ochirilgan_vaqtqanday bo'ladi va qator view'da ko'rinadimi?
Javob
ochirilgan_vaqt ga hech narsa yozilmasa — jadval sukuti, NULL. Demak qator faol va view'da ko'rinadi. Agar INSERT da ochirilgan_vaqt ni qo'lda bersangiz — faol_vazifalar da CHECK OPTION yo'q, qator kiradi, lekin view'da ko'rinmaydi. 2-mashqda CHECK OPTION li view bilan bu holatni sinaysiz.
4. Hujumchi nigohi: security_barrier
4.1 View xavfsizlik devori sifatida
View ko'pincha ruxsat vositasi: foydalanuvchiga jadvalga emas, faqat view'ga huquq beriladi (Rollar va huquqlar darsida). Masalan, aziz_v — faqat Azizning vazifalari. Aziz vazifalar ni o'qiy olmaydi, faqat aziz_v ni.
Hujumchi nima qiladi? U view'ga o'z funksiyasini qo'shib so'rov yozadi: SELECT … FROM aziz_v WHERE josus(matn). Funksiya o'ziga berilgan har qiymatni qayerdadir saqlaydi (jadvalga yozadi, NOTICE chiqaradi, xato matniga qo'shadi). Agar planner bu funksiyani view shartidan oldin bajarsa — funksiya boshqalarning qatorlarini ham "ko'radi".
Planner nima uchun shunday qiladi? U shartlarni narxi bo'yicha tartiblaydi: arzon shart oldin. Hujumchi funksiyasiga ataylab juda kichik narx (COST) yozadi.
4.2 Tajriba
Kanon bu tajribani RAISE NOTICE bilan qildi: oddiy view'da 48 ta NOTICE chiqdi (bazadagi hamma vazifa matni), security_barrier bilan — 13 ta (faqat Azizniki). Biz xuddi shu tajribani ixcham shaklda takrorladik: funksiya ko'rgan matnni vaqtinchalik jadvalga yozadi, keyin sanaymiz:
BEGIN;
CREATE TEMP TABLE korildi (matn text);
CREATE FUNCTION josus(t text) RETURNS boolean
LANGUAGE plpgsql COST 0.0000001 AS $$
BEGIN INSERT INTO korildi VALUES (t); RETURN true; END $$;
CREATE VIEW aziz_v AS
SELECT id, matn FROM vazifalar WHERE egasi_id = 1;
CREATE VIEW aziz_sb WITH (security_barrier) AS
SELECT id, matn FROM vazifalar WHERE egasi_id = 1;
SELECT count(*) AS natija FROM aziz_v WHERE josus(matn);
SELECT count(*) AS korgan FROM korildi;
TRUNCATE korildi;
SELECT count(*) AS natija FROM aziz_sb WHERE josus(matn);
SELECT count(*) AS korgan FROM korildi;
ROLLBACK;BEGIN
CREATE TABLE
CREATE FUNCTION
CREATE VIEW
CREATE VIEW
natija
--------
13
(1 row)
korgan
--------
48
(1 row)
TRUNCATE TABLE
natija
--------
13
(1 row)
korgan
--------
13
(1 row)
ROLLBACKNatija ikkalasida 13 — hujumchi ekranda hech narsa ortiqcha ko'rmaydi. Lekin oddiy view'da funksiya 48 ta matnni ko'rdi va saqladi: Dilnozaning, Madinaning — hammaniki. security_barrier bilan — faqat 13 ta, Azizniki.
Sababi rejada ko'rinadi. Funksiyaning ichi bu yerda muhim emas — u faqat true qaytaradi:
BEGIN;
CREATE FUNCTION josus(t text) RETURNS boolean
LANGUAGE plpgsql COST 0.0000001 AS
$$ BEGIN RETURN true; END $$;
CREATE VIEW aziz_v AS
SELECT id, matn FROM vazifalar WHERE egasi_id = 1;
CREATE VIEW aziz_sb WITH (security_barrier) AS
SELECT id, matn FROM vazifalar WHERE egasi_id = 1;
EXPLAIN (COSTS OFF) SELECT count(*) FROM aziz_v WHERE josus(matn);
EXPLAIN (COSTS OFF) SELECT count(*) FROM aziz_sb WHERE josus(matn);
ROLLBACK;BEGIN
CREATE FUNCTION
CREATE VIEW
CREATE VIEW
QUERY PLAN
--------------------------------------------------
Aggregate
-> Seq Scan on vazifalar
Filter: (josus(matn) AND (egasi_id = 1))
(3 rows)
QUERY PLAN
--------------------------------------
Aggregate
-> Subquery Scan on aziz_sb
Filter: josus(aziz_sb.matn)
-> Seq Scan on vazifalar
Filter: (egasi_id = 1)
(5 rows)
ROLLBACKOddiy view'da ikkala shart bitta Filter da va josus birinchi. security_barrier da view alohida qadam (Subquery Scan) bo'lib qoldi: avval ichkarida egasi_id = 1, faqat keyin tashqarida josus.
Bir qiziq kuzatuv: biz avval josus ni LANGUAGE sql bilan (SELECT true) yozgan edik — ikkala rejada ham u umuman yo'qoldi. Oddiy SQL funksiyani planner ichiga ochib, doimiy true ga aylantirdi va shartni olib tashladi. plpgsql funksiya esa "qora quti": planner ichiga qaramaydi.
4.3 Himoya qoidalari
- Ruxsat uchun ishlatiladigan har view —
WITH (security_barrier). Narxi: planner view ichiga ba'zi optimallashtirishlarni olib kira olmaydi. - LEAKPROOF funksiyalar (masalan, oddiy
=operatori ortidagi funksiya) — "hech narsa sizdirmaydi" deb belgilangan; planner ularni barrier'dan oldin ham ishlatishi mumkin, shuning uchun indekslar ishlaydi. FunksiyaniLEAKPROOFdeb faqat superuser belgilay oladi (hujjat bo'yicha, "CREATE FUNCTION"). - Qatorlarni foydalanuvchiga ko'ra cheklashning zamonaviy yo'li — Row Level Security (Row Level Security); u siyosat shartlarini xuddi shunday himoyalaydi.
5. Materialized view
5.1 Kanon: foydalanuvchi statistikasi
Materialized view (MV) — natijasi diskka yozilgan view. Kanon sql/103-statistika-mv.sql (kod o'zgarmagan, qatorlar bo'lingan, izohlar qisqartirilgan):
CREATE MATERIALIZED VIEW foydalanuvchi_statistikasi AS
SELECT f.id AS foydalanuvchi_id,
count(v.id) AS jami,
count(v.id) FILTER (WHERE v.bajarildi) AS bajarilgan,
count(v.id) FILTER (WHERE NOT v.bajarildi
AND v.muddat < now()) AS muddati_otgan,
round(100.0 * count(v.id) FILTER (WHERE v.bajarildi)
/ nullif(count(v.id), 0), 1) AS bajarilgan_foiz,
avg(v.bajarilgan_vaqt - v.yaratilgan_vaqt)
FILTER (WHERE v.bajarildi) AS ortacha_bajarish,
max(v.yaratilgan_vaqt) AS oxirgi_vazifa,
now() AS yangilangan
FROM foydalanuvchilar f
LEFT JOIN vazifalar v
ON v.egasi_id = f.id AND v.ochirilgan_vaqt IS NULL
GROUP BY f.id;
-- REFRESH ... CONCURRENTLY uchun UNIQUE indeks SHART
CREATE UNIQUE INDEX foydalanuvchi_statistikasi_pkey
ON foydalanuvchi_statistikasi (foydalanuvchi_id);Qarorlar:
LEFT JOIN— vazifasi yo'q foydalanuvchi ham statistikada bo'lsin (0 bilan). JOIN'lar darsidagi naqsh.FILTER (WHERE …)— bitta o'tishda bir nechta sanoq (Agregatlar).nullif(count, 0)— nolga bo'lishdan himoya: vazifasiz foydalanuvchida foizNULL, xato emas.now() AS yangilangan— eng muhim ustun: statistika qachon hisoblangani. Frontend uni ko'rsatadi: "10:05 holatiga". MV eskirishi yashirin bo'lmasligi kerak.- Unique indeks —
CONCURRENTLYuchun shart (pastda).
5.2 O'qish
\pset null '(null)'
SELECT foydalanuvchi_id, jami, bajarilgan, bajarilgan_foiz
FROM foydalanuvchi_statistikasi ORDER BY 1;Null display is "(null)".
foydalanuvchi_id | jami | bajarilgan | bajarilgan_foiz
------------------+------+------------+-----------------
1 | 12 | 5 | 41.7
2 | 6 | 3 | 50.0
3 | 6 | 3 | 50.0
4 | 9 | 4 | 44.4
5 | 6 | 3 | 50.0
6 | 4 | 2 | 50.0
7 | 4 | 2 | 50.0
8 | 0 | 0 | (null)
(8 rows)Shahnoza (8) — vazifasiz, foiz (null). Qolgan ustunlarni (muddati_otgan, ortacha_bajarish, yangilangan) bu yerda ko'rsatmadik: ular now() ga bog'liq va baza qurilgan vaqtga qarab har safar boshqacha. Masalan, kanonda Azizning o'rtacha bajarish vaqti 2 days 05:42:15.740922 chiqqan — namuna ma'lumotdagi bajarilgan_vaqt baza qurilgan paytdan olinadi.
5.3 MV eskiradi
BEGIN;
INSERT INTO vazifalar (egasi_id, matn)
VALUES (8, 'Shahnozaning birinchi vazifasi');
SELECT jami AS mv_dagi,
(SELECT count(*) FROM vazifalar WHERE egasi_id = 8) AS haqiqiy
FROM foydalanuvchi_statistikasi WHERE foydalanuvchi_id = 8;
REFRESH MATERIALIZED VIEW CONCURRENTLY foydalanuvchi_statistikasi;
SELECT jami AS refreshdan_keyin
FROM foydalanuvchi_statistikasi WHERE foydalanuvchi_id = 8;
ROLLBACK;BEGIN
INSERT 0 1
mv_dagi | haqiqiy
---------+---------
0 | 1
(1 row)
REFRESH MATERIALIZED VIEW
refreshdan_keyin
------------------
1
(1 row)
ROLLBACKVazifa qo'shildi — MV hali 0 deydi. REFRESH dan keyin — 1. MV o'zi hech qachon yangilanmaydi; kimdir REFRESH chaqirishi kerak.
5.4 Ikki xil REFRESH va ularning qulflari
REFRESH MATERIALIZED VIEW butun so'rovni qayta bajaradi va natijani almashtiradi. Bu paytda MV'ni o'qiganlar nima ko'radi? Qulflarni o'zimiz tekshirdik:
BEGIN;
REFRESH MATERIALIZED VIEW foydalanuvchi_statistikasi;
SELECT mode FROM pg_locks
WHERE relation = 'foydalanuvchi_statistikasi'::regclass
AND pid = pg_backend_pid() ORDER BY mode;
ROLLBACK;
BEGIN;
REFRESH MATERIALIZED VIEW CONCURRENTLY foydalanuvchi_statistikasi;
SELECT mode FROM pg_locks
WHERE relation = 'foydalanuvchi_statistikasi'::regclass
AND pid = pg_backend_pid() ORDER BY mode;
ROLLBACK;BEGIN
REFRESH MATERIALIZED VIEW
mode
---------------------
AccessExclusiveLock
ExclusiveLock
ShareLock
(3 rows)
ROLLBACK
BEGIN
REFRESH MATERIALIZED VIEW
mode
------------------
AccessShareLock
ExclusiveLock
RowExclusiveLock
(3 rows)
ROLLBACKOddiy REFRESH |
CONCURRENTLY |
|
|---|---|---|
| Eng og'ir qulf | AccessExclusiveLock |
ExclusiveLock |
MV'dan SELECT |
Kutadi | Ishlaydi (eski ma'lumotni ko'radi) |
| Qanday yangilaydi | Butunlay almashtiradi | Yangi natija bilan solishtirib, farqni yozadi |
| Talab | Yo'q | Unique indeks, MV oldin to'ldirilgan |
Statistika sahifasi nuqtai nazaridan (qulf mosligi — hujjatdagi jadval bo'yicha; biz yuqorida qulf turini o'lchadik, ikki sessiyada kutishni emas):
| Qadam | A sessiya (fon ishi) | B sessiya (sahifa) | Natija |
|---|---|---|---|
| 1 | REFRESH MATERIALIZED VIEW boshlandi |
SELECT … FROM foydalanuvchi_statistikasi |
B kutadi — AccessExclusiveLock |
| 2 | REFRESH tugadi |
— | B yangi raqamlarni oladi |
| 3 | REFRESH … CONCURRENTLY boshlandi |
Xuddi shu SELECT |
B darhol eski raqamlarni oladi |
| 4 | CONCURRENTLY tugadi |
Keyingi SELECT |
Yangi raqamlar |
AccessExclusiveLock bilan SELECT ham kutadi: katta MV yangilanayotgan 30 soniya davomida statistika sahifasi "qotadi". ExclusiveLock esa oddiy SELECT ning qulfi (AccessShareLock) bilan to'qnashmaydi (Qulflar darsidagi jadval). Narxi: CONCURRENTLY sekinroq — u natijani eski qatorlar bilan solishtiradi; o'zgarish katta bo'lsa, oddiy REFRESH tezroq bo'lishi mumkin (hujjat bo'yicha, "REFRESH MATERIALIZED VIEW"). Ikki REFRESH bir vaqtda ishlamaydi — ikkinchisi kutadi.
5.5 Unique indeks sharti va WITH NO DATA
BEGIN;
CREATE MATERIALIZED VIEW t_mv AS
SELECT egasi_id, count(*) FROM vazifalar GROUP BY 1;
REFRESH MATERIALIZED VIEW CONCURRENTLY t_mv;
ROLLBACK;
BEGIN;
CREATE MATERIALIZED VIEW t_mv2 AS SELECT 1 AS x WITH NO DATA;
SELECT * FROM t_mv2;
ROLLBACK;BEGIN
SELECT 7
ERROR: cannot refresh materialized view "public.t_mv" concurrently
HINT: Create a unique index with no WHERE clause on one or more columns of the materialized view.
ROLLBACK
BEGIN
CREATE MATERIALIZED VIEW
ERROR: materialized view "t_mv2" has not been populated
HINT: Use the REFRESH MATERIALIZED VIEW command.
ROLLBACK- Unique indekssiz
CONCURRENTLY— rad. HINT aniq: "MV'ning bir yoki bir nechta ustunidaWHEREsiz unique indeks yarating". Indeks orqali Postgres qaysi qator qaysi ekanini taniydi va farqni topadi. WITH NO DATA— MV yaratiladi, lekin bo'sh va "to'ldirilmagan". O'qish — xato, HINTREFRESHni taklif qiladi. Bu katta MV'ni migratsiyada tez yaratib, keyin fonda to'ldirish uchun. Lekin to'ldirilmagan MV'gaCONCURRENTLYishlamaydi — birinchiREFRESHoddiy bo'ladi.
5.6 MV — jadval kabi
SELECT relname, relkind,
pg_size_pretty(pg_total_relation_size(oid)) AS hajm
FROM pg_class
WHERE relname LIKE 'foydalanuvchi_statistikasi%' ORDER BY 1; relname | relkind | hajm
---------------------------------+---------+-------
foydalanuvchi_statistikasi | m | 40 kB
foydalanuvchi_statistikasi_pkey | i | 16 kB
(2 rows)relkind: m — materialized view, i — indeks. MV diskda joy egallaydi, indekslanadi, VACUUM qilinadi — haqiqiy jadval kabi. Oddiy view'da hajm yo'q: u faqat matn.
5.7 Qachon va kim yangilaydi
MV'ni yangilashning to'g'ri joyi — fon ishi (background job): jadval bo'yicha har N daqiqada REFRESH … CONCURRENTLY. Variantlar:
- Ilovaning o'z rejalashtiruvchisi (cron, navbat) — fon vazifalari 29-qism mavzusi, hozir bilish shart emas.
- Bazaning ichida —
pg_cronkengaytmasi (Extension'lar). - Har
INSERTda trigger bilanREFRESH— har yozish butun statistikani qayta hisoblaydi vaREFRESHlar navbatga turadi. Bu MV'ning ma'nosini yo'qotadi; har yozishda aniq son kerak bo'lsa — counter cache.
Qanchalik tez-tez? Savol: "foydalanuvchi qanchalik eski raqamga chiday oladi?" Statistika uchun 5–15 daqiqa odatda yetarli. yangilangan ustuni buni foydalanuvchiga halol ko'rsatadi.
6. Ko'p uchraydigan xatolar
1. View'ni "tezlashtirish" deb o'ylash. Oddiy view inline bo'ladi — tezlik bermaydi. Tezlik — indeks yoki MV.
2. View'da SELECT * va keyin jadvalga ustun qo'shish. View yangi ustunni ko'rmaydi; ustunni o'chirish esa bog'liqlik xatosi beradi. Migratsiyada view'ni qayta yarating.
3. CHECK OPTION siz yozish view'i. Qator view shartidan "chiqib ketadi". WITH CASCADED CHECK OPTION qo'ying.
4. Ruxsat uchun view security_barrier siz. Arzon funksiya yashirin qatorlarni ko'radi. WITH (security_barrier) yoki RLS.
5. Katta MV'ni oddiy REFRESH bilan yangilash. Yangilanish davomida o'qish to'xtaydi. CONCURRENTLY + unique indeks.
6. MV'ning qachon yangilanganini ko'rsatmaslik. Foydalanuvchi eski raqamni yangi deb o'ylaydi. now() AS yangilangan ustuni.
7. Har yozishda REFRESH. Qimmat va navbat. Fon ishi jadvali yoki counter cache.
7. Mashqlar
Mashqlar kurs/mashqlar/27/04-view/ da. Holat 04 bazasida, hammasi BEGIN … ROLLBACK ichida. Yechimlar tekshirilgan (biz ishga tushirdik, PostgreSQL 17.6).
1-mashq (oson)
faol_vazifalar ustiga ochiq_vazifalar view'ini yarating: faqat bajarilmagan vazifalar, ustunlari id, egasi_id, matn, ustuvorlik. Keyin har foydalanuvchida nechta yuqori ustuvorlikdagi ochiq vazifa borligini chiqaring (egasi_id bo'yicha tartiblangan). Ishora: view boshqa view ustiga qurilishi mumkin; count(*) + GROUP BY.
Yechim
BEGIN;
CREATE VIEW ochiq_vazifalar AS
SELECT id, egasi_id, matn, ustuvorlik
FROM faol_vazifalar WHERE NOT bajarildi;
SELECT egasi_id, count(*) AS yuqori
FROM ochiq_vazifalar WHERE ustuvorlik = 'yuqori'
GROUP BY egasi_id ORDER BY egasi_id;
ROLLBACK;BEGIN
CREATE VIEW
egasi_id | yuqori
----------+--------
1 | 2
2 | 1
3 | 2
4 | 1
5 | 1
6 | 1
7 | 1
(7 rows)
ROLLBACKView ichida view — ikkala shart (ochirilgan_vaqt IS NULL va NOT bajarildi) planner tomonidan bitta so'rovga ochiladi. Yuqori ustuvorlikdagi ochiq vazifasi yo'q foydalanuvchilar natijada yo'q — ular kerak bo'lsa foydalanuvchilar bilan LEFT JOIN.
2-mashq (o'rta)
ochiq_vazifalar ni yangi ustunlar bilan yarating: egasi_id, matn, ustuvorlik, bajarildi, bajarilgan_vaqt, ochirilgan_vaqt, WITH CASCADED CHECK OPTION bilan. View orqali uch amal. (a) Azizning "JavaScript…" bilan boshlanadigan vazifasini bajarilgan deb belgilashga urinib ko'ring. (b) Shahnozaga (egasi_id = 8) yangi vazifa qo'shing va matn, bajarildi ni qaytaring. (c) Darhol Savatga tushgan vazifa qo'shishga urinib ko'ring — faol_vazifalar dagi shart ham tekshiriladimi? Ishora: «WITH CHECK OPTION» bo'limi; view'da yo'q ustunni INSERT da berib bo'lmaydi; xatolardan keyin — SAVEPOINT.
Yechim
(a) va (b):
\set VERBOSITY terse
BEGIN;
CREATE VIEW ochiq_vazifalar AS
SELECT egasi_id, matn, ustuvorlik,
bajarildi, bajarilgan_vaqt, ochirilgan_vaqt
FROM faol_vazifalar WHERE NOT bajarildi
WITH CASCADED CHECK OPTION;
SAVEPOINT s;
UPDATE ochiq_vazifalar
SET bajarildi = true, bajarilgan_vaqt = '2026-10-24 10:00+05'
WHERE egasi_id = 1 AND matn LIKE 'JavaScript%';
ROLLBACK TO SAVEPOINT s;
INSERT INTO ochiq_vazifalar (egasi_id, matn)
VALUES (8, 'Kutubxonaga a''zo bo''lish #o''qish')
RETURNING matn, bajarildi;
ROLLBACK;BEGIN
CREATE VIEW
SAVEPOINT
ERROR: new row violates check option for view "ochiq_vazifalar"
ROLLBACK
matn | bajarildi
----------------------------------+-----------
Kutubxonaga a'zo bo'lish #o'qish | f
(1 row)
INSERT 0 1
ROLLBACK(a) Bajarilgan vazifa endi "ochiq" emas — view shartidan chiqadi, rad. Bu view orqali faqat ochiq vazifalarni tahrirlash mumkin.
Yechim boshidagi \set VERBOSITY terse — psql sozlamasi. U xatoning faqat birinchi qatorini chiqaradi. DETAIL da trigger qo'ygan joriy vaqt bor edi va u har safar boshqacha — mashq tekshiruvi uchun uni yashirdik.
(b) Yangi vazifa bajarildi = false (jadval sukuti) — shartga mos, o'tdi.
(c) — xuddi shu tranzaksiyada (view yaratilgandan keyin):
INSERT INTO ochiq_vazifalar (egasi_id, matn, ochirilgan_vaqt)
VALUES (8, 'Darhol Savatga', '2026-10-24 10:00+05');ERROR: new row violates check option for view "faol_vazifalar"
DETAIL: Failing row contains (51, 8, Darhol Savatga, f, 2026-10-26 08:39:35.173+00, orta, null, null, null, 0, 2026-10-26 08:39:35.173+00, 2026-10-24 05:00:00+00, 1, 2).CASCADED tufayli pastdagi faol_vazifalar sharti ham tekshirildi — Savatdagi vazifa rad etildi, garchi faol_vazifalar ning o'zida CHECK OPTION yo'q bo'lsa ham (xato aynan uning nomini aytadi). LOCAL bilan bu INSERT o'tardi. Bu yerdagi DETAIL — vazifalar ning to'liq qatori: id (51) va trigger qo'ygan vaqtlar sizda boshqacha bo'ladi (id ketma-ketlikdan olinadi va ROLLBACK dan keyin ham orqaga qaytmaydi).
3-mashq (qiyin)
Teglar statistikasi uchun MV: teg_statistikasi — har teg (teglar.id, egasi_id, nom) uchun faol vazifalar soni (vazifalar_soni). CONCURRENTLY yangilanadigan bo'lsin. Azizning (egasi_id = 1) eng ko'p ishlatiladigan 3 tegini chiqaring; keyin Azizga 'Kitob o''qish #o''qish' vazifasini qo'shing, #o'qish uchun MV va haqiqiy sonni solishtiring, MV'ni o'qishni to'smasdan yangilang va yana ko'rsating. Ishora: «Kanon: foydalanuvchi statistikasi» — LEFT JOIN va unique indeks naqshi; ko'prik jadval — vazifa_teglari; teg nomi bir xil vazifalar sonini tenglikda — ORDER BY vazifalar_soni DESC, nom.
Yechim
BEGIN;
CREATE MATERIALIZED VIEW teg_statistikasi AS
SELECT t.id AS teg_id, t.egasi_id, t.nom,
count(v.id) AS vazifalar_soni
FROM teglar t
LEFT JOIN vazifa_teglari vt ON vt.teg_id = t.id
LEFT JOIN vazifalar v
ON v.id = vt.vazifa_id AND v.ochirilgan_vaqt IS NULL
GROUP BY t.id;
CREATE UNIQUE INDEX teg_statistikasi_pkey
ON teg_statistikasi (teg_id);
SELECT nom, vazifalar_soni FROM teg_statistikasi
WHERE egasi_id = 1
ORDER BY vazifalar_soni DESC, nom LIMIT 3;
INSERT INTO vazifalar (egasi_id, matn)
VALUES (1, 'Kitob o''qish #o''qish');
SELECT s.vazifalar_soni AS mv_dagi,
(SELECT count(*) FROM vazifa_teglari vt
JOIN vazifalar v ON v.id = vt.vazifa_id
WHERE vt.teg_id = s.teg_id
AND v.ochirilgan_vaqt IS NULL) AS haqiqiy
FROM teg_statistikasi s
WHERE s.egasi_id = 1 AND s.nom = '#o''qish';
REFRESH MATERIALIZED VIEW CONCURRENTLY teg_statistikasi;
SELECT vazifalar_soni FROM teg_statistikasi
WHERE egasi_id = 1 AND nom = '#o''qish';
ROLLBACK;BEGIN
SELECT 32
CREATE INDEX
nom | vazifalar_soni
---------+----------------
#o'qish | 3
#ta'mir | 2
#bozor | 1
(3 rows)
INSERT 0 1
mv_dagi | haqiqiy
---------+---------
3 | 4
(1 row)
REFRESH MATERIALIZED VIEW
vazifalar_soni
----------------
4
(1 row)
ROLLBACKGROUP BY t.id — teglar.id birlamchi kalit, shuning uchun egasi_id va nom ni GROUP BY ga yozmasa ham bo'ladi (ular id'ga bog'liq — funksional bog'liqlik). Ikkinchi LEFT JOIN dagi shart ON ichida, WHERE da emas — aks holda Savatdagi vazifali teglar natijadan yo'qolardi. Yangi vazifa teglar trigger'i orqali vazifa_teglari ga darhol tushdi (haqiqiy son oshdi), MV esa REFRESH gacha eski raqamni ko'rsatdi.
8. Loyiha qadami
Holat 01 → holat 04. Bitta fayl: sql/103-statistika-mv.sql — foydalanuvchi_statistikasi MV va unique indeks (to'liq matni «Kanon: foydalanuvchi statistikasi» bo'limida). Fayl oxiridagi izoh (kanon):
COMMENT ON MATERIALIZED VIEW foydalanuvchi_statistikasi IS
'Statistika sahifasi; REFRESH MATERIALIZED VIEW CONCURRENTLY '
'bilan yangilanadi (fon ishi), yangilangan — '
'oxirgi yangilanish vaqti';(Kanonda matn bitta qatorda; bu yerda Postgres'ning qo'shni satr literallarini birlashtirish qoidasi bilan bo'ldik — ular orasida yangi qator bo'lsa, bitta satr bo'ladi.)
Qo'llash:
psql -X -1 -v ON_ERROR_STOP=1 \
-d postgres://postgres@127.0.0.1:5499/vazifalar \
-f sql/103-statistika-mv.sqlTekshiruv: «O'qish» bo'limidagi so'rov 8 qator bersin, Shahnozada jami = 0. Commit:
git add sql/103-statistika-mv.sql
M='db: foydalanuvchi statistikasi — materialized view,'
M+=' CONCURRENTLY uchun unique indeks (103)'
git commit -m "$M"REFRESH ni hozircha hech kim chaqirmaydi — MV holat 04 qurilgan paytdagi raqamlarni ko'rsatadi. Rejalashtirilgan yangilanish fon vazifalari bilan (29-qism) yoki pg_cron bilan keladi; API statistikani o'qishi — ORM darslaridan keyin.
9. Real ishda
- Admin dashboard va hisobotlar — kunlik/soatlik MV'lar: "bugungi buyurtmalar", "eng faol foydalanuvchilar"; raqam yonida "yangilangan: 10:05".
- Reyting jadvallari — o'yin yoki kurs reytingi har daqiqada
REFRESH … CONCURRENTLY. - Eski sxema ustidan moslik qatlami — jadval nomi o'zgarganda eski nomli view vaqtincha qoldiriladi, eski kod buzilmaydi (Zero-downtime: expand-contract).
- ORM'lar — Prisma 7 view'ni
viewbloki bilan o'qiy oladi (viewspreview imkoniyati — hujjatga ko'ra, 2026-10 holatiga). Materialized view'ni Prisma qo'llamaydi: uni xom SQL migratsiyada yaratasiz,REFRESHham — xom so'rov. - Analitika bazalari — ClickHouse, BigQuery'da MV boshqacha: yangi ma'lumot kelganda o'zi yangilanadi. Postgres'da yo'q; "avtomatik yangilanadigan MV" kerak bo'lsa —
pg_ivmkabi kengaytmalar (sinalmagan). - Intervyuda: "View va materialized view farqi?", "
REFRESH CONCURRENTLYqanday ishlaydi va nima talab qiladi?", "View xavfsizlik uchun yetarlimi?" —security_barrierva RLS'ni tilga oling.
Xulosa
- View — saqlangan so'rov, inline qilinadi: tezlik bermaydi, qulaylik va ruxsat vositasi;
SELECT *yaratilgan paytdagi ustunlarga muzlaydi. - Bitta jadvalli oddiy view avtomatik yangilanadi;
WITH CASCADED CHECK OPTIONqatorning view'dan chiqib ketishini taqiqlaydi. - Ruxsat uchun view —
security_barrier: busiz arzon funksiya 48 ta begona matnni ko'rdi, bilan — faqat 13 ta o'zinikini. - MV — diskdagi natija, o'zi yangilanmaydi;
yangilanganustuni eskirishni halol ko'rsatadi. - Oddiy
REFRESH—AccessExclusiveLock(o'qish ham kutadi);CONCURRENTLY—ExclusiveLock, unique indeks va to'ldirilgan MV talab qiladi. - Yangilash — fon ishi jadvalida, har yozishda emas.
Keyingi dars: Funksiyalar va PL/pgSQL — mantiqni bazaning ichida yozish: LANGUAGE sql va plpgsql, IMMUTABLE/STABLE/VOLATILE, xatolarni ushlash va SECURITY DEFINER xavfi.
Manbalar
- PostgreSQL 18 hujjati — CREATE VIEW (updatable view shartlari,
CHECK OPTION,security_barrier): https://www.postgresql.org/docs/18/sql-createview.html - PostgreSQL 18 hujjati — Rules and Privileges (view orqali sizish va
security_barrier): https://www.postgresql.org/docs/18/rules-privileges.html - PostgreSQL 18 hujjati — CREATE MATERIALIZED VIEW, REFRESH MATERIALIZED VIEW (
CONCURRENTLY, qulflar): https://www.postgresql.org/docs/18/sql-refreshmaterializedview.html - PostgreSQL 18 hujjati — Explicit Locking (qulf mosligi jadvali): https://www.postgresql.org/docs/18/explicit-locking.html
- Prisma ORM 7 hujjati — Views (preview, materialized view qo'llanmaydi): https://www.prisma.io/docs/orm/v7/prisma-schema/data-model/views
- IlmHamroh kanon tajribasi (PostgreSQL 18.6, Docker, 2026-10-24); darsdagi so'rovlar — biz ishga tushirdik (PostgreSQL 17.6, Windows 11), 2026-10-26.
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!