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

PostgreSQL triggerlari: BEFORE/AFTER, ROW/STATEMENT, audit va yashirin narx

Qisqacha: Trigger — jadvalga INSERT, UPDATE yoki DELETE bo'lganda bazaning o'zi chaqiradigan funksiya. BEFORE ROW trigger qatorni yozishdan oldin o'zgartira (NEW) yoki jim tashlab yubora oladi; AFTER trigger tayyor natijaga qarab boshqa jadvalga yozadi (audit, hisoblagich); STATEMENT trigger bitta buyruqqa bir marta ishlaydi. Trigger'lar bepul emas: kanonda 5 000 qatorli INSERT trigger'lar bilan o'n barobardan ko'proq sekin bo'lgan. Va ular ko'rinmaydi — kodni o'qigan odam ularni bilmasligi mumkin.

Bu darsda

  • Trigger'ning to'rt turini (BEFORE/AFTER × ROW/STATEMENT) va qaysi biri nima uchunligini ajratasiz.
  • NEW, OLD, TG_OP va boshqa TG_ o'zgaruvchilari bilan trigger funksiyasi yozasiz.
  • UPDATE OF va WHEN shartlarining farqini bilasiz, updated_at va audit trigger'larini o'qiysiz.
  • Transition table bilan statement-level trigger yozasiz.
  • Trigger'larning narxini o'lchaysiz va ommaviy yuklashda ular bilan qanday ishlashni bilasiz.

Oldin bilishingiz kerak: Funksiyalar va PL/pgSQL, Denormalizatsiya va hisoblangan maydonlar, Soft delete, timestamps va tarix jadvallari.

1. Nega bu kerak?

26-qismda vazifalar bazasiga beshta trigger qo'shdik: izohlar soni, teglar, yangilangan_vaqt, tarix va tartib. Ular ishladi, lekin biz ularni "qora quti" sifatida ishlatdik. Endi ochib ko'ramiz: ular qachon, qaysi tartibda chaqiriladi, nimani o'zgartira oladi va qancha turadi.

Oldingi darsda funksiya yozishni o'rgandik. Trigger — o'sha funksiyani jadval hodisasiga ulash. O'xshatish: do'kon eshigidagi sensor. Kim kirsa — xaridormi, kuryermi — sensor o'zi qo'ng'iroq chaladi va hisoblagichga +1 qo'shadi. Sotuvchi har mijozda "hisoblagichni bos" deb eslashi shart emas. Lekin sensor juda ko'p bo'lsa va har biri sekin bo'lsa — eshik oldida navbat yig'iladi.

Bu darsdagi so'rovlarni biz ishga tushirdik: PostgreSQL 17.6, kanon holat 04 sxemasi. Kanon (IlmHamroh, 2026-10, PostgreSQL 18.6) bilan chiqish bir xil; faqat vaqt o'lchovlari farq qiladi — ular alohida belgilangan. Hamma o'zgarish ROLLBACK bilan qaytariladi.

2. Trigger anatomiyasi

2.1 Ikki savol: qachon va necha marta

Har trigger ikki o'q bo'yicha tanlanadi:

ROW (har qator uchun) STATEMENT (buyruqqa bir marta)
BEFORE NEW ni o'zgartirish, qatorni tashlash buyruqdan oldin tekshiruv
AFTER boshqa jadvalga yozish (audit, hisoblagich) jamlangan ish (transition table bilan)

Uchinchi vaqt — INSTEAD OF — faqat view uchun: "view'ga yozilganda o'rniga shuni qil". View va materialized view darsida avtomatik yangilanmaydigan view'ni ko'rgandik — INSTEAD OF trigger uni yoziladigan qila oladi.

Kanondagi trigger'lar ro'yxati pg_trigger katalogidan. tgtype — bitlar to'plami: 1-bit — ROW, 2-bit — BEFORE:

sql
SELECT tgname,
       CASE WHEN tgtype & 2 = 2 THEN 'BEFORE' ELSE 'AFTER' END
         AS qachon,
       CASE WHEN tgtype & 1 = 1 THEN 'ROW' ELSE 'STATEMENT' END
         AS daraja,
       tgfoid::regproc AS funksiya
FROM pg_trigger WHERE NOT tgisinternal
ORDER BY tgrelid::regclass::text, tgname;
text
          tgname           | qachon | daraja |         funksiya
---------------------------+--------+--------+--------------------------
 izohlar_soni_trg          | AFTER  | ROW    | izohlar_sonini_yangila
 vazifa_tarixi_trg         | AFTER  | ROW    | vazifa_tarixini_yoz
 vazifa_teglari_trg        | AFTER  | ROW    | vazifa_teglarini_yangila
 vazifalar_tartib_trg      | BEFORE | ROW    | tartibni_qoy
 vazifalar_yangilangan_trg | BEFORE | ROW    | yangilangan_vaqtni_qoy
(5 rows)

NOT tgisinternal — Postgres o'zi yaratgan ichki trigger'larni yashiradi. Ha, tashqi kalitlar (foreign key) ham ichkarida trigger bilan ishlaydi — buni narx bo'limida ko'rasiz.

2.2 Bitta INSERT yo'li

Bitta INSERT INTO vazifalar qatori qanday yo'ldan o'tadi? Diagrammada kanondagi trigger'lar joyiga qarang:

flowchart TD
  A["INSERT buyrug'i"] --> B["BEFORE STATEMENT"]
  B --> C["BEFORE ROW<br/>tartibni_qoy: NEW.tartib"]
  C --> D["Cheklovlar: NOT NULL,<br/>CHECK, UNIQUE"]
  D --> E["Qator yoziladi"]
  E --> F["AFTER ROW: teglar,<br/>tarix, FK tekshiruvi"]
  F --> G["AFTER STATEMENT"]
  G --> H["Tranzaksiya: COMMIT"]

Muhim tartib: BEFORE ROW trigger NOT NULL va CHECK dan oldin ishlaydi. Shuning uchun tartibni_qoy bo'sh tartib ni to'ldirib ulguradi va NOT NULL buzilmaydi. AFTER trigger'lar va asosiy buyruq — bitta tranzaksiyada: trigger xato bersa, INSERT ham bekor bo'ladi.

2.3 Trigger funksiyasining maxsus o'zgaruvchilari

Trigger funksiyasi RETURNS trigger deb e'lon qilinadi va argument olmaydi. Ma'lumotni maxsus o'zgaruvchilardan oladi:

O'zgaruvchi Ma'nosi
NEW / OLD yangi / eski qator (ROW trigger'da)
TG_OP 'INSERT', 'UPDATE', 'DELETE', 'TRUNCATE'
TG_WHEN, TG_LEVEL BEFORE/AFTER, ROW/STATEMENT
TG_NAME, TG_TABLE_NAME trigger va jadval nomi

Bitta funksiyani ikki trigger'ga ulab, ularni ko'ramiz:

sql
BEGIN;
CREATE TABLE t_m (id int, nom text);
CREATE FUNCTION t_kim() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
  RAISE NOTICE '% % % % %', TG_NAME, TG_WHEN, TG_LEVEL, TG_OP,
    TG_TABLE_NAME;
  RETURN NULL;
END $$;
CREATE TRIGGER t_m_row AFTER INSERT ON t_m
  FOR EACH ROW EXECUTE FUNCTION t_kim();
CREATE TRIGGER t_m_stmt AFTER INSERT ON t_m
  FOR EACH STATEMENT EXECUTE FUNCTION t_kim();
INSERT INTO t_m VALUES (1, 'osh'), (2, 'manti');
ROLLBACK;
text
BEGIN
CREATE TABLE
CREATE FUNCTION
CREATE TRIGGER
CREATE TRIGGER
NOTICE:  t_m_row AFTER ROW INSERT t_m
NOTICE:  t_m_row AFTER ROW INSERT t_m
NOTICE:  t_m_stmt AFTER STATEMENT INSERT t_m
INSERT 0 2
ROLLBACK

Ikki qator — ROW trigger ikki marta, STATEMENT trigger bir marta. Bitta funksiya bir nechta trigger'ga xizmat qilishi mumkin: TG_OP va TG_TABLE_NAME ga qarab o'zini boshqacha tutadi (kanondagi vazifa_tarixini_yoz aynan shunday).

Tekshirib ko'ring: INSERT INTO t_m SELECT g, 'x' FROM generate_series(1, 1000) g; buyrug'ida t_kim necha marta chaqiriladi?

Javob

1 001 marta: ROW trigger har qator uchun (1 000) va STATEMENT trigger bir marta. Ommaviy yuklashda ROW trigger'lar asosiy narxni beradi — buni narx bo'limida o'lchaymiz.

3. BEFORE ROW: qatorni yozishdan oldin

3.1 RETURN NEW va RETURN NULL

BEFORE ROW trigger'ning qaytargan qiymati — yoziladigan qator:

  • RETURN NEW — qator yoziladi (trigger o'zgartirgan bo'lsa — o'zgargan holda);
  • RETURN NULL — qator jim tashlab yuboriladi, xato yo'q.

Bitta hodisada bir nechta BEFORE trigger bo'lsa, ular nom bo'yicha alifbo tartibida ishlaydi va har biri oldingisining NEW ini oladi. Kanon tajribasi: avval b_ikkinchi, keyin a_birinchi yaratiladi, keyin manfiy id ni tashlaydigan c_tashla qo'shiladi:

sql
BEGIN;
CREATE TABLE t_tr (id int, izoh text[] DEFAULT '{}');
CREATE FUNCTION t_yoz() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN NEW.izoh := NEW.izoh || TG_NAME::text; RETURN NEW; END $$;
CREATE TRIGGER b_ikkinchi BEFORE INSERT ON t_tr
  FOR EACH ROW EXECUTE FUNCTION t_yoz();
CREATE TRIGGER a_birinchi BEFORE INSERT ON t_tr
  FOR EACH ROW EXECUTE FUNCTION t_yoz();
INSERT INTO t_tr (id) VALUES (1) RETURNING izoh;
CREATE FUNCTION t_tashla() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
  IF NEW.id < 0 THEN RETURN NULL; END IF;
  RETURN NEW;
END $$;
CREATE TRIGGER c_tashla BEFORE INSERT ON t_tr
  FOR EACH ROW EXECUTE FUNCTION t_tashla();
INSERT INTO t_tr (id) VALUES (-5);
SELECT count(*) FROM t_tr;
ROLLBACK;
text
BEGIN
CREATE TABLE
CREATE FUNCTION
CREATE TRIGGER
CREATE TRIGGER
          izoh
-------------------------
 {a_birinchi,b_ikkinchi}
(1 row)

INSERT 0 1
CREATE FUNCTION
CREATE TRIGGER
INSERT 0 0
 count
-------
     1
(1 row)

ROLLBACK

Ikki narsaga qarang:

  1. {a_birinchi,b_ikkinchi} — b_ikkinchi oldin yaratilgan, lekin a_birinchi oldin ishladi. Tartib — yaratilish vaqti emas, nom. Shuning uchun ba'zi jamoalar trigger nomiga raqam qo'yadi: 10_tartib, 20_vaqt.
  2. INSERT 0 0 — xato yo'q, lekin qator ham yo'q (jadvalda faqat birinchi qator — count 1). Ilova "muvaffaqiyatli" deb o'ylaydi. Bu BEFORE trigger'ning eng xavfli kuchi: ma'lumot jim yo'qoladi. Rad etish kerak bo'lsa — RAISE EXCEPTION, RETURN NULL emas.

3.2 updated_at trigger'i va UPDATE OF tuzog'i

Kanondagi yangilangan_vaqtni_qoy (26-qismdagi 013 migratsiyasi):

sql
CREATE OR REPLACE FUNCTION yangilangan_vaqtni_qoy() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
  NEW.yangilangan_vaqt := date_trunc('milliseconds', now());
  NEW.versiya := OLD.versiya + 1;
  RETURN NEW;
END $$;
CREATE TRIGGER vazifalar_yangilangan_trg
BEFORE UPDATE OF matn, bajarildi, ustuvorlik, muddat, ota_id,
  ochirilgan_vaqt, tartib ON vazifalar
FOR EACH ROW EXECUTE FUNCTION yangilangan_vaqtni_qoy();

UPDATE OF matn, bajarildi, … — trigger faqat shu ustunlar SET ro'yxatida bo'lsa ishlaydi. izohlar_soni o'zgarganda (izoh qo'shilganda) versiya oshmaydi — to'g'ri, bu tahrir emas.

Lekin kanon bitta tuzoqni topdi. UPDATE OF qiymat o'zgarishini emas, ustun tilga olinganini tekshiradi:

sql
BEGIN;
UPDATE vazifalar SET bajarildi = bajarildi WHERE id = 1
RETURNING versiya AS versiya_oshdi;
ROLLBACK;
text
BEGIN
 versiya_oshdi
---------------
             2
(1 row)

UPDATE 1
ROLLBACK

Hech narsa o'zgarmadi, versiya esa 1 dan 2 ga oshdi. API "bajarildi = false" ni ikki marta yuborsa — ikki marta yangi versiya, ikki marta yangi ETag, mijoz keshi bekor bo'ladi.

3.3 WHEN sharti

Yechim — trigger e'lonidagi WHEN sharti. U funksiyani chaqirishdan oldin tekshiriladi: shart yolg'on bo'lsa, funksiya umuman chaqirilmaydi (arzon). Kichik jadvalda ko'ramiz:

sql
BEGIN;
CREATE TABLE t_taom (id int PRIMARY KEY, narx int,
  versiya int NOT NULL DEFAULT 1);
CREATE FUNCTION t_versiya() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
  NEW.versiya := OLD.versiya + 1;
  RETURN NEW;
END $$;
CREATE TRIGGER t_taom_versiya BEFORE UPDATE ON t_taom
  FOR EACH ROW
  WHEN (OLD.narx IS DISTINCT FROM NEW.narx)
  EXECUTE FUNCTION t_versiya();
INSERT INTO t_taom VALUES (1, 35000);
UPDATE t_taom SET narx = 35000;
UPDATE t_taom SET narx = 36000;
SELECT * FROM t_taom;
ROLLBACK;
text
BEGIN
CREATE TABLE
CREATE FUNCTION
CREATE TRIGGER
INSERT 0 1
UPDATE 1
UPDATE 1
 id | narx  | versiya
----+-------+---------
  1 | 36000 |       2
(1 row)

ROLLBACK

Ikki UPDATE, lekin versiya bir marta oshdi — 35 000 → 35 000 o'zgarish emas. IS DISTINCT FROM — NULL'ni ham to'g'ri solishtiradigan "teng emas" (NULL mantiqi): NULL <> NULL — NULL, NULL IS DISTINCT FROM NULL — false.

Butun qator uchun: WHEN (OLD.* IS DISTINCT FROM NEW.*) — UPDATE OF tuzog'ining tavsiya etilgan yechimi. Kanon trigger'i hozircha UPDATE OF bilan qolgan (26-qismdagi 013 qo'llangan, o'zgartirilmaydi). O'z loyihangizda bu shartni yangi migratsiya fayli bilan qo'shasiz: trigger'ni DROP TRIGGER va WHEN bilan qayta CREATE TRIGGER.

Tekshirib ko'ring: WHEN ichida nega OLD INSERT trigger'ida ishlatib bo'lmaydi?

Javob

INSERT'da eski qator yo'q — OLD mavjud emas. Postgres buni trigger yaratishdayoq rad etadi (INSERT trigger's WHEN condition cannot reference OLD values). Xuddi shunday DELETE'da NEW yo'q. Shuning uchun WHEN (OLD … NEW) faqat BEFORE/AFTER UPDATE uchun.

4. AFTER ROW: audit va bog'liq jadvallar

4.1 Audit trigger'i

AFTER trigger qator yozilgandan keyin ishlaydi — NEW endi o'zgarmaydi, qaytgan qiymat e'tiborsiz (RETURN NULL). U boshqa jadvalga yozish uchun: hisoblagich, teglar, tarix. Kanondagi tarix funksiyasi (26-qism, 010):

sql
CREATE FUNCTION vazifa_tarixini_yoz() RETURNS trigger
LANGUAGE plpgsql AS $$
DECLARE
  kim bigint := nullif(
    current_setting('ilova.foydalanuvchi_id', true), '')::bigint;
BEGIN
  INSERT INTO vazifa_tarixi (vazifa_id, amal, eski, yangi, kim_id)
  VALUES (
    coalesce(NEW.id, OLD.id), TG_OP,
    CASE WHEN TG_OP <> 'INSERT'
      THEN to_jsonb(OLD) - 'izohlar_soni' END,
    CASE WHEN TG_OP <> 'DELETE'
      THEN to_jsonb(NEW) - 'izohlar_soni' END,
    kim);
  RETURN NULL;
END $$;

Bitta funksiya uch hodisaga: coalesce(NEW.id, OLD.id) — DELETE'da NEW NULL, INSERT'da OLD NULL. to_jsonb(OLD) — butun qatorni JSONB'ga aylantiradi (JSONB darsida o'rgangan - operatori bilan keraksiz kalitni olib tashlaydi). "Kim" — ilova tranzaksiyada qo'yadigan sozlamadan (Soft delete, timestamps va tarix jadvallari darsidagi SET LOCAL).

Amalda:

sql
BEGIN;
SET LOCAL ilova.foydalanuvchi_id = '1';
UPDATE vazifalar SET matn = 'Qoralama yozish va tekshirish'
WHERE id = 44;
SELECT amal, eski->>'matn' AS eski, yangi->>'matn' AS yangi,
       kim_id
FROM vazifa_tarixi WHERE vazifa_id = 44
ORDER BY id DESC LIMIT 1;
ROLLBACK;
text
BEGIN
SET
UPDATE 1
  amal  |      eski       |             yangi             | kim_id
--------+-----------------+-------------------------------+--------
 UPDATE | Qoralama yozish | Qoralama yozish va tekshirish |      1
(1 row)

ROLLBACK

Ilova kodida "tarixga yoz" degan qator yo'q — lekin tarix yozildi. Bu audit trigger'ining asosiy afzalligi: psql'dan qo'lda qilingan tahrir ham, unutilgan admin skript ham iz qoldiradi.

4.2 Trigger ichidan o'z jadvaliga yozish — rekursiya

AFTER UPDATE trigger o'sha jadvalni yana UPDATE qilsa nima bo'ladi? Har UPDATE trigger'ni qayta chaqiradi:

sql
CREATE TABLE t_r (id int, n int);
CREATE FUNCTION t_oshir() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
  UPDATE t_r SET n = n + 1 WHERE id = NEW.id;
  RETURN NULL;
END $$;
CREATE TRIGGER t_r_trg AFTER UPDATE ON t_r
  FOR EACH ROW EXECUTE FUNCTION t_oshir();
INSERT INTO t_r VALUES (1, 0);
UPDATE t_r SET n = 1;

Biz ishga tushirdik (PostgreSQL 17.6; CONTEXT juftligi yuzlab marta takrorlanadi, qisqartirdik):

text
ERROR:  stack depth limit exceeded
HINT:  Increase the configuration parameter "max_stack_depth" (currently 2048kB), after ensuring the platform's stack depth limit is adequate.
CONTEXT:  SQL statement "UPDATE t_r SET n = n + 1 WHERE id = NEW.id"
PL/pgSQL function t_oshir() line 3 at SQL statement
…

Tarjimasi: "stek chuqurligi chegarasidan oshib ketdi". HINT ning maslahatiga (max_stack_depth ni oshirish) amal qilmang — muammo cheksiz rekursiyada, stekda emas. Yechimlar: BEFORE trigger'da NEW ni o'zgartirish (qo'shimcha UPDATE'siz), WHEN sharti yoki pg_trigger_depth() — hozirgi trigger chuqurligi:

sql
BEGIN;
CREATE TABLE t_r (id int, n int);
CREATE FUNCTION t_oshir() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
  IF pg_trigger_depth() < 3 THEN
    UPDATE t_r SET n = n + 1 WHERE id = NEW.id;
  END IF;
  RETURN NULL;
END $$;
CREATE TRIGGER t_r_trg AFTER UPDATE ON t_r
  FOR EACH ROW EXECUTE FUNCTION t_oshir();
INSERT INTO t_r VALUES (1, 0);
UPDATE t_r SET n = 1;
SELECT n FROM t_r;
ROLLBACK;
text
BEGIN
CREATE TABLE
CREATE FUNCTION
CREATE TRIGGER
INSERT 0 1
UPDATE 1
 n
---
 3
(1 row)

ROLLBACK

n = 1 (bizning UPDATE) → 1-chuqurlikda +1 → 2-chuqurlikda +1 → 3-chuqurlikda to'xtadi. pg_trigger_depth() — favqulodda tormoz; yaxshi dizaynda trigger o'z jadvaliga qayta yozmaydi.

Tekshirib ko'ring: izohlar_sonini_yangila trigger'i izohlar ga emas, vazifalar ga yozadi. vazifalar da esa vazifa_tarixi_trg bor. Izoh qo'shilganda tarixga yozuv tushadimi?

Javob

Yo'q. vazifa_tarixi_trg INSERT va DELETE'dan tashqari faqat UPDATE OF matn, bajarildi, ustuvorlik, muddat, ota_id, ochirilgan_vaqt da ishlaydi. Izoh trigger'i SET izohlar_soni = … qiladi — bu ustun ro'yxatda yo'q. UPDATE OF bu yerda foydali: hisoblagich o'zgarishi tarixni to'ldirmaydi (yana to_jsonb(…) - 'izohlar_soni' ham shuning uchun).

5. STATEMENT trigger va transition table

ROW trigger 13 qator uchun 13 marta chaqiriladi. Ba'zan bitta jamlangan ish kerak: "bu buyruq nechta qatorni o'zgartirdi?". Transition table (o'tish jadvali) — statement trigger ichida buyruq ta'sir qilgan barcha qatorlar to'plami (REFERENCING NEW TABLE AS … / OLD TABLE AS …). Kanon tajribasi:

sql
BEGIN;
CREATE TABLE t_hisob (amal text, soni int,
  vaqt timestamptz DEFAULT now());
CREATE FUNCTION t_stmt() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
  INSERT INTO t_hisob (amal, soni)
  SELECT TG_OP, count(*) FROM yangi;
  RETURN NULL;
END $$;
CREATE TRIGGER t_stmt_trg AFTER UPDATE ON vazifalar
  REFERENCING NEW TABLE AS yangi
  FOR EACH STATEMENT EXECUTE FUNCTION t_stmt();
UPDATE vazifalar SET ustuvorlik = 'yuqori' WHERE egasi_id = 1;
UPDATE vazifalar SET ustuvorlik = 'past' WHERE false;
SELECT amal, soni FROM t_hisob;
ROLLBACK;
text
BEGIN
CREATE TABLE
CREATE FUNCTION
CREATE TRIGGER
UPDATE 13
UPDATE 0
  amal  | soni
--------+------
 UPDATE |   13
 UPDATE |    0
(2 rows)

ROLLBACK

Ikki xulosa:

  1. 13 qator — bitta chaqiruv. Ommaviy amalda ROW trigger'dan ancha arzon: audit'ni INSERT INTO tarix SELECT … FROM yangi bilan bitta buyruqda yozish mumkin.
  2. 0 qatorli UPDATE'da ham statement trigger ishladi. "Hech narsa o'zgarmasa, trigger chaqirilmaydi" deb o'ylamang — kerak bo'lsa ichida IF NOT EXISTS (SELECT 1 FROM yangi) tekshiring.

6. Narx: trigger'lar bepul emas

6.1 Kanon o'lchovi

5 000 vazifani bitta INSERT … SELECT bilan qo'shish. Ikki variant:

  • trigger'lar bilan — oddiy rejim;
  • trigger'larsiz — SET LOCAL session_replication_role = replica. Bu sozlama replikatsiya uchun mo'ljallangan: oddiy trigger'lar va FK trigger'lari ishlamaydi. Faqat superuser qo'ya oladi. tartib ni o'zimiz beramiz (pastda nega).

Kanon o'lchovida (IlmHamroh, 2026-10, PostgreSQL 18.6, psql \timing): trigger'lar bilan 3 350 ms, trigger'larsiz 118 ms. Biz o'z PostgreSQL 17.6 klasterimizda xuddi shu ikki buyruqni bajardik: 7 028 ms va 512 ms. Ikkala mashina ham o'lchov paytida parallel ishlar bilan band edi — kanonning boshqa urinishlarida trigger'li INSERT 1 082 ms dan 4 680 ms gacha tarqalgan. Shuning uchun mutlaq raqamni emas, nisbatni eslang: trigger'lar bu INSERT'ni o'n barobardan ko'proq sekinlashtirdi (kanonda 28, bizda 14 barobar).

6.2 Qaysi trigger qancha turadi

EXPLAIN ANALYZE har trigger'ga sarflangan vaqtni alohida ko'rsatadi (EXPLAIN ANALYZE'ni o'qish):

sql
BEGIN;
EXPLAIN (ANALYZE, COSTS OFF) INSERT INTO vazifalar (egasi_id, matn)
SELECT 1 + g % 7, 'Ommaviy vazifa ' || g || ' #ish'
FROM generate_series(1, 5000) g;
ROLLBACK;

Biz ishga tushirdik (PostgreSQL 17.6, band mashina — parallel ishlar bor edi, bitta o'lchov; vaqt sizda boshqacha bo'ladi):

text
 Insert on vazifalar (actual time=2893.816..2893.816 rows=0 loops=1)
   ->  Function Scan on generate_series g (actual time=0.986..46.583 rows=5000 loops=1)
 Planning Time: 1.111 ms
 Trigger for constraint vazifalar_egasi_id_fkey: time=152.622 calls=5000
 Trigger for constraint vazifalar_ota_id_fkey: time=8.499 calls=5000
 Trigger vazifa_tarixi_trg: time=605.508 calls=5000
 Trigger vazifa_teglari_trg: time=3842.246 calls=5000
 Trigger vazifalar_tartib_trg: time=2414.537 calls=5000
 Execution Time: 7511.223 ms

Grafikda xuddi shu raqamlar (millisekund, 5 000 qator):

5 000 qatorli INSERT: har trigger'ga ketgan vaqt (ms)
  • teglarhar qatorda matnni ajratib, teg yozadi3 842 ms
  • tartibhar qatorda max(tartib)2 415 ms
  • tarixhar qatorda bitta INSERT606 ms
  • FK egasi153 ms
  • FK ota8 ms

Manba: IlmHamroh o'lchovi, 2026-10-25: PostgreSQL 17.6, Windows 11, holat 04, EXPLAIN ANALYZE, band mashina, bitta o'lchov

Nimaga qarash kerak:

  1. calls=5000 — har ROW trigger har qator uchun alohida chaqirildi.
  2. Eng qimmati — teglar trigger'i: u har qatorda matnni ajratadi va bir nechta jadvalga yozadi. Ikkinchisi — tartibni_qoy: har qator uchun alohida SELECT max(tartib).
  3. Tashqi kalitlar ham trigger (Trigger for constraint …) — ichki, lekin bepul emas.

6.3 Yashirin mantiq: trigger'ni o'chirsangiz nima buziladi

session_replication_role = replica bilan "tez yuklaymiz" degan fikr tug'iladi. Lekin trigger'lar faqat sekinlashtirmaydi — ular ma'lumotni to'ldiradi. Kanon tajribasi:

sql
BEGIN;
SET LOCAL session_replication_role = replica;
INSERT INTO vazifalar (egasi_id, matn)
VALUES (1, 'Trigger''siz vazifa');
ROLLBACK;

Kanon chiqishi (PostgreSQL 18.6; DETAIL dagi id va vaqt har safar boshqacha):

text
BEGIN
SET
ERROR:  null value in column "tartib" of relation "vazifalar" violates not-null constraint
DETAIL:  Failing row contains (15053, 1, Trigger'siz vazifa, f, 2026-10-24 14:46:34.086+00, orta, null, null, null, 0, 2026-10-24 14:46:34.086+00, null, 1, null).
ROLLBACK

tartib ni tartibni_qoy trigger'i to'ldirardi — u o'chdi va NOT NULL ishga tushdi. Bu yerda baxtimiz bor: xato chiqdi. Agar tartib NULL'ga ruxsat berganida — vazifalar jim, tartibsiz yozilardi; teglar va tarix esa umuman yozilmasdi. Bu — yashirin mantiq xavfi amalda: trigger'ni bilmagan dasturchi uni "tezlik uchun" o'chirib, ma'lumotni buzadi.

Ommaviy yuklashning to'g'ri yo'llari:

  • Statement trigger + transition table (bitta chaqiruv).
  • Yuklashdan keyin bir martalik to'ldirish: UPDATE … FROM bilan teglar va hisoblagichlarni qayta hisoblash (Denormalizatsiya va hisoblangan maydonlar darsidagi backfill).
  • Aniq trigger'ni o'chirish: ALTER TABLE vazifalar DISABLE TRIGGER vazifa_teglari_trg; — lekin bu jadvalni qattiq qulflaydi va boshqa sessiyalarga ham ta'sir qiladi. Tranzaksiya ichida qilib, oxirida ENABLE qilish shart.

Tekshirib ko'ring: Nega ALTER TABLE … DISABLE TRIGGER ishlayotgan saytda xavfli, SET LOCAL session_replication_role esa unchalik emas?

Javob

DISABLE TRIGGER — jadval sozlamasi: u COMMIT bo'lgach hamma ulanishlar uchun trigger'siz ishlaydi, ustiga ALTER TABLE qulfi (SHARE ROW EXCLUSIVE) yozuvchilarni to'xtatadi. SET LOCAL session_replication_role faqat shu tranzaksiyaga ta'sir qiladi va oxirida o'zi qaytadi. Ikkalasida ham trigger bajaradigan mantiqni o'zingiz qilishingiz kerak.

7. Trigger yoki ilova kodi?

Trigger'da Ilovada
Har qanday yo'l bilan yozilganda ham bajarilishi shart (audit, updated_at) Tashqi tizimga murojaat (email, HTTP, navbat)
Hisoblangan qiymat bir jadval ichida (tartib, hisoblagich) Murakkab, tez-tez o'zgaradigan biznes qoida
Ma'lumot yaxlitligi, cheklov bilan ifodalab bo'lmaydigan Foydalanuvchiga ko'rinadigan xabar va tekshiruv

Va eng muhim qoida: trigger'larni hujjatlang. Sxemada izoh (COMMENT ON TRIGGER), README'da ro'yxat. Yangi dasturchi "nega versiya o'zi oshyapti?" deb bir kun yo'qotmasin.

Ikki qo'shimcha turni bilib qo'ying (bu kursda chuqur ishlatmaymiz):

  • Constraint trigger (CREATE CONSTRAINT TRIGGER … DEFERRABLE INITIALLY DEFERRED) — tekshiruvni COMMIT'gacha kechiktiradi, Ilg'or constraint'lar va generated ustunlar darsidagi deferrable cheklov kabi.
  • Event trigger — jadval emas, DDL hodisasiga (CREATE TABLE, DROP) ulanadi; masalan "prod'da DROP TABLE ni taqiqlash". Faqat superuser yaratadi.

8. Ko'p uchraydigan xatolar

8.1 BEFORE trigger'da RETURN NEW ni unutish

Funksiya RETURN siz tugasa — control reached end of trigger procedure without RETURN xatosi. RETURN NULL yozilsa — qator jim yo'qoladi (INSERT 0 0). BEFORE ROW — doim RETURN NEW (yoki ataylab NULL, izoh bilan).

8.2 AFTER trigger'da NEW ni o'zgartirish

AFTER trigger'da NEW.ustun := … hech narsa qilmaydi — qator allaqachon yozilgan. Qatorni o'zgartirish — faqat BEFORE'da.

8.3 UPDATE OF = "qiymat o'zgardi" deb o'ylash

U faqat ustun SET da borligini tekshiradi. WHEN (OLD.x IS DISTINCT FROM NEW.x).

8.4 Trigger ichida o'z jadvalini yangilash

stack depth limit exceeded. BEFORE trigger'da NEW orqali yoki WHEN bilan.

8.5 Ommaviy yuklashda trigger'ni "shunchaki o'chirish"

Yashirin mantiq yo'qoladi (kanon: tartib NULL). Trigger nimani to'ldirishini o'qing va keyin o'zingiz to'ldiring yoki statement trigger'ga o'ting.

9. Mashqlar

Mashqlar kurs/mashqlar/27/06-triggerlar/ da. Ular o'z jadvallaringiz bilan, istalgan bo'sh bazada bajariladi; yechimlar tekshirilgan (biz ishga tushirdik, PostgreSQL 17.6).

1-mashq (oson): updated_at

m06_taom (id int PRIMARY KEY, nom text, narx int, yangilangan timestamptz NOT NULL DEFAULT '2026-01-01') jadvaliga BEFORE UPDATE trigger yozing: yangilangan ni '2026-10-25' ga qo'ysin (darsda now() bo'lardi — mashqda natija barqaror bo'lishi uchun aniq sana). Osh qo'shing, narxini yangilang va nom || ' ' || yangilangan::date ni chiqaring.

Yechim
sql
CREATE TABLE m06_taom (id int PRIMARY KEY, nom text, narx int,
  yangilangan timestamptz NOT NULL DEFAULT '2026-01-01');
CREATE FUNCTION m06_vaqt() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
  NEW.yangilangan := '2026-10-25';
  RETURN NEW;
END $$;
CREATE TRIGGER m06_taom_vaqt BEFORE UPDATE ON m06_taom
  FOR EACH ROW EXECUTE FUNCTION m06_vaqt();
INSERT INTO m06_taom VALUES (1, 'osh', 35000);
SELECT nom || ' ' || yangilangan::date FROM m06_taom;
UPDATE m06_taom SET narx = 36000 WHERE id = 1;
SELECT nom || ' ' || yangilangan::date FROM m06_taom;
DROP TABLE m06_taom;
DROP FUNCTION m06_vaqt;

Biz ishga tushirdik (psql -X -q -A -t, PostgreSQL 17.6):

text
osh 2026-01-01
osh 2026-10-25

INSERT'da trigger ishlamadi (BEFORE UPDATE) — DEFAULT qoldi; UPDATE'da sana trigger'dan keldi. Ilova yangilangan ni yubormadi ham.

2-mashq (o'rta): Narx tarixi

m06_mahsulot (id int PRIMARY KEY, nom text, narx int) va m06_narx_tarixi (mahsulot_id int, eski int, yangi int) yarating. AFTER UPDATE trigger faqat narx haqiqatan o'zgarganda tarixga yozsin (WHEN). Manti narxini 30 000 → 30 000 → 32 000 → 31 000 qilib yangilang va tarixni chiqaring.

Yechim
sql
CREATE TABLE m06_mahsulot (id int PRIMARY KEY, nom text, narx int);
CREATE TABLE m06_narx_tarixi (mahsulot_id int, eski int, yangi int);
CREATE FUNCTION m06_narx_yoz() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
  INSERT INTO m06_narx_tarixi VALUES (NEW.id, OLD.narx, NEW.narx);
  RETURN NULL;
END $$;
CREATE TRIGGER m06_narx_trg AFTER UPDATE OF narx ON m06_mahsulot
  FOR EACH ROW
  WHEN (OLD.narx IS DISTINCT FROM NEW.narx)
  EXECUTE FUNCTION m06_narx_yoz();
INSERT INTO m06_mahsulot VALUES (1, 'manti', 30000);
UPDATE m06_mahsulot SET narx = 30000 WHERE id = 1;
UPDATE m06_mahsulot SET narx = 32000 WHERE id = 1;
UPDATE m06_mahsulot SET narx = 31000 WHERE id = 1;
SELECT eski || ' -> ' || yangi FROM m06_narx_tarixi;
DROP TABLE m06_mahsulot, m06_narx_tarixi;
DROP FUNCTION m06_narx_yoz;

Biz ishga tushirdik (psql -X -q -A -t, PostgreSQL 17.6):

text
30000 -> 32000
32000 -> 31000

Uch UPDATE — ikki yozuv: birinchisi qiymatni o'zgartirmadi va WHEN funksiyani chaqirmadi. UPDATE OF narx va WHEN birga: biri "narx tilga olindimi", ikkinchisi "narx o'zgardimi".

3-mashq (qiyin): Statement-level audit

m06_buyurtma (id int PRIMARY KEY, holat text) va m06_audit (amal text, soni int) yarating. Bitta statement trigger (transition table bilan) INSERT va DELETE uchun ta'sir qilgan qatorlar sonini yozsin. Ishora: INSERT va DELETE uchun ikki alohida trigger kerak — REFERENCING NEW TABLE faqat INSERT/UPDATE'da, OLD TABLE — DELETE/UPDATE'da; funksiya bitta bo'lishi mumkin, TG_OP ga qarab yangi yoki eski dan sanaydi. 5 ta buyurtma qo'shing, 2 tasini o'chiring, 0 tasini o'chiring (WHERE false).

Yechim
sql
CREATE TABLE m06_buyurtma (id int PRIMARY KEY, holat text);
CREATE TABLE m06_audit (amal text, soni int);
CREATE FUNCTION m06_sana() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
  IF TG_OP = 'INSERT' THEN
    INSERT INTO m06_audit SELECT TG_OP, count(*) FROM yangi;
  ELSE
    INSERT INTO m06_audit SELECT TG_OP, count(*) FROM eski;
  END IF;
  RETURN NULL;
END $$;
CREATE TRIGGER m06_ins AFTER INSERT ON m06_buyurtma
  REFERENCING NEW TABLE AS yangi
  FOR EACH STATEMENT EXECUTE FUNCTION m06_sana();
CREATE TRIGGER m06_del AFTER DELETE ON m06_buyurtma
  REFERENCING OLD TABLE AS eski
  FOR EACH STATEMENT EXECUTE FUNCTION m06_sana();
INSERT INTO m06_buyurtma
SELECT g, 'yangi' FROM generate_series(1, 5) g;
DELETE FROM m06_buyurtma WHERE id <= 2;
DELETE FROM m06_buyurtma WHERE false;
SELECT amal || ': ' || soni FROM m06_audit;
DROP TABLE m06_buyurtma, m06_audit;
DROP FUNCTION m06_sana;

Biz ishga tushirdik (psql -X -q -A -t, PostgreSQL 17.6):

text
INSERT: 5
DELETE: 2
DELETE: 0

Uch buyruq — uch yozuv, 5 qator uchun bitta chaqiruv. Uchinchi qator darsdagi topilmani takrorlaydi: 0 qatorli buyruqda ham statement trigger ishlaydi.

10. Loyiha qadami: kanon tajribasi, loyihaga kirmaydi

Bu darsda vazifalar sxemasiga yangi migratsiya yo'q — beshta trigger 26-qismdan beri ishlab turibdi. Darsning loyihaga beradigan foydasi — ular haqidagi bilim:

  • vazifalar_yangilangan_trg UPDATE OF bilan ishlaydi va qiymat o'zgarmasa ham versiyani oshiradi. Tavsiya etilgan yechim — WHEN (OLD.* IS DISTINCT FROM NEW.*); kanon trigger'i hozircha UPDATE OF bilan qoldi va bu tuzoqni biladi.
  • Ommaviy yuklash (Seeding va test ma'lumotlari) trigger'lar bilan sekin — bu kutilgan narx. tartib, teglar va tarix kerak bo'lgani uchun trigger'lar o'chirilmaydi.
  • Kanondagi katta to'plam skripti (500 000 vazifa) trigger'larni o'chirib yuklagan va aynan tartib NOT NULL xatosiga uchragan — tartib ni qo'lda bergandan keyingina ishladi. Yuqoridagi "yashirin mantiq" bo'limi — o'sha voqea.

11. Real ishda

  • Audit — moliya, tibbiyot, davlat tizimlarida majburiy talab: "kim, qachon, nimani o'zgartirdi". Ko'pincha aynan trigger + JSONB bilan quriladi (yoki pgaudit kengaytmasi bilan — kengaytmalar keyingi darslarda).
  • ORM va trigger: Prisma kabi vositalar trigger'ni bilmaydi — updatedAt ni ilovada qo'yadi. Ikkalasi birga ishlasa, qaysi biri ustun ekanini aniqlang (Prisma: modellar va atributlar).
  • Migratsiyalarda trigger qo'shish/o'zgartirish — qulf oladi; katta jadvalda texnik oynada bajariladi.
  • Intervyu: "BEFORE va AFTER trigger farqi?", "Trigger'larning kamchiligi?", "Statement va row trigger qachon?" — ko'p uchraydi.

Xulosa

  • Trigger = funksiya + hodisa: BEFORE/AFTER × ROW/STATEMENT (+ view uchun INSTEAD OF).
  • BEFORE ROW NEW ni o'zgartiradi; RETURN NULL qatorni jim tashlaydi. Bir hodisadagi trigger'lar nom bo'yicha alifbo tartibida.
  • UPDATE OF — ustun tilga olindimi; WHEN (OLD.x IS DISTINCT FROM NEW.x) — qiymat o'zgardimi.
  • Statement trigger + transition table — ommaviy amalda bitta chaqiruv, 0 qatorda ham ishlaydi.
  • Trigger'lar narxi real: kanonda 5 000 qatorli INSERT o'n barobardan ko'proq sekin; FK ham ichki trigger.
  • Trigger'ni o'chirish yashirin mantiqni o'chiradi — hujjatlang va o'rnini to'ldiring.

Keyingi dars: Postgres'da full-text search — generated tsvector ustun, GIN indeks va o'zbekcha matn (apostrof, kirill) bilan qidiruv.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
PostgreSQL triggerlari: BEFORE/AFTER, ROW/STATEMENT, audit va yashirin narx — IlmHamroh