Tarkibga o'tish

12 β€” Keng tarqalgan dizayn naqshlari

⬅️ Oldingi: 11 β€” Yaxlitlik va constraint dizayni Β· 🏠 README Β· Keyingi: 13 β€” Anti-naqshlar: nima qilmaslik kerak ➑️

Bu bobda: real loyihalarda qayta-qayta uchraydigan tayyor yechimlarni β€” dizayn naqshlarini β€” o'rganamiz. Har biri uchun bir xil savol: qanday muammoni yechadi, qanday quriladi va qanday narxni (trade-off) to'laymiz. Soft delete, audit ustunlari, status modellashtirish, slug, lookup jadval, polimorfik bog'lanish (va nega u xavfli), i18n/tarjima, pul + valyuta, manzil β€” hammasini PostgreSQL 18 da haqiqatan ishga tushirib ko'ramiz.


0. Naqsh nima va nega kerak

11-bobgacha biz "qanday to'g'ri jadval qurish" ni o'rgandik: kalit, normalizatsiya, tur, constraint. Bu bob boshqacha β€” bu yerda tayyor retseptlar to'plami. Naqsh (pattern) β€” bu ko'p loyihada takror uchragan muammoning sinovdan o'tgan yechimi.

Hayotiy o'xshatish: oshpaz har safar noldan o'ylab topmaydi β€” "qovurma sous", "xamir", "marinad" kabi tayyor texnikalari bor. Sxema dizaynida ham xuddi shunday: "foydalanuvchini o'chirish kerak, lekin tarix qolsin" β€” bu soft delete naqshi; "kim, qachon o'zgartirdi" β€” bu audit ustunlari naqshi. Naqshni bilsang, g'ildirakni qaytadan ixtiro qilmaysan.

Lekin eng muhim qoida: har naqshning narxi bor. Naqshni "moda shunday" deb emas, balki uning trade-off'ini bilib qo'llash kerak. Shu bob davomida har naqsh uchun muammo β†’ yechim β†’ trade-off uchligini ko'rsatamiz.

Bu bobda barcha misollar PostgreSQL 18 (port 5434) da haqiqatan ishga tushirilgan. Natijalar β€” psql ning real chiqishi.


1. Soft delete β€” "o'chirma, belgila"

1.1 Muammo

Foydalanuvchi hisobini o'chirdik (DELETE). Ertaga ma'lum bo'ldi: uning buyurtmalari hisobotda kerak edi, qo'llab-quvvatlash bo'limi "kim edi bu?" deb so'rayapti, va o'zi "hisobimni tiklang" deyapti. DELETE qaytib bo'lmaydi β€” qator butunlay yo'q.

1.2 Yechim

Qatorni o'chirmaymiz β€” uni o'chirilgan deb belgilaymiz. Buning uchun deleted_at timestamptz ustuni qo'shamiz: NULL = tirik, vaqt = o'chirilgan.

Soft delete naqshi: deleted_at ustuni, qisman UNIQUE indeks va trade-off

CREATE TABLE foydalanuvchi (
    id          bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    email       text NOT NULL,
    ism         text NOT NULL,
    deleted_at  timestamptz          -- NULL = tirik, vaqt = o'chirilgan
);

"O'chirish" endi UPDATE:

UPDATE foydalanuvchi SET deleted_at = now() WHERE email = 'ali@example.com';

Tirik foydalanuvchilarni olish uchun har so'rovga WHERE deleted_at IS NULL qo'shiladi.

1.3 Yashirin tuzoq: UNIQUE buziladi

Agar email ga oddiy UNIQUE qo'ygan bo'lsak β€” soft delete uni buzadi. ali@example.com o'chirildi (lekin qator joyida), endi shu odam qayta ro'yxatdan o'tmoqchi β€” oddiy UNIQUE buni rad etadi, chunki o'lik qatorda hali ham o'sha email turibdi!

Yechim β€” qisman (partial) UNIQUE indeks: yagonalik faqat tirik qatorlar uchun amal qilsin:

CREATE UNIQUE INDEX uq_foydalanuvchi_email_tirik
    ON foydalanuvchi (email)
    WHERE deleted_at IS NULL;

Buni 5434 klasterida sinab ko'ramiz. Ali ni qo'shdik, soft-delete qildik, keyin xuddi shu email bilan yangi tirik foydalanuvchi qo'shdik β€” o'tdi. Ikkinchi tirik dublikat esa bloklandi:

 id |      email      |     ism     | ochirilgan
----+-----------------+-------------+------------
  1 | ali@example.com | Ali         | t
  2 | ali@example.com | Ali (yangi) | f
(2 rows)

NOTICE:  OK: qisman UNIQUE indeks ikkinchi tirik dublikatni bloklab qoldi

Eski qator (id=1) qoldi, yangi tirik qator (id=2) bir xil email bilan o'tdi, lekin uchinchi tirik dublikat qisman indeks tomonidan rad etildi β€” aynan biz xohlagan xulq.

1.4 So'rovni soddalashtirish: VIEW

Har so'rovda WHERE deleted_at IS NULL ni unutib qo'yish β€” eng katta xato. Yechim: tirik qatorlar uchun ko'rinish (view) yarating va kundalik so'rovlarda shuni ishlating:

CREATE VIEW tirik_foydalanuvchi AS
    SELECT * FROM foydalanuvchi WHERE deleted_at IS NULL;

1.5 Trade-off

Afzallik Kamchilik
Qayta tiklash mumkin Har so'rovga WHERE deleted_at IS NULL kerak (unutsang β€” o'lik qator ko'rinadi)
Tarix saqlanadi (audit, hisobot) Oddiy UNIQUE buziladi β€” qisman indeks shart
"Tasodifan o'chirdim" dan himoya ON DELETE CASCADE ishlamaydi (bola jadvallar qo'lda boshqariladi)
FK buzilmaydi Jadval o'lik qatorlardan shishadi; haqiqiy o'chirish (purge) rejasi kerak

Ekspert maslahati: GDPR / "meni unut" talablari uchun soft delete YETARLI EMAS β€” bu yerda ma'lumot haqiqatan o'chirilishi yoki anonimlashtirilishi kerak. Soft delete'ni "tarix kerak" uchun ishlating, "maxfiylik" uchun emas. Ko'pincha aralash strategiya to'g'ri: soft delete + N kundan keyin fon ishi (background job) bilan haqiqiy purge.


2. Audit ustunlari β€” "qachon va kim"

2.1 Muammo

"Bu qator qachon yaratilgan? Oxirgi marta qachon o'zgardi? Kim o'zgartirdi?" β€” bu savollar ertami-kechmi har loyihada chiqadi (debug, qo'llab-quvvatlash, tergov).

2.2 Yechim: created_at / updated_at

Har jadvalga ikki vaqt ustuni qo'shing. created_at ni DEFAULT now() hal qiladi. updated_at ni esa trigger bilan avtomatlashtiramiz β€” ilova kodi uni yangilashni unutsa ham baza o'zi yangilaydi.

Audit ustunlari trigger bilan va status modellashtirish: enum vs lookup jadval

CREATE TABLE maqola (
    id          bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    sarlavha    text NOT NULL,
    matn        text,
    created_at  timestamptz NOT NULL DEFAULT now(),
    updated_at  timestamptz NOT NULL DEFAULT now()
);

CREATE OR REPLACE FUNCTION trg_updated_at()
RETURNS trigger AS $$
BEGIN
    NEW.updated_at := now();
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER maqola_updated_at
    BEFORE UPDATE ON maqola
    FOR EACH ROW
    EXECUTE FUNCTION trg_updated_at();

Trigger UPDATE'da updated_at ni haqiqatan yangilashini ko'ramiz. INSERT qildik, 1 soniya kutdik (pg_sleep(1)), keyin UPDATE qildik:

 id |          created_at           |          updated_at           | updated_yangilandi | farq_sekund
----+-------------------------------+-------------------------------+--------------------+-------------
  1 | 2026-06-13 22:12:31.723071+05 | 2026-06-13 22:12:32.737757+05 | t                  |        1.01
(1 row)

updated_at created_at dan 1.01 soniya kechroq β€” trigger UPDATE paytida vaqtni haqiqatan yangiladi. (Bu funksiya bitta β€” uni loyihadagi barcha jadvallarga bog'lab ishlatish mumkin.)

2.3 created_by / updated_by β€” "kim"

Vaqtdan tashqari "kim" ham kerak bo'lsa β€” foydalanuvchiga FK qo'shamiz:

CREATE TABLE hujjat (
    id          bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    sarlavha    text NOT NULL,
    created_at  timestamptz NOT NULL DEFAULT now(),
    updated_at  timestamptz NOT NULL DEFAULT now(),
    created_by  bigint REFERENCES app_user(id),
    updated_by  bigint REFERENCES app_user(id)
);

created_by / updated_by qiymatini odatda ilova beradi (sessiyadagi joriy foydalanuvchi). Tekshiruvda admin yaratdi, editor tahrirladi:

 id | yaratgan | tahrirlagan
----+----------+-------------
  1 | admin    | editor
(1 row)

2.4 Trade-off va chegara

Bu naqsh bir bosqichli audit beradi: faqat OXIRGI o'zgarish (kim, qachon) saqlanadi. To'liq tarix kerak bo'lsa β€” har versiyani saqlash, "kim nimani nimaga o'zgartirdi" β€” bu temporal / history table mavzusi, uni 18-bobda ko'ramiz. Audit ustunlari β€” arzon va yengil; to'liq tarix β€” qimmatroq, lekin to'liqroq.


3. Status / holatni modellashtirish

3.1 Muammo

Buyurtmaning holati bor: yangi β†’ tolangan β†’ jonatildi β†’ yetkazildi, yoki bekor. Buni qanday saqlash kerak? Uch yondashuv bor, har birining o'rni bor.

3.2 Variant A β€” ENUM tip

CREATE TYPE buyurtma_status AS ENUM
    ('yangi','tolangan','jonatildi','yetkazildi','bekor');

CREATE TABLE buyurtma_enum (
    id     bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    status buyurtma_status NOT NULL DEFAULT 'yangi'
);

Enum ixcham va tez. Noto'g'ri qiymatni baza darhol rad etadi:

NOTICE:  OK: enum noto'g'ri qiymatni bloklab qoldi

Yangi qiymat qo'shish mumkin (ALTER TYPE ... ADD VALUE 'qaytarildi'), va u haqiqatan qo'shildi:

 enumlabel
------------
 yangi
 tolangan
 jonatildi
 yetkazildi
 bekor
 qaytarildi
(6 rows)

Lekin enum'dan qiymatni o'chirish og'riqli (alohida amal yo'q), va status uchun qo'shimcha ma'lumot β€” ko'rsatiladigan nom, saralash tartibi, rang β€” saqlay olmaysiz.

3.3 Variant B β€” lookup jadval + FK (tavsiya etiladi)

Agar statusga meta-ma'lumot kerak bo'lsa (nom, tartib, faollik) β€” lookup jadval to'g'riroq:

CREATE TABLE buyurtma_holati (
    kod    text PRIMARY KEY,          -- 'yangi', 'tolangan', ...
    nom    text NOT NULL,             -- UI da ko'rsatiladigan nom
    tartib int  NOT NULL              -- saralash uchun
);

INSERT INTO buyurtma_holati (kod, nom, tartib) VALUES
    ('yangi',     'Yangi',          1),
    ('tolangan',  'To''langan',     2),
    ('jonatildi', 'Jo''natildi',    3),
    ('yetkazildi','Yetkazildi',     4),
    ('bekor',     'Bekor qilingan', 9);

CREATE TABLE buyurtma (
    id         bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    holat_kod  text NOT NULL REFERENCES buyurtma_holati(kod)
);

JOIN bilan ko'rsatish nomini olamiz, FK esa noto'g'ri kodni bloklaydi:

 id | holat_kod |    nom
----+-----------+-----------
  1 | yangi     | Yangi
  2 | tolangan  | To'langan
  3 | yangi     | Yangi
(3 rows)

NOTICE:  OK: lookup FK noto'g'ri holat kodini bloklab qoldi

enum yoki lookup? Qiymatlar barqaror, kam va faqat ichki mantiq uchun bo'lsa β€” enum (ixcham, tez). Qiymatlar UI da ko'rsatiladigan nom, tartib, rang yoki "faolmi" bayrog'iga muhtoj bo'lsa, yoki admin ularni o'zgartirishi kerak bo'lsa β€” lookup jadval. Shubha bo'lsa β€” lookup jadval, chunki uni kengaytirish osonroq.

3.4 Variant C β€” holat mashinasi (state machine)

Yuqoridagilar "hozir qaysi holat" ni saqlaydi, lekin qaysi o'tish ruxsat etilgan ni emas. yangi'dan to'g'ridan-to'g'ri yetkazildi'ga sakrash mantiqsiz. Ruxsat etilgan o'tishlarni majburlash uchun:

  • ruxsatlar jadvali holat_otish(qaysidan, qaysiga) + INSERT/UPDATE oldidan tekshiruvchi trigger, yoki
  • oddiy hollarda CHECK / trigger bilan eski va yangi qiymat juftligini tekshirish.

Bu β€” to'liq mavzu; bu yerda uni naqsh sifatida bilib qo'yish kifoya. Holatlar ko'p va o'tish qoidalari murakkab bo'lsa, state machine'ni alohida modellashtiring.


4. Slug β€” tabiiy URL kaliti

4.1 Muammo

/maqola/4217 chiroyli ham, SEO uchun foydali ham emas. Foydalanuvchi va qidiruv tizimi /maqola/postgresql-18-yangiliklari ni afzal ko'radi. Lekin sarlavha o'zgaruvchan va takrorlanishi mumkin β€” uni to'g'ridan-to'g'ri kalit qilib bo'lmaydi.

4.2 Yechim

Sarlavhadan slug β€” URL-xavfsiz, barqaror identifikator hosil qilamiz va uni UNIQUE qilamiz. ID (surrogate kalit) ham qoladi β€” slug faqat tashqi (URL) kalit:

CREATE TABLE blog_post (
    id        bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    slug      text NOT NULL,
    sarlavha  text NOT NULL,
    CONSTRAINT uq_blog_slug UNIQUE (slug),
    CONSTRAINT slug_format CHECK (slug ~ '^[a-z0-9]+(-[a-z0-9]+)*$')
);

CHECK slug formatini majburlaydi: faqat kichik harf, raqam va -. Yaroqsiz slug bloklandi:

NOTICE:  OK: slug format CHECK yaroqsiz slug ni bloklab qoldi

4.3 Slug generatsiyasi

Sarlavhadan slug yasash uchun: kichik harfga o'tkazish + harf/raqam bo'lmagan hamma narsani - ga almashtirish + chetdagi - ni olib tashlash:

SELECT regexp_replace(
         regexp_replace(lower('  PostgreSQL 18: Dizayn Naqshlari!  '), '[^a-z0-9]+', '-', 'g'),
         '(^-|-$)', '', 'g'
       ) AS slug;
              slug
--------------------------------
 postgresql-18-dizayn-naqshlari

4.4 Trade-off

  • Slug barqaror bo'lishi kerak: maqola sarlavhasi o'zgarsa, slug avtomatik o'zgarmasligi lozim (aks holda eski havolalar buziladi). Odatda slug bir marta yaratiladi va qotib qoladi; o'zgartirsa β€” eski slug'dan yangisiga 301 redirect kerak.
  • Takrorlanish: ikki maqola bir xil sarlavha bilan β€” UNIQUE buzadi. Yechim: slug oxiriga raqam qo'shish (dizayn-naqshlari-2).
  • Slug β€” tabiiy kalit (6-bobdagi natural vs surrogate), shuning uchun uni PK QILMANG; PK surrogate (id) qolsin, slug esa UNIQUE tashqi kalit bo'lsin.

5. Lookup / reference jadval (kod-qiymat)

3-bo'limda lookup jadvalni status uchun ko'rdik, lekin bu undan keng naqsh. Mamlakatlar, valyutalar, kategoriyalar, ruxsat turlari β€” har qanday cheklangan, nomli qiymatlar to'plami lookup jadvalga tushadi:

CREATE TABLE valyuta (
    kod    char(3) PRIMARY KEY,    -- ISO 4217: 'UZS', 'USD'
    nom    text NOT NULL,
    onlik  smallint NOT NULL       -- nechta onlik raqam (UZS=2, JPY=0)
);

Nega yaxshi: noto'g'ri kod FK bilan bloklanadi, qiymatga meta qo'shsa bo'ladi (nom, tartib, faollik), bitta joyda boshqariladi, va kelajakda til/tarjima ham qo'shsa bo'ladi.

Kalit tanlash: lookup jadvalda ma'noli matnli kod ('UZS', 'yangi') ko'pincha integer surrogate'dan qulayroq β€” holat_kod = 'yangi' o'qishda JOIN'siz ham tushunarli. Kod barqaror va qisqa bo'lsa, uni PK qilish o'rinli. Faqat juda katta/o'zgaruvchan lug'atlarda surrogate integer afzal.


6. Polimorfik bog'lanish β€” va nega u xavfli

6.1 Muammo va "vasvasa"

izoh (comment) ham maqolaga, ham videoga, ertaga rasmga tegishli bo'lishi mumkin. Vasvasa: bitta izoh jadvali, ikki ustun β€” parent_turi ('maqola'/'video') va parent_id. Bitta jadval hamma narsaga ishora qiladi β€” chiroyli ko'rinadi.

-- ❌ ANTI-NAQSH: polimorfik FK
CREATE TABLE izoh_polimorfik (
    id          bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    parent_turi text   NOT NULL,   -- 'maqola' yoki 'video'
    parent_id   bigint NOT NULL,   -- qaysidir jadval id si β€” FK YO'Q!
    matn        text   NOT NULL
);

6.2 Nega xavfli β€” baza yaxlitlikni kafolatlay olmaydi

Muammo bitta jumlada: parent_id ga FK qo'yib bo'lmaydi, chunki u goh maqola.id, goh video.id ga ishora qiladi β€” bitta ustun ikki jadvalga FK bo'la olmaydi. Natijada baza yaxlitlikni yo'qotadi.

Polimorfik bog'lanish xavfi va xavfsiz muqobillar: alohida FK va super-jadval

Buni 5434 da ko'rsatdik. Mavjud bo'lmagan parent_id = 999 bilan izoh β€” baza TO'XTATMAYDI, yetim qator kirib ketadi:

 id | parent_turi | parent_id |              matn
----+-------------+-----------+--------------------------------
  2 | maqola      |       999 | Mavjud bo'lmagan maqolaga izoh
(1 row)

Yana yomoni: maqolani o'chirsak, uning izohlari yetim qolib ketadi (ON DELETE CASCADE yo'q):

 id | parent_turi | parent_id | parent_yoq
----+-------------+-----------+------------
  1 | maqola      |         1 | t
  2 | maqola      |       999 | t
(2 rows)

Qo'shimcha og'riqlar: har JOIN'da parent_turi bo'yicha CASE/shart kerak, indekslar samarasiz, va turli jadvallarning id turlari bir ustunga sig'maydi (biri bigint, biri uuid bo'lsa β€” tugadi).

6.3 Xavfsiz muqobil A β€” alohida nullable FK (exclusive arc)

Tur soni kam va barqaror bo'lsa (2-4 ta): har tur uchun alohida nullable FK + CHECK bilan "aniq bittasi to'ldirilsin":

CREATE TABLE izoh_xavfsiz (
    id         bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    maqola_id  bigint REFERENCES maqola(id) ON DELETE CASCADE,
    video_id   bigint REFERENCES video(id)  ON DELETE CASCADE,
    matn       text NOT NULL,
    CONSTRAINT bitta_ota CHECK (
        (maqola_id IS NOT NULL)::int + (video_id IS NOT NULL)::int = 1
    )
);

Endi baza hammasini kafolatlaydi. Yetim FK bloklanadi, ikkala (yoki hech qaysi) ota bloklanadi, va CASCADE ishlaydi:

NOTICE:  OK: FK yetim maqola_id ni bloklab qoldi
NOTICE:  OK: CHECK ikkala ota ni bloklab qoldi (aniq bitta bo'lsin)
== CASCADE ishladi: maqola o'chdi, izoh ham ketdi ==
 qolgan_maqola_izoh
--------------------
                  0

6.4 Xavfsiz muqobil B β€” super-jadval (ota entity)

Tur soni ko'p yoki o'sib boradigan bo'lsa, har turga alohida ustun cho'zilib ketadi. Bunda umumiy ota jadval (kontent) yarating; maqola va video uning id siga FK bilan ishora qilsin (shared primary key), izoh esa BITTA haqiqiy FK bilan kontentga bog'lansin:

CREATE TABLE kontent (id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, turi text NOT NULL);
CREATE TABLE maqola  (id bigint PRIMARY KEY REFERENCES kontent(id), sarlavha text);
CREATE TABLE video   (id bigint PRIMARY KEY REFERENCES kontent(id), davomiyligi int);
CREATE TABLE izoh    (id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
                      kontent_id bigint NOT NULL REFERENCES kontent(id) ON DELETE CASCADE,
                      matn text NOT NULL);

Oltin qoida: agar bog'lanishga FK qo'yib bo'lmasa β€” bu dizayn xatosining alomati. To'xtab, qayta o'ylang. Polimorfik FK 13-bobdagi anti-naqshlar ro'yxatida ham qaytadi.


7. Tarjima / i18n jadvali

7.1 Muammo

Ilova ko'p tilli: mahsulot nomi va tavsifi uz, ru, en da kerak. Har til uchun ustun (nom_uz, nom_ru, nom_en) qo'shish β€” yangi til qo'shilganda jadvalni ALTER qilishni talab qiladi va kengaytirilmaydi (bu jaywalking'ga yaqin anti-naqsh).

7.2 Yechim β€” alohida tarjima jadvali

Tilga bog'liq matnlarni alohida jadvalga ajratamiz; kalit (mahsulot_id, til):

CREATE TABLE mahsulot (
    id    bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    narx  numeric(12,2) NOT NULL
);
CREATE TABLE mahsulot_tarjima (
    mahsulot_id bigint  NOT NULL REFERENCES mahsulot(id) ON DELETE CASCADE,
    til         char(2) NOT NULL,           -- 'uz','ru','en'
    nom         text    NOT NULL,
    tavsif      text,
    PRIMARY KEY (mahsulot_id, til)
);

Tilga bog'liq bo'lmagan maydonlar (narx, id) asosiy jadvalda; tilga bog'liqlari tarjima jadvalida. Yangi til = yangi qator, ALTER kerak emas.

7.3 Fallback (zaxira til)

So'ralgan tilda tarjima yo'q bo'lsa, asosiy tilga (uz) qaytamiz β€” COALESCE bilan:

SELECT m.id, COALESCE(t_ru.nom, t_uz.nom) AS nom_korsatiladi
FROM mahsulot m
LEFT JOIN mahsulot_tarjima t_ru ON t_ru.mahsulot_id = m.id AND t_ru.til = 'ru'
LEFT JOIN mahsulot_tarjima t_uz ON t_uz.mahsulot_id = m.id AND t_uz.til = 'uz';
 id | nom_korsatiladi
----+-----------------
  1 | Chaynik

Eslatma til kodlari haqida: bu yerda namuna uchun char(2) (ISO 639-1: uz, ru, en) ishlatildi. To'liq lokal kerak bo'lsa (pt-BR, zh-Hant) β€” text va til jadvaliga FK qiling. Kirill yozuvini SAQLASH kerak bo'lsa, u ma'lumotning o'zida (qiymatda) bo'ladi β€” sxema/identifikator esa lotin qoladi.


8. Pul va valyuta

8.1 Muammo β€” float'da pul = falokat

Pulni float/real da saqlash β€” klassik xato. Suzuvchi nuqta ikkilik tizimda 0.1 ni aniq saqlay olmaydi. 5434 da ko'rsatdik:

 numeric_jami |     float_jami      | float_teng_03
--------------+---------------------+---------------
          0.3 | 0.30000000000000004 | f

numeric da 0.1 + 0.2 = 0.3 aniq; float da esa 0.30000000000000004 β€” va = 0.3 tekshiruvi false qaytaradi. Pul hisobida bu xatolar to'planib, balansni buzadi.

8.2 Yechim β€” numeric + valyuta kodi

Pul har doim numeric(p, s) da, va valyuta kodi bilan birga saqlanadi (summa o'zi yetarli emas β€” "150000" so'mmi, dollarmu?):

CREATE TABLE tolov (
    id           bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    summa        numeric(14,2) NOT NULL CHECK (summa >= 0),  -- float EMAS!
    valyuta_kod  char(3) NOT NULL REFERENCES valyuta(kod)
);

Valyuta bo'yicha jami β€” aniq:

 valyuta_kod |     nom      | soni |   jami
-------------+--------------+------+-----------
 USD         | AQSH dollari |    1 |     19.99
 UZS         | O'zbek so'mi |    2 | 400000.50
(2 rows)

8.3 Dizayn tafsilotlari

  • Onlik raqamlar soni valyutaga bog'liq: UZS/USD = 2, JPY/KRW = 0. Shuning uchun lookup valyuta jadvalida onlik ustuni bor. Universal tizimda summani eng kichik birlikda (tiyin/sent) butun son sifatida saqlash ham keng tarqalgan yondashuv (bigint, 19.99 USD β†’ 1999).
  • Hech qachon turli valyutalarni qo'shmang: sum(summa) ni doim GROUP BY valyuta_kod bilan. UZS va USD ni jamlash β€” ma'nosiz.
  • Kurs (konvertatsiya) alohida masala: kurslar vaqtga bog'liq, shuning uchun tranzaksiya paytidagi kursni snapshot qilib saqlang (qarang: 1-bobdagi narx snapshot mantiqi).

9. Manzil modellashtirish

9.1 Muammo

Foydalanuvchining bir nechta manzili bo'lishi mumkin: uy, ish, yetkazish. Manzilni foydalanuvchi jadvalidagi ustunlarga (kocha, shahar...) tiqish β€” faqat bitta manzilga yetadi va N:1 talabni buzadi.

9.2 Yechim β€” alohida manzil jadvali (1:N)

CREATE TABLE manzil (
    id          bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    mijoz_id    bigint NOT NULL REFERENCES mijoz(id) ON DELETE CASCADE,
    turi        text   NOT NULL CHECK (turi IN ('uy','ish','yetkazish')),
    mamlakat    char(2) NOT NULL,          -- ISO 3166-1 alpha-2
    viloyat     text,
    shahar      text   NOT NULL,
    kocha       text   NOT NULL,
    pochta_kod  text,
    is_asosiy   boolean NOT NULL DEFAULT false
);

-- Har mijozda faqat BITTA asosiy manzil (qisman unique):
CREATE UNIQUE INDEX uq_mijoz_asosiy_manzil
    ON manzil (mijoz_id) WHERE is_asosiy;

"Har mijozda bitta asosiy manzil" qoidasini yana qisman UNIQUE indeks majburlaydi. Ikkinchi asosiy manzil bloklandi:

NOTICE:  OK: qisman unique ikkinchi asosiy manzilni bloklab qoldi

9.3 Dizayn maslahatlari

  • Manzil "tekis matn" yoki "tuzilgan"? Mamlakatlar manzil formati turlicha (AQSH ZIP, Yaponiya teskari tartib). Universal tizimda ko'pincha bir nechta erkin qator1, qator2 + mamlakat + pochta_kod to'g'riroq, qattiq viloyat/shahar/kocha strukturasidan ko'ra. Faqat bitta mamlakat ichida ishlasangiz, tuzilgan ustunlar qulay.
  • Yetkazish manzili snapshot: buyurtma yetkazib bo'lingach, mijoz manzilini o'zgartirsa β€” eski buyurtma qayerga ketganini bilish kerak. Shuning uchun buyurtmaga yetkazish manzilini nusxa (snapshot) qilib saqlang, FK bilan tirik manzilga bog'lab emas.

10. Daraxt strukturalari β€” bu yerda emas

"Kategoriya ichida subkategoriya", "xodim-boshliq", "izohga javob" kabi ierarxik (daraxt) ma'lumot ham juda keng naqsh. Eng sodda ko'rinishi β€” parent_id ustuni (adjacency list). Lekin bu o'zining trade-off'lari (chuqurlikni so'rash, butun shoxni olish) va boshqa usullari (materialized path, nested set, closure table) bilan alohida katta mavzu.

Shuning uchun bu yerda chuqurlashtirmaymiz β€” daraxt va graf strukturalarini to'liq 17-bobda ko'ramiz. Bu yerda faqat bilib qo'ying: parent_id β€” naqsh, lekin "naive tree" (faqat parent_id, recursive so'rovsiz) 13-bobda anti-naqsh sifatida ham keladi.


11. Xulosa

Naqshlar β€” tayyor retseptlar, lekin har biri narx bilan keladi:

  • Soft delete β€” tarix kerak bo'lsa; lekin qisman UNIQUE indeks va WHERE deleted_at IS NULL majburiyatini unutmang.
  • Audit ustunlari β€” arzon "qachon/kim"; updated_at ni trigger bilan avtomatlashtir. To'liq tarix uchun 18-bob.
  • Status β€” barqaror/ichki bo'lsa enum, meta kerak bo'lsa lookup jadval; murakkab o'tishlar uchun state machine.
  • Slug β€” barqaror URL kaliti; PK emas, UNIQUE tashqi kalit.
  • Lookup jadval β€” cheklangan nomli qiymatlar uchun FK bilan, ko'pincha matnli kod bilan.
  • Polimorfik FK β€” XAVFLI (FK yo'qoladi); exclusive arc yoki super-jadval bilan almashtir.
  • i18n β€” tilga bog'liq matn alohida jadvalda, (id, til) kaliti, COALESCE fallback.
  • Pul β€” har doim numeric + valyuta kodi; float'da pul yo'q.
  • Manzil β€” 1:N alohida jadval; asosiy manzil qisman UNIQUE; yetkazish manzili snapshot.

Keyingi bobda esa teskari tomon β€” aynan shu naqshlarning suiiste'moli va boshqa keng tarqalgan anti-naqshlarni (nima qilmaslik kerak) ko'rib chiqamiz.


Mashqlar

Oson

  1. Soft delete qo'shish. Quyidagi jadvalga soft delete naqshini qo'shing va tirik qatorlar uchun email ni yagona qiling: xodim(id, email text, ism text). Qaysi turdagi indeks kerak va nega?

  2. updated_at unutilishi. Bir jamoa updated_at ustunini qo'ydi, lekin uni faqat ilova kodida (UPDATE ... SET updated_at = now()) yangilaydi, trigger qo'ymadi. Bu yondashuvning xavfi nima? Bir misol keltiring qachon updated_at noto'g'ri bo'lib qoladi.

  3. enum yoki lookup? Quyidagilarning har biri uchun enum yoki lookup jadval tanlang va bir jumla bilan asoslang: (a) jinsi (erkak/ayol/korsatilmagan), (b) buyurtma holati (UI da nom va rang bilan), (c) hafta kuni.

  4. Float'da pul. mahsulot(id, narx real) jadvali bor. Bu nega xato? To'g'ri tur nima? Bir misol bilan (qiymat) ko'rsating qachon real xato natija beradi.

  5. Slug PK emas. Bir dasturchi blog_post da slug ni PRIMARY KEY qildi (id yo'q). Bu nega yomon g'oya? Kamida ikki sabab keltiring.

O'rta

  1. Soft delete + FK muammosi. buyurtma.mijoz_id β†’ mijoz(id) FK bor va mijoz soft delete ishlatadi. Mijozni soft-delete qilganda nima sodir bo'ladi (qator hali turibdi)? Bu holatda ON DELETE CASCADE ishlaydimi? Bu vaziyatni qanday boshqarish kerak?

  2. Audit ustunlari to'plamini loyihala. hujjat jadvaliga to'liq audit qo'shing: created_at, updated_at, created_by, updated_by. updated_at uchun trigger yozing. created_at va created_by ni UPDATE da o'zgarmas (immutable) qilish uchun nima qilasiz?

  3. i18n ni qayta loyihala. Quyidagi jadval ko'p tilli kontent uchun ishlatilgan: mahsulot(id, nom_uz, nom_ru, nom_en, tavsif_uz, tavsif_ru, tavsif_en, narx). Buni i18n naqshiga qayta loyihalang. Yangi til (kz) qo'shilganda eski va yangi dizaynda nima farq?

  4. Manzil β€” bitta asosiy. Mijozning ko'p manzili bor, lekin aniq bittasi "asosiy" bo'lishi kerak. Buni baza darajasida qanday majburlaysiz? DDL yozing va nega ilova kodiga ishonmaslik kerakligini tushuntiring.

  5. Status o'tishini cheklash. buyurtma holati yangi β†’ tolangan β†’ yetkazildi tartibida bo'lishi kerak, sakrash mumkin emas. Buni qanday majburlaysiz? Bir necha yondashuvni (CHECK, trigger, o'tishlar jadvali) sanab, qaysi biri qachon to'g'ri ekanini ayting.

Qiyin

  1. Polimorfikni xavfsiz qayta loyihala. Quyidagi anti-naqsh berilgan: like(id, user_id, target_turi text, target_id bigint) β€” target_turi 'maqola'/'izoh'/'foto' bo'lishi mumkin, FK yo'q. (a) Bu dizaynning aniq xavflarini sanang. (b) Exclusive arc yondashuvi bilan qayta loyihalang (DDL + CHECK). (c) Agar turlar soni 10 taga yetsa, qaysi yondashuv to'g'ri va nega?

  2. Soft delete + unique + tarix. Foydalanuvchi email ni o'zgartirib, keyin hisobini o'chiradi, keyin yana shu email bilan yangi hisob ochadi β€” bularning hammasi vaqt o'tishi bilan. Tirik foydalanuvchilar orasida email yagona bo'lsin, lekin barcha eski (o'chirilgan) qatorlar tarix sifatida qolsin. To'liq DDL yozing va 3-4 INSERT/UPDATE bilan stsenariyni ko'rsating.

  3. Ko'p valyutali hamyon. Foydalanuvchining bir nechta valyutadagi balansi bo'lishi mumkin (UZS, USD, ...). Sxemani loyihalang: hamyon, valyuta jadvallari. "Bir foydalanuvchida har valyutadan bitta hamyon" qoidasini majburlang. Balansni qaysi turda saqlaysiz va nega? Turli valyutalarni qo'shib yuborishdan qanday himoyalanasiz?

  4. To'liq mahsulot katalogi naqshlari. Marketplace mahsulot katalogini loyihalang, bir nechta naqshni birlashtirib: (a) soft delete (mahsulot "arxivlandi", lekin eski buyurtmalarda ko'rinadi), (b) i18n (nom/tavsif uz/ru/en), (c) status (lookup jadval: qoralama/faol/arxiv), (d) pul (numeric + valyuta), (e) audit (created/updated at+by). To'liq DDL yozing va har naqsh qaysi muammoni yechayotganini bir jumla bilan izohlang.


Yechimlar

Yechim β€” 1

deleted_at timestamptz ustuni va qisman (partial) UNIQUE indeks kerak β€” oddiy UNIQUE emas, chunki o'chirilgan qatorda eski email qolib, qayta ro'yxatdan o'tishni bloklab qo'yadi.

ALTER TABLE xodim ADD COLUMN deleted_at timestamptz;
CREATE UNIQUE INDEX uq_xodim_email_tirik
    ON xodim (email) WHERE deleted_at IS NULL;

Qisman indeks yagonalikni faqat tirik (deleted_at IS NULL) qatorlarga qo'llaydi.

Yechim β€” 2

Xavf: ilova kodida updated_at ni yangilash unutilishi mumkin. Bir necha joydan UPDATE qilinsa (admin paneli, API, migratsiya skripti, qo'lda psql UPDATE), kimdir updated_at = now() qo'shishni o'tkazib yuboradi β€” natijada updated_at haqiqiy o'zgarish vaqtini ko'rsatmaydi.

Misol: ma'lumotni to'g'ridan-to'g'ri psql dan UPDATE maqola SET matn = '...' WHERE id = 1; qilsangiz β€” updated_at eski qiymatda qoladi, garchi qator o'zgargan bo'lsa ham. Trigger (BEFORE UPDATE) buni hal qiladi: kim qaysi yo'l bilan UPDATE qilmasin, baza updated_at ni avtomatik yangilaydi.

Yechim β€” 3
  • (a) jinsi β€” enum (yoki CHECK IN (...)). Qiymatlar barqaror, kam, meta kerak emas.
  • (b) buyurtma holati (nom + rang bilan) β€” lookup jadval. UI uchun nom, rang, tartib saqlash kerak; bu enum'ga sig'maydi.
  • (c) hafta kuni β€” enum yoki shunchaki CHECK (kun BETWEEN 1 AND 7). Qat'iy, o'zgarmas; lookup jadval ortiqcha (agar lokalizatsiya kerak bo'lmasa). Agar kun nomini ko'p tilda chiqarish kerak bo'lsa β€” lookup + i18n.
Yechim β€” 4

real (float) pulni aniq saqlay olmaydi β€” ikkilik suzuvchi nuqta ko'p o'nlik kasrni yaxlitlaydi. Xatolar to'planib balansni buzadi.

To'g'ri tur: numeric(p, s) (masalan numeric(12, 2)).

Misol (5434 da tasdiqlangan): 0.1 + 0.2 float da 0.30000000000000004 beradi, numeric da esa aniq 0.3. 1000 ta tranzaksiya yig'ilganda bu xato sezilarli summaga aylanadi va summa = 0.3 kabi tekshiruvlar false qaytaradi.

ALTER TABLE mahsulot ALTER COLUMN narx TYPE numeric(12,2);
Yechim β€” 5
  1. Slug o'zgaruvchan: slug β€” tabiiy kalit, vaqt o'tib o'zgarishi mumkin (sarlavha tahrir qilinsa). PK o'zgarsa β€” unga ishora qilgan barcha FK lar ham o'zgarishi kerak (yoki buziladi). Surrogate id esa hech qachon o'zgarmaydi.
  2. FK og'irligi: text PK bola jadvallarda takrorlanadi, indeks kattaroq va sekinroq bo'ladi (bigint ga nisbatan).
  3. (Qo'shimcha) takror sarlavhalar slug to'qnashuviga olib keladi; surrogate kalit bunga befarq.

To'g'ri yondashuv: id (surrogate) β€” PK; slug β€” UNIQUE tashqi kalit.

Yechim β€” 6

Mijoz soft-delete qilinganda qator JOIDA QOLADI (deleted_at to'ldiriladi, DELETE bo'lmaydi). Demak:

  • ON DELETE CASCADE ishlamaydi β€” chunki haqiqiy DELETE yuz bermayapti. Buyurtmalar daxlsiz qoladi (ko'pincha bu to'g'ri β€” buyurtma tarixi yo'qolmasligi kerak).
  • FK ham buzilmaydi, chunki ota qator hali mavjud.

Boshqarish: bu aslida soft delete'ning afzalligi β€” bog'liq ma'lumot saqlanadi. Lekin "tirik mijozlar" so'rovlarida WHERE m.deleted_at IS NULL ni unutmaslik kerak. Agar o'chirilgan mijozning buyurtmalarini ham yashirish kerak bo'lsa, buni JOIN shartida hisobga oling. Mijozni HAQIQATAN o'chirish (purge) kerak bo'lsa β€” avval bog'liq buyurtmalar bilan nima qilishni hal qiling (arxivlash/anonimlashtirish).

Yechim β€” 7
CREATE TABLE hujjat (
    id          bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    sarlavha    text NOT NULL,
    created_at  timestamptz NOT NULL DEFAULT now(),
    updated_at  timestamptz NOT NULL DEFAULT now(),
    created_by  bigint REFERENCES app_user(id),
    updated_by  bigint REFERENCES app_user(id)
);

CREATE OR REPLACE FUNCTION trg_audit_update()
RETURNS trigger AS $$
BEGIN
    NEW.updated_at := now();
    -- created_at / created_by ni o'zgarmas qilamiz:
    NEW.created_at := OLD.created_at;
    NEW.created_by := OLD.created_by;
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER hujjat_audit
    BEFORE UPDATE ON hujjat
    FOR EACH ROW EXECUTE FUNCTION trg_audit_update();

created_at/created_by ni immutable qilish: trigger ichida NEW.created_at := OLD.created_at β€” UPDATE ularni o'zgartirmaydi, kim urinmasin. updated_by ni odatda ilova beradi (joriy foydalanuvchi).

Yechim β€” 8
CREATE TABLE mahsulot (
    id    bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    narx  numeric(12,2) NOT NULL
);
CREATE TABLE mahsulot_tarjima (
    mahsulot_id bigint  NOT NULL REFERENCES mahsulot(id) ON DELETE CASCADE,
    til         char(2) NOT NULL,
    nom         text    NOT NULL,
    tavsif      text,
    PRIMARY KEY (mahsulot_id, til)
);

Farq: eski (ustunli) dizaynda yangi til kz qo'shish uchun ALTER TABLE ... ADD COLUMN nom_kz, ADD COLUMN tavsif_kz kerak β€” sxema o'zgaradi, migratsiya yoziladi, kod yangilanadi. Yangi (i18n) dizaynda esa atigi yangi qatorlar qo'shiladi (INSERT ... (mahsulot_id, 'kz', ...)), sxema o'zgarmaydi. i18n dizayn ochiq tugagan tillar to'plamiga moslashadi.

Yechim β€” 9
CREATE UNIQUE INDEX uq_mijoz_asosiy_manzil
    ON manzil (mijoz_id) WHERE is_asosiy;

Qisman UNIQUE indeks: mijoz_id faqat is_asosiy = true qatorlar ichida yagona β€” ya'ni har mijozda eng ko'pi bitta asosiy manzil.

Nega ilova kodiga ishonmaslik kerak: ilova "yangi asosiyni belgilashdan oldin eskisini bekor qil" mantig'ini bajarmasa yoki ikki so'rov bir vaqtda kelsa (race condition), ikki asosiy manzil paydo bo'lib qoladi. Baza darajasidagi qisman indeks β€” bu xatoning oldini fizik ravishda oladi (11-bob: "baza himoyasi β€” oxirgi qal'a").

Yechim β€” 10

Yondashuvlar:

  • CHECK β€” yetarli emas: CHECK faqat joriy qatorning ustunlarini ko'radi, eski qiymatni (OLD) bilmaydi, shuning uchun "yangidan tolanganga o'tish" mumkinmi degan o'tish qoidasini tekshira olmaydi.
  • BEFORE UPDATE trigger β€” to'g'ri yo'l oddiy hollarda: OLD.holat va NEW.holat juftligini ruxsat etilgan o'tishlar bilan solishtiradi.
CREATE OR REPLACE FUNCTION trg_holat_otish()
RETURNS trigger AS $$
BEGIN
  IF NOT (
       (OLD.holat = 'yangi'    AND NEW.holat IN ('tolangan','bekor')) OR
       (OLD.holat = 'tolangan' AND NEW.holat IN ('yetkazildi','bekor')) OR
       (OLD.holat = NEW.holat)
     ) THEN
    RAISE EXCEPTION 'Ruxsat etilmagan o''tish: % -> %', OLD.holat, NEW.holat;
  END IF;
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;
  • O'tishlar jadvali (holat_otish(qaysidan, qaysiga)) + trigger β€” eng moslashuvchan: o'tish qoidalari ma'lumot bo'lib qoladi (kodga emas), admin ularni o'zgartira oladi. Holatlar ko'p va qoidalar tez-tez o'zgaradigan bo'lsa shu to'g'ri.
Yechim β€” 11

(a) Xavflar: target_id ga FK yo'q β€” yetim like'lar (mavjud bo'lmagan maqolaga like) kirib ketadi; maqola o'chsa like yetim qoladi (CASCADE yo'q); JOIN'da target_turi bo'yicha shart kerak; turli jadval id turlari to'qnashishi mumkin; UNIQUE ("bir user bir narsaga bir marta like") murakkablashadi.

(b) Exclusive arc:

CREATE TABLE like_xavfsiz (
    id        bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    user_id   bigint NOT NULL REFERENCES app_user(id) ON DELETE CASCADE,
    maqola_id bigint REFERENCES maqola(id) ON DELETE CASCADE,
    izoh_id   bigint REFERENCES izoh(id)   ON DELETE CASCADE,
    foto_id   bigint REFERENCES foto(id)   ON DELETE CASCADE,
    CHECK (
       (maqola_id IS NOT NULL)::int
     + (izoh_id   IS NOT NULL)::int
     + (foto_id   IS NOT NULL)::int = 1
    ),
    UNIQUE (user_id, maqola_id),
    UNIQUE (user_id, izoh_id),
    UNIQUE (user_id, foto_id)
);

(c) 10 turga yetsa: exclusive arc cho'zilib ketadi (10 ustun + 10 FK + ulkan CHECK). Bunda super-jadval to'g'ri: umumiy likeable (yoki kontent) ota jadval, har tur uning id siga FK, like esa bitta kontent_id FK bilan ulanadi. Ustun soni turlar soniga bog'liq emas.

Yechim β€” 12
CREATE TABLE foydalanuvchi (
    id          bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    email       text NOT NULL,
    deleted_at  timestamptz
);
CREATE UNIQUE INDEX uq_email_tirik
    ON foydalanuvchi (email) WHERE deleted_at IS NULL;

-- 1) Ro'yxatdan o'tish
INSERT INTO foydalanuvchi (email) VALUES ('a@x.com');           -- id 1
-- 2) Email o'zgartirish
UPDATE foydalanuvchi SET email = 'b@x.com' WHERE id = 1;
-- 3) Hisobni o'chirish (soft)
UPDATE foydalanuvchi SET deleted_at = now() WHERE id = 1;
-- 4) Xuddi shu email bilan yangi hisob β€” o'tadi (eskisi o'lik)
INSERT INTO foydalanuvchi (email) VALUES ('b@x.com');           -- id 2

Natija: 2 ta qator β€” id=1 (b@x.com, o'chirilgan, tarix) va id=2 (b@x.com, tirik). Qisman indeks faqat tirik qatorlarni tekshirgani uchun ikkalasi birga yashaydi. Endi id=2 ni tirikligida yana bir b@x.com tirik qo'shsa β€” bloklanadi.

Bu naqsh 5434 da (1.3-bo'lim) aynan shu mantiq bilan tasdiqlangan.

Yechim β€” 13
CREATE TABLE valyuta (
    kod   char(3) PRIMARY KEY,
    onlik smallint NOT NULL
);
CREATE TABLE hamyon (
    id          bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    user_id     bigint NOT NULL REFERENCES app_user(id),
    valyuta_kod char(3) NOT NULL REFERENCES valyuta(kod),
    balans      numeric(18,2) NOT NULL DEFAULT 0 CHECK (balans >= 0),
    UNIQUE (user_id, valyuta_kod)     -- har valyutadan bitta hamyon
);
  • Balans turi: numeric β€” pul aniq bo'lishi shart (float emas, 8-bo'lim).
  • Bir valyutadan bitta hamyon: UNIQUE (user_id, valyuta_kod).
  • Valyuta aralashuvidan himoya: har hamyon o'z valyuta_kod iga ega; balansni boshqa hamyon balansiga to'g'ridan-to'g'ri qo'shib bo'lmaydi (har biri alohida qator). Konvertatsiya alohida amal (kurs bilan) bo'lib, ikki hamyon o'rtasida tranzaksiya sifatida bajariladi. Hisobotlarda sum(balans) doim GROUP BY valyuta_kod bilan.
Yechim β€” 14
-- Lookup: mahsulot statusi (i18n / meta uchun jadval)
CREATE TABLE mahsulot_status (
    kod text PRIMARY KEY, nom text NOT NULL, tartib int NOT NULL
);
INSERT INTO mahsulot_status VALUES
    ('qoralama','Qoralama',1), ('faol','Faol',2), ('arxiv','Arxiv',9);

-- Valyuta lookup
CREATE TABLE valyuta (kod char(3) PRIMARY KEY, onlik smallint NOT NULL);

-- Asosiy mahsulot: pul + status + audit + soft delete
CREATE TABLE mahsulot (
    id          bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    narx        numeric(12,2) NOT NULL CHECK (narx >= 0),  -- (d) pul
    valyuta_kod char(3) NOT NULL REFERENCES valyuta(kod),  -- (d) valyuta
    status_kod  text NOT NULL DEFAULT 'qoralama'
                     REFERENCES mahsulot_status(kod),       -- (c) status (lookup)
    created_at  timestamptz NOT NULL DEFAULT now(),         -- (e) audit
    updated_at  timestamptz NOT NULL DEFAULT now(),
    created_by  bigint REFERENCES app_user(id),
    updated_by  bigint REFERENCES app_user(id),
    deleted_at  timestamptz                                 -- (a) soft delete
);

-- (b) i18n: tilga bog'liq matnlar alohida
CREATE TABLE mahsulot_tarjima (
    mahsulot_id bigint  NOT NULL REFERENCES mahsulot(id) ON DELETE CASCADE,
    til         char(2) NOT NULL,
    nom         text    NOT NULL,
    tavsif      text,
    PRIMARY KEY (mahsulot_id, til)
);

-- updated_at trigger
CREATE TRIGGER mahsulot_upd BEFORE UPDATE ON mahsulot
    FOR EACH ROW EXECUTE FUNCTION trg_updated_at();

Har naqsh nimani yechadi:

  • (a) soft delete (deleted_at) β€” mahsulot "arxivlandi"/o'chdi, lekin eski buyurtmalar unga ishora qilishni davom etadi (tarix saqlanadi).
  • (b) i18n β€” nom/tavsif uz/ru/en, yangi til ALTER'siz qo'shiladi.
  • (c) status (lookup) β€” qoralama/faol/arxiv UI nomi va tartibi bilan; noto'g'ri status FK bilan bloklanadi.
  • (d) pul β€” numeric + valyuta kodi, float xatosi yo'q.
  • (e) audit β€” kim/qachon yaratdi va oxirgi tahrirladi; updated_at trigger bilan avtomatik.

(Eslatma: status enum emas, lookup jadval tanlandi β€” chunki UI nomi/tartibi kerak; soft delete borligida arxiv statusi va deleted_at farqi: arxiv β€” biznes holati, deleted_at β€” texnik o'chirish.)


⬅️ Oldingi: 11 β€” Yaxlitlik va constraint dizayni Β· 🏠 README Β· Keyingi: 13 β€” Anti-naqshlar: nima qilmaslik kerak ➑️