Mundarija (27)
- Bu darsda
- 1. Nega bu kerak?
- 2. Timestamps: yangilangan_vaqt
- 2.1 Ustun va trigger
- 2.2 Amalda
- 2.3 created_by — kim yaratgan
- 3. Soft delete — yumshoq o'chirish
- 3.1 G'oya
- 3.2 Noyoblik faqat faollar orasida
- 3.3 Amalda
- 3.4 Soft delete'ning narxi
- 4. Tarix jadvali
- 4.1 Tuzilma
- 4.2 Trigger va "kim"
- 4.3 Amalda: SET LOCAL
- 4.4 "Nima o'zgardi?"
- 4.5 Hajm
- 5. Temporal jadvallar g'oyasi
- 6. Ko'p uchraydigan xatolar
- 7. Mashqlar
- 1-mashq (oson)
- 2-mashq (o'rta)
- 3-mashq (qiyin)
- 8. SQL qadami
- 9. Real ishda
- Xulosa
- Manbalar
Soft delete, timestamps va tarix jadvallari: ma'lumotni izsiz yo'qotmaslik
Qisqacha: Uch naqsh ma'lumotni izsiz yo'qotishdan saqlaydi. Timestamps — har qatorda
yaratilgan_vaqtvayangilangan_vaqt; ikkinchisini trigger qo'yadi. Soft delete — qator o'chirilmaydi,ochirilgan_vaqtbelgilanadi ("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_vaqtniBEFOREtrigger 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
jsonbva trigger bilan yozasiz, "kim o'zgartirdi"niSET LOCALorqali 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):
-- 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:
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;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:
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;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()vaclock_timestamp()emas? Bitta tranzaksiyada 100 ta vazifa yangilansa, ularningyangilangan_vaqti 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 DELETEochirilgan_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:
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:
INSERT INTO vazifalar (egasi_id, matn)
VALUES (1, 'onamga dori olib borish #OILA');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:
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;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:
UPDATE vazifalar SET ochirilgan_vaqt = NULL WHERE id = 9;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:
SELECT count(*) AS hammasi,
(SELECT count(*) FROM faol_vazifalar) AS faol
FROM vazifalar; hammasi | faol
---------+------
48 | 47
(1 row)3.4 Soft delete'ning narxi
Soft delete bepul emas. Uch tuzoq:
- Har so'rov shartni eslashi kerak.
WHERE ochirilgan_vaqt IS NULLni bitta joyda unutsangiz — o'chirilgan vazifa ro'yxatga qaytadi. Ko'rinish (faol_vazifalar) bunga yordam beradi: ilova odatda jadvalga emas, ko'rinishga murojaat qiladi. - Tashqi kalit soft delete'ni bilmaydi.
ON DELETE CASCADEfaqat haqiqiyDELETEda ishlaydi. Savatdagi vazifaning izohlari va teglari joyida qoladi va "teg bo'yicha qidiruv" ularni ham topadi — so'rovda vazifa holatini tekshirish kerak. - 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 CASCADEni 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:
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,CASCADEtarixni ham olib ketardi — tarixning ma'nosi yo'qoladi. Kanon diagrammasida shuning uchun nuqtali chiziq (ER modellash).eski/yangi— butun qatorjsonbda. 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:
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):
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;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?
SELECT amal, count(*) FROM vazifa_tarixi GROUP BY 1 ORDER BY 1; 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:
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; 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:
SELECT
pg_size_pretty(pg_total_relation_size('vazifa_tarixi')) AS tarix,
pg_size_pretty(pg_total_relation_size('vazifalar')) AS vazifalar; 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)vaFOREIGN 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
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;BEGIN
UPDATE 2
id | matn
----+----------------------------
16 | Bozordan meva olish #bozor
29 | Gaz to'lovi #to'lov
(2 rows)
faol
------
4
(1 row)
ROLLBACKSavat — 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
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;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)
ROLLBACKFaqat 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
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;BEGIN
UPDATE 1
id | matn
----+------------------------------------------
8 | Bozordan kartoshka va piyoz olish #bozor
(1 row)
UPDATE 1
tarixda
---------
2
(1 row)
ROLLBACKTiklash 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):
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:
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/deletedAtkabi 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 LOCALva 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_vazifalarko'rinishi. - Tashqi kalit
CASCADEsoft delete'ni bilmaydi; Savat muddatli bo'lsin — keyin haqiqiyDELETE. - Tarix jadvali — tashqi kalitsiz,
jsonbeski/yangi,AFTERtrigger; "kim" —SET LOCALvacurrent_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_settingva "Customized Options", "CREATE TABLE — WITHOUT OVERLAPS / PERIOD" — postgresql.org/docs/18, 2026-10-24 da ko'rildi.
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!