IlmHamroh
JavaScript Full-stack/27-qism. Ma'lumotlar bazasi chuqur: Postgres, ORM, Mongo, Redis4/65-dars23 daqiqa
Mundarija (32)

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 REFRESH bilan yangilanadi. REFRESH … CONCURRENTLY o'qishni to'smaydi, faqat unique indeks bo'lsa ishlaydi. Xavfsizlik uchun ishlatiladigan view security_barrier siz yashirin qatorlarni sizdirishi mumkin.

Bu darsda

  • View EXPLAIN'da "erib ketishini" (inlining) va SELECT * view'ning ustunlarini muzlatishini ko'rasiz.
  • View orqali UPDATE qilasiz va WITH CHECK OPTION nima uchun kerakligini tushuntirasiz.
  • security_barrier siz view qanday sizishini o'z tajribangizda ko'rasiz.
  • Materialized view yaratasiz, REFRESH va REFRESH … CONCURRENTLY ning qulflarini solishtirasiz.
  • vazifalar ga 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 --> D

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

sql
SELECT pg_get_viewdef('faol_vazifalar', true);
text
          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):

sql
EXPLAIN (COSTS OFF)
SELECT id FROM faol_vazifalar WHERE egasi_id = 1;
text
                        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:

sql
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;
text
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.
ROLLBACK
  1. rang qo'shildi, lekin view'da yo'q — view id, egasi_id, nom bo'lib qolgan.
  2. nom ni o'chirish rad etildi: "boshqa obyektlar unga bog'liq". HINT CASCADE taklif 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.

sql
BEGIN;
UPDATE faol_vazifalar
SET bajarildi = true, bajarilgan_vaqt = '2026-10-24 10:00+05'
WHERE id = 1
RETURNING id, bajarildi, versiya;
ROLLBACK;
text
BEGIN
 id | bajarildi | versiya
----+-----------+---------
  1 | t         |       2
(1 row)

UPDATE 1
ROLLBACK

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

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

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

sql
BEGIN;
CREATE VIEW t_soni AS
  SELECT egasi_id, count(*) n FROM vazifalar GROUP BY egasi_id;
UPDATE t_soni SET n = 0;
ROLLBACK;
text
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.
ROLLBACK

Xabar 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_vazifalar orqali INSERT qilinsa, ochirilgan_vaqt qanday 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:

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

ROLLBACK

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

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

ROLLBACK

Oddiy 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. Funksiyani LEAKPROOF deb 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):

sql
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 foiz NULL, 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 — CONCURRENTLY uchun shart (pastda).

5.2 O'qish

sql
\pset null '(null)'
SELECT foydalanuvchi_id, jami, bajarilgan, bajarilgan_foiz
FROM foydalanuvchi_statistikasi ORDER BY 1;
text
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

sql
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;
text
BEGIN
INSERT 0 1
 mv_dagi | haqiqiy
---------+---------
       0 |       1
(1 row)

REFRESH MATERIALIZED VIEW
 refreshdan_keyin
------------------
                1
(1 row)

ROLLBACK

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

sql
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;
text
BEGIN
REFRESH MATERIALIZED VIEW
        mode
---------------------
 AccessExclusiveLock
 ExclusiveLock
 ShareLock
(3 rows)

ROLLBACK
BEGIN
REFRESH MATERIALIZED VIEW
       mode
------------------
 AccessShareLock
 ExclusiveLock
 RowExclusiveLock
(3 rows)

ROLLBACK
Oddiy 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

sql
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;
text
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
  1. Unique indekssiz CONCURRENTLY — rad. HINT aniq: "MV'ning bir yoki bir nechta ustunida WHERE siz unique indeks yarating". Indeks orqali Postgres qaysi qator qaysi ekanini taniydi va farqni topadi.
  2. WITH NO DATA — MV yaratiladi, lekin bo'sh va "to'ldirilmagan". O'qish — xato, HINT REFRESH ni taklif qiladi. Bu katta MV'ni migratsiyada tez yaratib, keyin fonda to'ldirish uchun. Lekin to'ldirilmagan MV'ga CONCURRENTLY ishlamaydi — birinchi REFRESH oddiy bo'ladi.

5.6 MV — jadval kabi

sql
SELECT relname, relkind,
       pg_size_pretty(pg_total_relation_size(oid)) AS hajm
FROM pg_class
WHERE relname LIKE 'foydalanuvchi_statistikasi%' ORDER BY 1;
text
             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_cron kengaytmasi (Extension'lar).
  • Har INSERT da trigger bilan REFRESH — har yozish butun statistikani qayta hisoblaydi va REFRESH lar 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
sql
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;
text
BEGIN
CREATE VIEW
 egasi_id | yuqori
----------+--------
        1 |      2
        2 |      1
        3 |      2
        4 |      1
        5 |      1
        6 |      1
        7 |      1
(7 rows)

ROLLBACK

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

sql
\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;
text
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):

sql
INSERT INTO ochiq_vazifalar (egasi_id, matn, ochirilgan_vaqt)
VALUES (8, 'Darhol Savatga', '2026-10-24 10:00+05');
text
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
sql
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;
text
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)

ROLLBACK

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

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

bash
psql -X -1 -v ON_ERROR_STOP=1 \
  -d postgres://postgres@127.0.0.1:5499/vazifalar \
  -f sql/103-statistika-mv.sql

Tekshiruv: «O'qish» bo'limidagi so'rov 8 qator bersin, Shahnozada jami = 0. Commit:

bash
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 view bloki bilan o'qiy oladi (views preview imkoniyati — hujjatga ko'ra, 2026-10 holatiga). Materialized view'ni Prisma qo'llamaydi: uni xom SQL migratsiyada yaratasiz, REFRESH ham — 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_ivm kabi kengaytmalar (sinalmagan).
  • Intervyuda: "View va materialized view farqi?", "REFRESH CONCURRENTLY qanday ishlaydi va nima talab qiladi?", "View xavfsizlik uchun yetarlimi?" — security_barrier va 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 OPTION qatorning 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; yangilangan ustuni 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

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
PostgreSQL view va materialized view: updatable view, securitybarrier, REFRESH CONCURRENTLY — IlmHamroh