IlmHamroh
JavaScript Full-stack/26-qism. SQL va PostgreSQL38/64-dars19 daqiqa
Mundarija (27)

Soft delete, timestamps va tarix jadvallari: ma'lumotni izsiz yo'qotmaslik

Qisqacha: Uch naqsh ma'lumotni izsiz yo'qotishdan saqlaydi. Timestamps — har qatorda yaratilgan_vaqt va yangilangan_vaqt; ikkinchisini trigger qo'yadi. Soft delete — qator o'chirilmaydi, ochirilgan_vaqt belgilanadi ("Savat"); noyoblik esa faqat faollar orasida — partial unique index bilan. Tarix jadvali — har o'zgarishda eski va yangi qiymat, kim va qachon — trigger yozadi.

Bu darsda

  • yangilangan_vaqt ni BEFORE trigger bilan avtomatik yangilaysiz va qaysi o'zgarish "tahrir" ekanini belgilaysiz.
  • Soft delete'ni partial unique index va ko'rinish (view) bilan to'g'ri qurasiz, uning tuzoqlarini bilasiz.
  • Tarix jadvalini jsonb va trigger bilan yozasiz, "kim o'zgartirdi"ni SET LOCAL orqali uzatasiz.
  • Tarixdan "qaysi maydon o'zgardi"ni chiqarasiz va eski qiymatni tiklaysiz.
  • Temporal jadvallar g'oyasini va PostgreSQL 18 dagi yangiliklarni bilasiz.

Oldin bilishingiz kerak: Denormalizatsiya va hisoblangan maydonlar (trigger), Modellash naqshlari (jsonb), UPDATE va DELETE xavfsiz, SQL'da sana va vaqt zonalari.

1. Nega bu kerak?

Malika telefonda ro'yxatni varaqlab, barmog'i tegib ketdi — "Universitet kontrakti" vazifasi o'chdi. U yordamga yozadi: "Qaytarib bera olasizmi?" Oddiy DELETE bo'lsa — javob "yo'q". Qator bazadan ketdi, u bilan izohlar ham (CASCADE).

Ikkinchi holat. Jasur aka jamoaviy ro'yxatda vazifa matni o'zgarganini ko'rdi: "Kim o'zgartirdi? Avval nima yozilgan edi?" UPDATE eski qiymatni hech qayerda qoldirmaydi.

Uchinchisi oddiyroq: frontend "oxirgi o'zgartirilgan vazifalar" ro'yxatini ko'rsatmoqchi. Buning uchun har qatorda "qachon o'zgargan" degan vaqt kerak.

O'xshatish: bank ko'chirmasi. Bank hech qachon "operatsiyani o'chirmaydi" — xato to'lov bo'lsa, qarshi operatsiya yoziladi va tarixda ikkalasi qoladi. Bu darsda vazifalar ga shu darajadagi "xotira" beramiz.

2. Timestamps: yangilangan_vaqt

2.1 Ustun va trigger

yaratilgan_vaqt bizda birinchi kundan bor (DEFAULT now()). Endi yangilangan_vaqt qo'shamiz. Uni ilova kodida qo'yish mumkin, lekin counter cache darsidagi sabab bilan — bitta joyda unutiladi — trigger ishonchliroq. Kanon sql/010-vaqt-tarix.sql boshi (kod o'zgarmagan, uzun qatorlar bo'lingan, izohlar qisqartirilgan):

sql
-- ochirilgan_vaqt ham shu yerda: trigger ro'yxatida u bor,
-- shuning uchun ustun trigger'dan oldin yaratilishi kerak
ALTER TABLE vazifalar
  ADD COLUMN yangilangan_vaqt timestamptz NOT NULL DEFAULT now(),
  ADD COLUMN ochirilgan_vaqt timestamptz;
UPDATE vazifalar
SET yangilangan_vaqt = coalesce(bajarilgan_vaqt, yaratilgan_vaqt);

CREATE FUNCTION yangilangan_vaqtni_qoy() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
  NEW.yangilangan_vaqt := now();  -- tranzaksiya boshlangan vaqt
  RETURN NEW;                     -- BEFORE: shu qator yoziladi
END $$;

-- faqat foydalanuvchi o'zgartiradigan ustunlar:
-- izohlar_soni (counter cache) o'zgarishi "tahrir" emas
CREATE TRIGGER vazifalar_yangilangan_trg
BEFORE UPDATE OF matn, bajarildi, ustuvorlik, muddat, ota_id,
                 ochirilgan_vaqt ON vazifalar
FOR EACH ROW EXECUTE FUNCTION yangilangan_vaqtni_qoy();

Bu safar trigger BEFORE: u qator yozilishidan oldin ishlaydi va NEW ni o'zgartira oladi. NEW.yangilangan_vaqt := now() — yoziladigan qatorga vaqtni qo'yamiz, RETURN NEW — "shu o'zgartirilgan qatorni yoz". Counter trigger'i AFTER edi, chunki u boshqa jadvalga yozardi; bu yerda esa o'sha qatorni o'zgartiramiz.

Backfill ham o'ylangan: mavjud qatorlarda haqiqiy "oxirgi o'zgarish" noma'lum, shuning uchun eng yaqin ma'lum vaqt — bajarilgan yoki yaratilgan vaqt — yozildi. DEFAULT now() qoldirilsa, hamma eski vazifalar "bugun o'zgargan" bo'lib qolardi.

2.2 Amalda

Bu va keyingi so'rovlar kanon sorovlar/38-vaqt-tarix.sql dan (holat 38). Biz o'z PostgreSQL 17.6 klasterimizda qayta ishga tushirdik — chiqish bir xil; vaqtga bog'liq ikki joyni alohida aytamiz. Avval 9-vazifaning ustuvorligini o'zgartiramiz:

sql
SET TIME ZONE 'Asia/Tashkent';
SELECT id, yangilangan_vaqt FROM vazifalar WHERE id = 9;
UPDATE vazifalar SET ustuvorlik = 'yuqori' WHERE id = 9;
SELECT id, yangilangan_vaqt > '2026-10-03 10:55+05' AS yangilandi
FROM vazifalar WHERE id = 9;
text
SET
 id |    yangilangan_vaqt
----+------------------------
  9 | 2026-10-03 10:55:00+05
(1 row)

UPDATE 1
 id | yangilandi
----+------------
  9 | t
(1 row)

Biz yangilangan_vaqt ga tegmadik — trigger o'zi qo'ydi. Yangi qiymatni to'g'ridan-to'g'ri ko'rsatmadik: u ishga tushirilgan paytga bog'liq (kanon — 2026-10-24 10:06, bizda boshqa). Shuning uchun "eskisidan kattami?" deb so'radik. (Kanon so'rovi > '2026-10-24' bilan solishtirgan — u faqat o'sha kuni ishlaydi; biz barqaror shaklga o'zgartirdik.)

Endi izoh qo'shamiz — bu izohlar_soni ni o'zgartiradi, lekin "tahrir" emas:

sql
SET TIME ZONE 'Asia/Tashkent';
INSERT INTO izohlar (vazifa_id, muallif_id, matn)
VALUES (23, 1, 'Qistirma olindi');
SELECT yangilangan_vaqt AS keyin, izohlar_soni
FROM vazifalar WHERE id = 23;
text
SET
INSERT 0 1
         keyin          | izohlar_soni
------------------------+--------------
 2026-10-07 10:15:00+05 |            3
(1 row)

izohlar_soni 3 ga oshdi, yangilangan_vaqt esa eski (7-oktyabr). Sabab — UPDATE OF … ro'yxatida izohlar_soni yo'q. Agar u ro'yxatda bo'lsa, har izoh vazifani "yangi o'zgargan" qilib, "oxirgi tahrirlar" ro'yxatini buzardi.

Tekshirib ko'ring: Nega now() va clock_timestamp() emas? Bitta tranzaksiyada 100 ta vazifa yangilansa, ularning yangilangan_vaqt i qanday bo'ladi?

Javob

now() — tranzaksiya boshlangan vaqt (Funksiyalar darsida ko'rganimizdek, tranzaksiya ichida o'zgarmaydi). 100 ta vazifaning hammasida bir xil vaqt — ular bitta amalda o'zgardi va bu to'g'ri. clock_timestamp() har qatorda biroz boshqa vaqt berardi va "bir amal" ekanini yashirardi.

2.3 created_by — kim yaratgan

Ko'p tizimda vaqt bilan birga kim ham saqlanadi: created_by, updated_by. Bizda yaratgan odam — egasi_id. "Kim o'zgartirgan" esa tarix jadvalida (keyingi bo'limlar) — har qatorda bitta updated_by ustuni faqat oxirgi o'zgartiruvchini biladi, tarix esa hammasini.

3. Soft delete — yumshoq o'chirish

3.1 G'oya

Soft delete (yumshoq o'chirish) — qatorni o'chirish o'rniga uni "o'chirilgan" deb belgilash: ochirilgan_vaqt ga vaqt yoziladi. Qator va uning izohlari joyida — "Savat"dan tiklash mumkin. Haqiqiy DELETE (hard delete) keyinroq, masalan 30 kundan keyin bajariladi.

Nimaga qarang: vazifaning hayot yo'li endi uch holatli.

stateDiagram-v2
  [*] --> Faol: INSERT
  Faol --> Savatda: ochirilgan_vaqt = now()
  Savatda --> Faol: ochirilgan_vaqt = NULL
  Savatda --> [*]: 30 kundan keyin DELETE

ochirilgan_vaqt ustuni 2.1 dagi ALTER bilan allaqachon qo'shilgan. Uning yonida boolean (ochirildi) emas, vaqt tanlangani — bitta ustunda ikki ma'lumot: o'chirilganmi va qachon.

3.2 Noyoblik faqat faollar orasida

22-qismdagi frontend qoidasini eslang: "Bu vazifa ro'yxatda allaqachon bor" — registrga befarq takror matn rad etiladi. Endi bu qoida bazada. Lekin oddiy UNIQUE soft delete bilan to'qnashadi: o'chirilgan "Onamga dori olib borish" Savatda turibdi — yangisini yozib bo'lmaydi. Yechim — partial unique index: noyoblik faqat WHERE shartiga mos qatorlar orasida. Kanon 010 davomi:

sql
CREATE UNIQUE INDEX vazifalar_egasi_id_matn_key
  ON vazifalar (egasi_id, lower(matn))
  WHERE ochirilgan_vaqt IS NULL;

CREATE VIEW faol_vazifalar AS
  SELECT * FROM vazifalar WHERE ochirilgan_vaqt IS NULL;

lower(matn) — ifoda bo'yicha indeks (registrga befarq), WHERE — faqat faollar. Indekslarning bu turlarini Partial, expression va covering indekslar darsida chuqur ko'ramiz. Ko'rinish (view) faol_vazifalar — saqlangan so'rov: "faqat o'chirilmaganlar" shartini har safar yozmaslik uchun (Denormalizatsiya darsida materialized view'ni ko'rgan edik; oddiy view esa ma'lumot saqlamaydi, har safar so'rovni bajaradi).

3.3 Amalda

Avval faol takror — katta harf bilan yozilgan bo'lsa ham:

sql
INSERT INTO vazifalar (egasi_id, matn)
VALUES (1, 'onamga dori olib borish #OILA');
text
ERROR:  duplicate key value violates unique constraint "vazifalar_egasi_id_matn_key"
DETAIL:  Key (egasi_id, lower(matn))=(1, onamga dori olib borish #oila) already exists.

Rad etildi. Endi 9-vazifani Savatga yuboramiz va xuddi shu matnni qayta yozamiz:

sql
UPDATE vazifalar SET ochirilgan_vaqt = now() WHERE id = 9;
INSERT INTO vazifalar (egasi_id, matn)
VALUES (1, 'Onamga dori olib borish #oila')
RETURNING id;
SELECT id, matn, ochirilgan_vaqt IS NOT NULL AS savatda
FROM vazifalar
WHERE lower(matn) = 'onamga dori olib borish #oila'
ORDER BY id;
text
UPDATE 1
 id
----
 49
(1 row)

INSERT 0 1
 id |             matn              | savatda
----+-------------------------------+---------
  9 | Onamga dori olib borish #oila | t
 49 | Onamga dori olib borish #oila | f
(2 rows)

Ikki qator bir xil matn bilan — biri Savatda, biri faol. Partial index buni ruxsat etdi. Endi Savatdagisini tiklashga urinamiz:

sql
UPDATE vazifalar SET ochirilgan_vaqt = NULL WHERE id = 9;
text
ERROR:  duplicate key value violates unique constraint "vazifalar_egasi_id_matn_key"
DETAIL:  Key (egasi_id, lower(matn))=(1, onamga dori olib borish #oila) already exists.

To'g'ri xatti-harakat: tiklansa, ikki faol takror bo'lardi. Ilova bu xatoni ushlab, "Bunday vazifa ro'yxatda bor — nomini o'zgartirib tiklang" deyishi kerak. Ko'rinish faqat faollarni sanaydi:

sql
SELECT count(*) AS hammasi,
       (SELECT count(*) FROM faol_vazifalar) AS faol
FROM vazifalar;
text
 hammasi | faol
---------+------
      48 |   47
(1 row)

3.4 Soft delete'ning narxi

Soft delete bepul emas. Uch tuzoq:

  1. Har so'rov shartni eslashi kerak. WHERE ochirilgan_vaqt IS NULL ni bitta joyda unutsangiz — o'chirilgan vazifa ro'yxatga qaytadi. Ko'rinish (faol_vazifalar) bunga yordam beradi: ilova odatda jadvalga emas, ko'rinishga murojaat qiladi.
  2. Tashqi kalit soft delete'ni bilmaydi. ON DELETE CASCADE faqat haqiqiy DELETE da ishlaydi. Savatdagi vazifaning izohlari va teglari joyida qoladi va "teg bo'yicha qidiruv" ularni ham topadi — so'rovda vazifa holatini tekshirish kerak.
  3. Ma'lumot yo'qolmaydi — bu ham muammo. Foydalanuvchi "hisobimni o'chiring" desa, qonun (O'zbekistonda shaxsiy ma'lumotlar to'g'risidagi qonun, Yevropada GDPR) haqiqiy o'chirishni talab qilishi mumkin. Shuning uchun Savat muddatli: "30 kundan keyin butunlay o'chiriladi" (2-mashq).

Tekshirib ko'ring: Nega kanon ON DELETE CASCADE ni olib tashlamadi, soft delete bor-ku?

Javob

Haqiqiy o'chirish baribir bor: Savat tozalanganda yoki foydalanuvchi hisobini o'chirganda. O'shanda izohlar va teglar ham ketishi kerak — CASCADE shu uchun. Soft delete — "birinchi bosqich", CASCADE — "ikkinchi bosqich".

4. Tarix jadvali

4.1 Tuzilma

Soft delete "o'chirildi"ni saqlaydi. "Nima o'zgardi" uchun esa tarix jadvali (history / audit table) kerak: har o'zgarishda bitta qator — qaysi vazifa, qanday amal, eski va yangi qiymat, kim, qachon. Kanon 010 dagi jadval:

sql
CREATE TABLE vazifa_tarixi (
  id        bigint      GENERATED ALWAYS AS IDENTITY,
  vazifa_id bigint      NOT NULL,
  amal      text        NOT NULL,
  eski      jsonb,
  yangi     jsonb,
  -- kim: ilova har tranzaksiyada
  -- SET LOCAL ilova.foydalanuvchi_id = '…' qiladi; bo'lmasa NULL
  kim_id    bigint,
  vaqt      timestamptz NOT NULL DEFAULT now(),
  CONSTRAINT vazifa_tarixi_pkey PRIMARY KEY (id),
  CONSTRAINT vazifa_tarixi_amal_check
    CHECK (amal IN ('INSERT', 'UPDATE', 'DELETE'))
);
CREATE INDEX vazifa_tarixi_vazifa_id_vaqt_idx
  ON vazifa_tarixi (vazifa_id, vaqt);

Uch ongli qaror:

  • vazifa_id — tashqi kalit EMAS. Vazifa butunlay o'chirilsa, CASCADE tarixni ham olib ketardi — tarixning ma'nosi yo'qoladi. Kanon diagrammasida shuning uchun nuqtali chiziq (ER modellash).
  • eski/yangi — butun qator jsonb da. Ustun qo'shilsa, tarix jadvali o'zgarmaydi (jsonb — bu yerda haqiqatan "har xil shakl" bor).
  • kim_id — kim o'zgartirgani. Lekin trigger buni qayerdan biladi?

4.2 Trigger va "kim"

Trigger bazada ishlaydi va foydalanuvchini bilmaydi — barcha so'rovlar bitta postgres (yoki ilova) roli bilan keladi. Yechim: ilova tranzaksiya boshida sessiya sozlamasi qo'yadi, trigger uni o'qiydi. Kanon 010 oxiri:

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 $$;

CREATE TRIGGER vazifa_tarixi_trg
AFTER INSERT OR DELETE
   OR UPDATE OF matn, bajarildi, ustuvorlik, muddat, ota_id,
                ochirilgan_vaqt
ON vazifalar
FOR EACH ROW EXECUTE FUNCTION vazifa_tarixini_yoz();
Qism Ma'nosi
current_setting('ilova.foydalanuvchi_id', true) Sozlamani o'qish; true — yo'q bo'lsa xato emas, NULL
nullif(…, '')::bigint Bo'sh satr ham NULL bo'lsin, keyin songa
to_jsonb(OLD) Butun qatorni JSON obyektga
- 'izohlar_soni' Counter'ni tarixdan chiqarish (u "tahrir" emas)
coalesce(NEW.id, OLD.id) DELETE da NEW yo'q, INSERT da OLD yo'q

Nomda nuqta (ilova.foydalanuvchi_id) — Postgres'da o'z sozlamalari uchun ruxsat etilgan shakl ("custom option").

4.3 Amalda: SET LOCAL

Ilova "kim"ni tranzaksiya ichida SET LOCAL bilan beradi — u faqat shu tranzaksiya oxirigacha amal qiladi. Kanon so'rovi SET + RESET bilan yozilgan; biz uni ilovadagi to'g'ri shaklga — BEGIN … SET LOCAL … COMMIT ga o'zgartirib ishga tushirdik (natija bir xil):

sql
BEGIN;
SET LOCAL ilova.foydalanuvchi_id = '1';
UPDATE vazifalar
SET matn = 'SQL JOIN va subquery mashqlari #o''qish'
WHERE id = 34;
COMMIT;
SELECT vazifa_id, amal, kim_id,
       eski ->> 'matn' AS eski_matn, yangi ->> 'matn' AS yangi_matn
FROM vazifa_tarixi WHERE vazifa_id = 34;
text
BEGIN
SET
UPDATE 1
COMMIT
 vazifa_id |  amal  | kim_id |              eski_matn               |               yangi_matn
-----------+--------+--------+--------------------------------------+----------------------------------------
        34 | UPDATE |      1 | SQL JOIN mashqlarini yechish #o'qish | SQL JOIN va subquery mashqlari #o'qish
(1 row)

Eski matn, yangi matn va kim — hammasi bor. Nega SET emas, SET LOCAL? Oddiy SET ulanish yopilguncha qoladi. Ilovada ulanishlar qayta ishlatiladi (Connection pooling) — keyingi foydalanuvchining so'rovi Azizning foydalanuvchi_id si bilan yozilib qolishi mumkin. SET LOCAL tranzaksiya tugashi bilan o'chadi.

Tarixda nechta yozuv bor?

sql
SELECT amal, count(*) FROM vazifa_tarixi GROUP BY 1 ORDER BY 1;
text
  amal  | count
--------+-------
 INSERT |     1
 UPDATE |     3
(2 rows)

Uchta UPDATE (ustuvorlik, Savatga, matn) va bitta INSERT (49-vazifa). Muvaffaqiyatsiz urinishlar (takror, tiklash) tarixga yozilmadi: xato bilan bekor bo'lgan amalning trigger yozuvi ham bekor.

4.4 "Nima o'zgardi?"

Tarixda butun qator saqlangan. Shuning uchun "aynan qaysi maydon o'zgardi"ni so'rov bilan chiqarish mumkin: yangi ning har kaliti uchun eski va yangi qiymatni solishtiramiz. jsonb_object_keys — kalitlar ro'yxati, CROSS JOIN LATERAL — har tarix qatori uchun alohida (Subquery va LATERAL). Bu chiqishda now() vaqtlari bor — har ishga tushirishda boshqa, shuning uchun kanon chiqishini keltiramiz:

sql
SELECT t.id, k AS maydon, t.eski -> k AS eski, t.yangi -> k AS yangi
FROM vazifa_tarixi t
CROSS JOIN LATERAL jsonb_object_keys(t.yangi) AS k
WHERE t.amal = 'UPDATE'
  AND t.eski -> k IS DISTINCT FROM t.yangi -> k
ORDER BY t.id, k;
text
 id |      maydon      |                  eski                  |                  yangi
----+------------------+----------------------------------------+------------------------------------------
  1 | ustuvorlik       | "orta"                                 | "yuqori"
  1 | yangilangan_vaqt | "2026-10-03T10:55:00+05:00"            | "2026-10-24T10:06:54.35671+05:00"
  2 | ochirilgan_vaqt  | null                                   | "2026-10-24T10:06:54.379624+05:00"
  2 | yangilangan_vaqt | "2026-10-24T10:06:54.35671+05:00"      | "2026-10-24T10:06:54.379624+05:00"
  4 | matn             | "SQL JOIN mashqlarini yechish #o'qish" | "SQL JOIN va subquery mashqlari #o'qish"
  4 | yangilangan_vaqt | "2026-10-14T19:30:00+05:00"            | "2026-10-24T10:06:54.397752+05:00"
(6 rows)

Har o'zgarish — bitta maydon + yangilangan_vaqt (ikkinchi trigger qo'ygan). Frontend'da bu "Aziz matnni o'zgartirdi: … → …" kabi lentaga aylanadi. IS DISTINCT FROM — NULL bilan ham to'g'ri solishtirish (NULL mantiqi).

4.5 Hajm

Tarix har o'zgarishda butun qatorni saqlaydi — u tez o'sadi. Kanon holat 38 da (4 ta yozuvdan keyin) o'lchagan:

sql
SELECT
  pg_size_pretty(pg_total_relation_size('vazifa_tarixi')) AS tarix,
  pg_size_pretty(pg_total_relation_size('vazifalar')) AS vazifalar;
text
 tarix | vazifalar
-------+-----------
 48 kB | 128 kB
(1 row)

Kichik bazada raqamlar sahifa o'lchamlaridan iborat (har jadval va indeks kamida bir necha 8 kB sahifa), lekin qoida aniq: faol ilovada tarix asosiy jadvaldan bir necha marta katta bo'ladi. Real loyihada tarix uchun saqlash muddati (masalan, 1 yil) va eskisini arxivlash rejasi kerak.

5. Temporal jadvallar g'oyasi

Tarix jadvalini qo'lda yozdik. SQL standartida (SQL:2011) bunga o'rnatilgan imkoniyat bor — temporal tables: jadval har qatorning amal qilish davrini o'zi saqlaydi va "2026-10-01 holatiga ko'ra ma'lumot" degan so'rovni qo'llab-quvvatlaydi. Ikki xil vaqt bor:

  • Tizim vaqti (system time) — qator bazada qachon bo'lgan (bizning tarix jadvalimiz shuni qiladi). MariaDB va SQL Server'da o'rnatilgan; Postgres'da yo'q — trigger yoki kengaytma bilan qilinadi.
  • Ilova vaqti (application time) — fakt haqiqiy hayotda qachon to'g'ri bo'lgan: "bu narx 1-noyabrdan 31-dekabrgacha". PostgreSQL 18 da shu uchun yangi cheklovlar qo'shildi: PRIMARY KEY (…, davr WITHOUT OVERLAPS) va FOREIGN KEY (…, PERIOD davr) — bitta kalit uchun davrlar ustma-ust tushmasin (postgresql.org/docs/18, "CREATE TABLE"). Biz buni sinamadik: klasterimiz 17.6.

Bizning vazifalar uchun qo'lda yozilgan tarix yetarli: u kim va nimani o'zgartirganini saqlaydi.

6. Ko'p uchraydigan xatolar

1. yangilangan_vaqt ni ilova kodida qo'yish. Bir joyda unutiladi va "oxirgi o'zgarishlar" noto'g'ri. Tuzatish: BEFORE UPDATE trigger.

2. Oddiy UNIQUE + soft delete. O'chirilgan qator noyob qiymatni "egallab" turadi — yangi yozib bo'lmaydi. Tuzatish: UNIQUE INDEX … WHERE ochirilgan_vaqt IS NULL.

3. ochirilgan_vaqt IS NULL ni unutish. O'chirilgan vazifalar hisobot va qidiruvda paydo bo'ladi. Tuzatish: ilova faqat ko'rinish orqali o'qisin; review'da FROM vazifalar ni tekshiring.

4. Tarixga tashqi kalit. vazifa_id REFERENCES vazifalar ON DELETE CASCADE — vazifa o'chsa, uning tarixi ham yo'qoladi. Tuzatish: tarix — tashqi kalitsiz.

5. "Kim"ni SET bilan berish. Pool'da ulanish keyingi so'rovga o'tadi va boshqa foydalanuvchining amali Aziz nomiga yoziladi. Tuzatish: SET LOCAL tranzaksiya ichida.

6. Tarixni hech qachon tozalamaslik. Bir yildan keyin tarix jadvali eng katta jadval, zaxira sekin. Tuzatish: saqlash muddati va arxiv.

7. Mashqlar

Holat 38 da. Hammasi BEGIN … ROLLBACK ichida.

1-mashq (oson)

16- va 29-vazifalarni Savatga yuboring (ochirilgan_vaqt = '2026-10-20 09:00+05'). Dilnozaning (egasi_id = 2) Savatidagi vazifalarni (id, matn) va faol vazifalari sonini chiqaring. Ishora: faollar — faol_vazifalar ko'rinishi.

Yechim
sql
BEGIN;
UPDATE vazifalar SET ochirilgan_vaqt = '2026-10-20 09:00+05'
WHERE id IN (16, 29);
SELECT id, matn FROM vazifalar
WHERE egasi_id = 2 AND ochirilgan_vaqt IS NOT NULL
ORDER BY ochirilgan_vaqt DESC, id;
SELECT count(*) AS faol FROM faol_vazifalar WHERE egasi_id = 2;
ROLLBACK;
text
BEGIN
UPDATE 2
 id |            matn
----+----------------------------
 16 | Bozordan meva olish #bozor
 29 | Gaz to'lovi #to'lov
(2 rows)

 faol
------
    4
(1 row)

ROLLBACK

Savat — jadvalning o'zi, faqat boshqa shart bilan. Ko'rinish faol qatorlarni o'zi ajratdi.

2-mashq (o'rta)

"30 kundan eski Savatni tozalash": 8-vazifani '2026-09-01 12:00+05' da, 28-vazifani '2026-10-20 12:00+05' da Savatga yuboring. "Bugun" — '2026-10-24 12:00+05' deb, 30 kundan oldin o'chirilganlarni butunlay o'chiring (RETURNING id, matn). Keyin 8- va 28-vazifalar tarixida qanday amallar borligini sanang. Ishora: interval ayirish — Sana va vaqt zonalari; tarix — vazifa_tarixi.

Yechim
sql
BEGIN;
UPDATE vazifalar SET ochirilgan_vaqt = '2026-09-01 12:00+05'
WHERE id = 8;
UPDATE vazifalar SET ochirilgan_vaqt = '2026-10-20 12:00+05'
WHERE id = 28;
DELETE FROM vazifalar
WHERE ochirilgan_vaqt < timestamptz '2026-10-24 12:00+05'
                        - interval '30 days'
RETURNING id, matn;
SELECT amal, count(*) FROM vazifa_tarixi
WHERE vazifa_id IN (8, 28)
GROUP BY amal ORDER BY amal;
ROLLBACK;
text
BEGIN
UPDATE 1
UPDATE 1
 id |                   matn
----+------------------------------------------
  8 | Bozordan kartoshka va piyoz olish #bozor
(1 row)

DELETE 1
  amal  | count
--------+-------
 DELETE |     1
 UPDATE |     2
(2 rows)

ROLLBACK

Faqat 8-vazifa o'chdi (28 hali 4 kunlik). Vazifa yo'q, lekin tarixda uning DELETE yozuvi bor — eski ustunida oxirgi holati to'liq. Tarix tashqi kalitsiz bo'lgani uchun shunday. Real ilovada bu so'rov har kecha rejalashtirilgan vazifa bo'lib ishlaydi, "bugun" esa now().

3-mashq (qiyin)

Xato tahrirni tarixdan qaytarish: 8-vazifa matnini 'Bozordan faqat piyoz olish #bozor' ga o'zgartiring. Keyin bitta UPDATE … FROM bilan matnni tarixdagi oxirgi UPDATE yozuvining eski qiymatidan tiklang (RETURNING). Tarixda 8-vazifa uchun nechta yozuv borligini chiqaring. Ishora: eski ->> 'matn'; oxirgi yozuv — ORDER BY id DESC LIMIT 1; UPDATE … FROM — UPDATE va DELETE.

Yechim
sql
BEGIN;
UPDATE vazifalar SET matn = 'Bozordan faqat piyoz olish #bozor'
WHERE id = 8;
UPDATE vazifalar v
SET matn = t.eski ->> 'matn'
FROM (SELECT eski FROM vazifa_tarixi
      WHERE vazifa_id = 8 AND amal = 'UPDATE'
      ORDER BY id DESC LIMIT 1) t
WHERE v.id = 8
RETURNING v.id, v.matn;
SELECT count(*) AS tarixda FROM vazifa_tarixi WHERE vazifa_id = 8;
ROLLBACK;
text
BEGIN
UPDATE 1
 id |                   matn
----+------------------------------------------
  8 | Bozordan kartoshka va piyoz olish #bozor
(1 row)

UPDATE 1
 tarixda
---------
       2
(1 row)

ROLLBACK

Tiklash ham oddiy UPDATE — u ham tarixga yozildi (2 ta yozuv: o'zgartirish va qaytarish). Bank ko'chirmasidagidek: xato o'chirilmadi, ustidan tuzatish yozildi. Teglar trigger'i ham ishladi — matn bilan teglar sinxron.

8. SQL qadami

Holat 36 → holat 38. Bitta fayl: sql/010-vaqt-tarix.sql — yangilangan_vaqt va ochirilgan_vaqt ustunlari, BEFORE trigger, partial unique index, faol_vazifalar ko'rinishi, vazifa_tarixi jadvali va AFTER trigger (to'liq matni yuqoridagi bo'limlarda).

Kanon topilmasi — ochiq aytamiz. Faylning dastlabki variantida yangilangan_vaqt trigger'i UPDATE OF … ochirilgan_vaqt ni ko'rsatgan, lekin ochirilgan_vaqt ustuni fayl o'rtasida, trigger'dan keyin yaratilardi — fayl qo'llanmasdi. Xato fayl bazaga qo'llanishidan oldin topilgan va tuzatilgan: ustun birinchi ALTER ga ko'chirilgan (fayl boshidagi izoh shu haqida). Saboq: sxema faylini doim noldan qurib tekshiring — kanon tekshir.sh 38 hamma holatni namunasiz va namuna bilan qurib, exit 0 bergan.

Holat 38 ning \d vazifalar chiqishidan trigger'lar qismi (kanon, PostgreSQL 18.6; qatorlar uzun — psql shunday chiqaradi):

text
Triggers:
    vazifa_tarixi_trg AFTER INSERT OR DELETE OR UPDATE OF matn, bajarildi, ustuvorlik, muddat, ota_id, ochirilgan_vaqt ON vazifalar FOR EACH ROW EXECUTE FUNCTION vazifa_tarixini_yoz()
    vazifa_teglari_trg AFTER INSERT OR UPDATE OF matn ON vazifalar FOR EACH ROW EXECUTE FUNCTION vazifa_teglarini_yangila()
    vazifalar_yangilangan_trg BEFORE UPDATE OF matn, bajarildi, ustuvorlik, muddat, ota_id, ochirilgan_vaqt ON vazifalar FOR EACH ROW EXECUTE FUNCTION yangilangan_vaqtni_qoy()

Uchta trigger: biri BEFORE (vaqt), ikkitasi AFTER (teglar, tarix). Postgres bir xil turdagi trigger'larni nom bo'yicha alifbo tartibida chaqiradi (hujjat bo'yicha) — bu yerda tartib muhim emas, lekin trigger'lar bir-biriga bog'liq bo'lsa, nomlashda buni hisobga oling.

Commit:

bash
git add sql/010-vaqt-tarix.sql
M="db: yangilangan vaqt, yumshoq o'chirish (Savat), noyob matn"
M+=" va o'zgarishlar tarixi (010)"
git commit -m "$M"

Frontend bilan bog'liqlik: 22-qismdagi "Bu vazifa ro'yxatda allaqachon bor" qoidasi endi ikki joyda — frontend'da (tez javob) va bazada (kafolat). Server (25-qism API) bazaning 23505 xatosini "409 Conflict" ga aylantiradi — buni node-postgres asoslari dan boshlab ko'ramiz. Keyinroq Deadlock va serialization failure darsida bu tekshiruv buyruq oxiriga ko'chadi (sql/014) va xato kodi 23P01 bo'ladi — API uni ham 409 qiladi.

9. Real ishda

  • "Savat" va "Undo". Gmail, Telegram (xabarni o'chirish oynasi), Trello — hammasi soft delete + muddatli tozalash.
  • Audit log. Bank, to'lov tizimlari (Click, Payme), davlat xizmatlari — kim nimani o'zgartirgani qonun bo'yicha saqlanadi. Ko'pincha aynan shu naqsh: trigger + jsonb, yoki alohida audit xizmati.
  • ORM'lar. Prisma, Sequelize, Django'da updatedAt, paranoid/deletedAt kabi sozlamalar — ilova darajasidagi xuddi shu g'oya (trigger'siz — shuning uchun «Ko'p uchraydigan xatolar» dagi 1-xavf ularga ham tegishli).
  • Intervyuda: "soft delete'ning kamchiliklari?", "audit log'ni qanday qurasiz?" — javobda partial unique index, ko'rinish, SET LOCAL va saqlash muddatini tilga oling.

Xulosa

  • yangilangan_vaqt — BEFORE UPDATE OF … trigger; ro'yxatda faqat foydalanuvchi tahrirlari.
  • Soft delete — ochirilgan_vaqt + partial unique index (WHERE ochirilgan_vaqt IS NULL) + faol_vazifalar ko'rinishi.
  • Tashqi kalit CASCADE soft delete'ni bilmaydi; Savat muddatli bo'lsin — keyin haqiqiy DELETE.
  • Tarix jadvali — tashqi kalitsiz, jsonb eski/yangi, AFTER trigger; "kim" — SET LOCAL va current_setting.
  • Temporal jadvallar: tizim vaqti — qo'lda tarix; ilova vaqti — PostgreSQL 18 dagi WITHOUT OVERLAPS.

Keyingi dars: Pul, miqdor va enum'larni modellash — nega pul float da saqlanmaydi, valyuta va kurs tarixi, enum, lookup jadval va CHECK farqi.

Manbalar

  • PostgreSQL 18 hujjati: "CREATE TRIGGER" (trigger tartibi), "PL/pgSQL Trigger Functions", "CREATE INDEX — Partial Indexes", "CREATE VIEW", current_setting va "Customized Options", "CREATE TABLE — WITHOUT OVERLAPS / PERIOD" — postgresql.org/docs/18, 2026-10-24 da ko'rildi.
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Soft delete, timestamps va tarix jadvallari: ma'lumotni izsiz yo'qotmaslik — IlmHamroh