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

Zero-downtime migratsiya: expand-contract naqshi PostgreSQL'da

Qisqacha: Ishlab turgan tizimda RENAME COLUMN bir zumda bajariladi — lekin eski kod darhol column does not exist bilan sinadi, chunki deploy paytida eski va yangi kod bir vaqtda ishlaydi. Expand-contract (kengaytirish-qisqartirish) bu o'zgarishni uch bosqichga bo'ladi. (1) expand — yangi ustun va ikki tomonlama sinxronlash, eski kod ham, yangi kod ham ishlaydi. (2) migrate — ilova yangi ustunga o'tadi, eski ma'lumot partiyalab ko'chiriladi. (3) contract — hamma nusxa yangilangach, eski ustun o'chiriladi. Har bosqich alohida deploy va har birida orqaga qaytish mumkin.

Bu darsda

  • Nega oddiy RENAME ishlab turgan tizimni sindirishini haqiqiy xatoda ko'rasiz.
  • Expand bosqichini yozasiz: yangi ustun, ikki tomonlama trigger, NOT VALID cheklov.
  • Katta jadvalda ma'lumotni partiyalab ko'chirasiz (backfill).
  • Contract bosqichini xavfsiz qulflar bilan bajarasiz.
  • Har bosqichda rollback rejasini va feature flag'ning o'rnini tushunasiz.

Oldin bilishingiz kerak: Xavfli migratsiyalar va qulflar, Prisma Migrate chuqur, Triggerlar, Funksiyalar va PL/pgSQL.

1. Nega bu kerak?

foydalanuvchilar jadvalida ism ustuni bor: "Aziz Karimov", "Dilnoza Rahimova". Unda aslida ism va familiya birga. 28-qismda profil sahifasi bo'ladi va u "To'liq ism" deb yozadi. Ustun nomi ma'nosiga mos bo'lsin: ism → toliq_ism.

ALTER TABLE darsida RENAME COLUMN ni ko'rgan edik — bir zumda. Muammo bazada emas, deploy'da. Server yangilanayotganda nima bo'ladi?

  • Ilova bir necha nusxada ishlaydi (masalan ikkita konteyner). Ular navbat bilan yangilanadi (rolling deploy).
  • Bir necha soniya yoki daqiqa davomida eski nusxa (ism ni o'qiydi) va yangi nusxa (toliq_ism ni o'qiydi) bir vaqtda so'rov qabul qiladi.
  • Bitta nusxada ham: migratsiya ilova ishga tushishidan oldin yoki keyin bajariladi — orada bir lahza eski kod yangi sxemada (yoki aksincha) ishlaydi.

Migratsiya RENAME qilsa — eski kod sinadi. Migratsiya qilmasa — yangi kod sinadi. Hech qanday tartibda ikkalasi birga ishlamaydi.

O'xshatish: ko'chaning nomini o'zgartirish. Ertalab eski lavhani olib, yangisini ilib qo'ysangiz — taksi haydovchilari kechgacha manzilni topa olmaydi. To'g'ri yo'l: bir muddat ikkala lavha osiladi ("Navoiy ko'chasi (sobiq Lenin)"), navigatorlar yangilanadi, keyin eski lavha olinadi.

2. Oddiy RENAME — qanday sinadi

Holat 36 bazasida (biz ishga tushirdik, PostgreSQL 17.6, Windows 11). Yangi nom berib, eski kod yuboradigan so'rovni bajaramiz:

sql
BEGIN;
ALTER TABLE foydalanuvchilar RENAME COLUMN ism TO toliq_ism;
SELECT ism FROM foydalanuvchilar LIMIT 1;
ROLLBACK;

Natija:

text
BEGIN
ALTER TABLE
ERROR:  column "ism" does not exist
LINE 1: SELECT ism FROM foydalanuvchilar LIMIT 1;
               ^
ROLLBACK

RENAME bir zumda o'tdi. Eski so'rov esa darhol yiqildi. Prodakshnda bu — deploy davomida har so'rovda 500 xato.

3. Uch bosqich

flowchart TD
  A[1. EXPAND: toliq_ism qo'shiladi, trigger sinxronlaydi] --> B[Deploy: yangi kod toliq_ism bilan ishlaydi]
  B --> C[2. BACKFILL: eski qatorlar partiyalab to'ldiriladi]
  C --> D[Hamma nusxa yangi kodda — tekshiruv]
  D --> E[3. CONTRACT: VALIDATE, NOT NULL, ism o'chiriladi]

Diagrammaning har bosqichi alohida vaqtda bajariladi — orada soatlar yoki kunlar o'tishi mumkin. Muhim qoida: har bosqichdan keyin tizim to'liq ishlaydigan holatda va oldingi bosqichga qaytish mumkin.

Bosqich Eski kod ishlaydimi Yangi kod ishlaydimi Orqaga qaytish
Boshlanish Ha Yo'q —
Expand'dan keyin Ha Ha Ustun va trigger'ni o'chirish
Yangi kod deploy'dan keyin Ha Ha Eski kodni qayta deploy
Contract'dan keyin Yo'q Ha Qiyin — yangi expand kerak

Oxirgi qator — eng muhim xulosa: contract — qaytarilmas qadam. Uni faqat eski kod hech qayerda qolmaganiga ishonch hosil qilgandan keyin qilasiz.

4. Expand: yangi ustun va ikki tomonlama sinxronlash

4.1 Migratsiya 107

Kanon sql/107-toliq-ism-expand.sql (izohlar qisqartirildi, kod aynan, uzun qatorlar bo'lindi). Biz uni bitta tranzaksiyada qo'llaymiz:

sql
BEGIN;
ALTER TABLE foydalanuvchilar ADD COLUMN toliq_ism text;
CREATE FUNCTION ism_sinxron() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
  IF TG_OP = 'INSERT' THEN
    NEW.toliq_ism := coalesce(NEW.toliq_ism, NEW.ism);
    NEW.ism := coalesce(NEW.ism, NEW.toliq_ism);
  ELSIF NEW.ism IS DISTINCT FROM OLD.ism THEN
    NEW.toliq_ism := NEW.ism;
  ELSIF NEW.toliq_ism IS DISTINCT FROM OLD.toliq_ism THEN
    NEW.ism := NEW.toliq_ism;
  END IF;
  RETURN NEW;
END $$;
CREATE TRIGGER foydalanuvchilar_ism_sinxron_trg
  BEFORE INSERT OR UPDATE OF ism, toliq_ism ON foydalanuvchilar
  FOR EACH ROW EXECUTE FUNCTION ism_sinxron();
UPDATE foydalanuvchilar SET toliq_ism = ism
WHERE toliq_ism IS NULL;
ALTER TABLE foydalanuvchilar
  ADD CONSTRAINT foydalanuvchilar_toliq_ism_check
  CHECK (toliq_ism IS NOT NULL AND btrim(toliq_ism) <> ''
    AND char_length(toliq_ism) <= 100) NOT VALID;
COMMIT;

Natija:

text
BEGIN
ALTER TABLE
CREATE FUNCTION
CREATE TRIGGER
UPDATE 8
ALTER TABLE
COMMIT

Har qism nima qiladi:

  • ADD COLUMN toliq_ism text — default'siz, NULL ga ruxsat: faqat metama'lumot (kanon o'lchovi — 14 ms, Xavfli migratsiyalar).
  • ism_sinxron() trigger'i — ikki tomonlama yozish (dual write). Eski kod ism ni yozsa — toliq_ism ham yangilanadi. Yangi kod toliq_ism ni yozsa — ism ham. INSERT da qaysi biri berilgan bo'lsa, ikkinchisi undan to'ldiriladi (coalesce). IS DISTINCT FROM — NULL bilan ham to'g'ri solishtiradi (NULL mantiqi).
  • UPDATE … WHERE toliq_ism IS NULL — mavjud 8 qatorni to'ldirish (backfill). Jadval kichik — bitta buyruq. Katta jadvalda — partiyalab (keyingi bo'lim).
  • CHECK … NOT VALID — yangi yozuvlar darhol tekshiriladi, eski qatorlar — keyin (contract'da VALIDATE).

Nega ism hali NOT NULL? Uning cheklovlari (NOT NULL, foydalanuvchilar_ism_check) joyida qoladi — eski kod uchun hech narsa o'zgarmadi.

Tekshirib ko'ring: Trigger UPDATE OF ism, toliq_ism uchun yozilgan. Nega oddiy BEFORE UPDATE emas?

Javob

UPDATE OF ustunlar — trigger faqat shu ustunlardan biri SET ichida bo'lganda ishlaydi. vazifalar_versiyasi kabi boshqa ustun yangilanganda (bu jadvalda tez-tez) trigger ortiqcha chaqirilmaydi. Har qatorlik trigger — har UPDATE ga qo'shimcha ish, shuning uchun uni faqat kerak joyda ishga tushiramiz.

Tuzog'ini ham eslang (Triggerlar): UPDATE OF ustun tilga olinganini tekshiradi, qiymat o'zgarganini emas. Shuning uchun ism_sinxron() ichida qiymatni o'zi solishtiradi (IS DISTINCT FROM) — SET ism = ism kabi "bo'sh" yangilash hech narsani buzmaydi.

4.2 Eski va yangi kod birga

Endi ikki xil ilovani taqlid qilamiz. Eski kod faqat ism ni biladi, yangi kod — faqat toliq_ism ni:

sql
INSERT INTO foydalanuvchilar (email, ism)
VALUES ('eski@example.com', 'Eski Ilova')
RETURNING ism, toliq_ism;
INSERT INTO foydalanuvchilar (email, toliq_ism)
VALUES ('yangi@example.com', 'Yangi Ilova')
RETURNING ism, toliq_ism;

Natija:

text
    ism     | toliq_ism  
------------+------------
 Eski Ilova | Eski Ilova
(1 row)

INSERT 0 1
     ism     |  toliq_ism   
-------------+-------------
 Yangi Ilova | Yangi Ilova
(1 row)

INSERT 0 1

Ikkalasi ham ishladi va har qatorda ikkala ustun to'ldi. Tahrirlash ham:

sql
UPDATE foydalanuvchilar SET ism = 'Aziz Karimov-Eski'
WHERE id = 1 RETURNING ism, toliq_ism;
UPDATE foydalanuvchilar SET toliq_ism = 'Aziz Karimov'
WHERE id = 1 RETURNING ism, toliq_ism;
SELECT count(*) FILTER (WHERE ism IS DISTINCT FROM toliq_ism)
  AS farqli, count(*) AS jami
FROM foydalanuvchilar;

Natija:

text
        ism        |     toliq_ism     
-------------------+-------------------
 Aziz Karimov-Eski | Aziz Karimov-Eski
(1 row)

UPDATE 1
     ism      |  toliq_ism   
--------------+--------------
 Aziz Karimov | Aziz Karimov
(1 row)

UPDATE 1
 farqli | jami 
--------+------
      0 |   10
(1 row)

10 qatordan farqlisi — 0. Kanon ham aynan shu natijani olgan (0/10). Eski va yangi kod bir bazada, bir vaqtda, ma'lumotni buzmasdan ishlayapti.

4.3 Eski cheklov hali ishlaydi

Yangi kod bo'sh ism yuborsa-chi?

sql
\set VERBOSITY terse
INSERT INTO foydalanuvchilar (email, toliq_ism)
VALUES ('bosh@example.com', '  ');

Natija:

text
ERROR:  new row for relation "foydalanuvchilar" violates check constraint "foydalanuvchilar_ism_check"

Qiziq: xatoni eski ustunning cheklovi berdi. Trigger ' ' ni ism ga ko'chirdi, ism ning CHECK i (btrim(ism) <> '') uni rad etdi. Yangi toliq_ism_check ham buni rad etardi. PostgreSQL CHECK larni nomi bo'yicha alifbo tartibida tekshiradi (hujjat, CREATE TABLE). …_ism_check esa …_toliq_ism_check dan oldin keladi. \set VERBOSITY terse — psql faqat asosiy xato qatorini ko'rsatsin: aks holda DETAIL da qatorning yaratilgan_vaqt i chiqadi va u har ishga tushirishda boshqacha.

5. Backfill: katta jadvalda partiyalab

5.1 Nega bitta UPDATE yomon

107 da UPDATE … WHERE toliq_ism IS NULL — bitta buyruq. 8 qatorda normal. 5 million foydalanuvchida esa:

  • Bitta uzun tranzaksiya — 5 million qator qulflangan, shu foydalanuvchilarning har qanday tahriri kutadi.
  • 5 million eski qator versiyasi — VACUUM ularni tranzaksiya tugaguncha tozalay olmaydi (MVCC), jadval shishadi.
  • Yarmida yiqilsa — hammasi qaytadi va qaytadan boshlanadi.

Yechim — kichik partiyalar, har biri alohida tranzaksiyada.

5.2 Procedure va COMMIT

PostgreSQL'da procedure (CREATE PROCEDURE) — funksiyadan farqli o'laroq — ichida COMMIT qila oladi. Kanon 20 000 qatorli nusxada 5 000 lik partiyalar bilan ishlagan (skript orqali). Biz xuddi shuni procedure bilan qildik:

sql
CREATE TABLE nusxa AS
  SELECT g AS id, 'Foydalanuvchi ' || g AS ism,
         NULL::text AS toliq_ism
  FROM generate_series(1, 20000) g;
ALTER TABLE nusxa ADD PRIMARY KEY (id);
CREATE PROCEDURE backfill(partiya int) LANGUAGE plpgsql AS $$
DECLARE n int;
BEGIN
  LOOP
    UPDATE nusxa SET toliq_ism = ism
    WHERE id IN (SELECT id FROM nusxa WHERE toliq_ism IS NULL
                 ORDER BY id LIMIT partiya);
    GET DIAGNOSTICS n = ROW_COUNT;
    EXIT WHEN n = 0;
    RAISE NOTICE 'partiya: % qator', n;
    COMMIT;
  END LOOP;
END $$;
CALL backfill(5000);
SELECT count(*) FILTER (WHERE toliq_ism IS NULL) AS qoldi_null
FROM nusxa;
DROP PROCEDURE backfill;
DROP TABLE nusxa;

Natija:

text
SELECT 20000
ALTER TABLE
CREATE PROCEDURE
NOTICE:  partiya: 5000 qator
NOTICE:  partiya: 5000 qator
NOTICE:  partiya: 5000 qator
NOTICE:  partiya: 5000 qator
CALL
 qoldi_null 
------------
          0
(1 row)

DROP PROCEDURE
DROP TABLE

To'rtta partiya, har biri o'z COMMIT i bilan, NULL qolmadi — kanon natijasi bilan bir xil (4 × 5 000, NULL 0). Muhim tafsilotlar:

  • WHERE toliq_ism IS NULL — qayta boshlashga chidamli: jarayon uzilsa, qayta CALL qilganda qolgan joyidan davom etadi.
  • GET DIAGNOSTICS n = ROW_COUNT — oxirgi UPDATE nechta qatorga tegdi; 0 bo'lsa — tugadi.
  • Haqiqiy prodakshnda partiyalar orasiga qisqa pauza qo'yiladi (pg_sleep(0.1)) — replikatsiya va boshqa so'rovlar nafas olsin.
  • Backfill trigger bilan birga ishlaydi: backfill paytida eski kod yozgan qator ham trigger orqali to'g'ri.

Diqqat: COMMIT li procedure'ni tranzaksiya ichidan (BEGIN; CALL …) chaqirib bo'lmaydi — invalid transaction termination xatosi. Prisma migrate deploy ham, tranzaksiyali boshqa vositalar ham uni migratsiya ichida yaxshi bajarmaydi. Shuning uchun katta backfill odatda alohida skript yoki fon vazifasi — migratsiya faylida emas.

6. Contract: eski ustunni olib tashlash

6.1 Qachon

Contract — faqat quyidagilar bajarilganda:

  1. Yangi kod hamma nusxada deploy qilingan, eski nusxa qolmagan.
  2. Backfill tugagan (toliq_ism IS NULL — 0 qator).
  3. Hech kim (hisobot, boshqa servis, analitika so'rovi) ism ni o'qimayapti — kodda qidiruv (grep), bazada esa pg_stat_statements (Sekin so'rovlarni topish).

6.2 Migratsiya 108

Kanon sql/108-toliq-ism-contract.sql (kod aynan; izohlar va oxirgi COMMENT ON COLUMN qatori tushirildi, BEGIN/COMMIT — pastda izohlangan):

sql
BEGIN;
SET LOCAL lock_timeout = '3s';
ALTER TABLE foydalanuvchilar
  VALIDATE CONSTRAINT foydalanuvchilar_toliq_ism_check;
ALTER TABLE foydalanuvchilar ALTER COLUMN toliq_ism SET NOT NULL;
DROP TRIGGER foydalanuvchilar_ism_sinxron_trg ON foydalanuvchilar;
DROP FUNCTION ism_sinxron();
ALTER TABLE foydalanuvchilar DROP COLUMN ism;
COMMIT;

Natija:

text
BEGIN
SET
ALTER TABLE
ALTER TABLE
DROP TRIGGER
DROP FUNCTION
ALTER TABLE
COMMIT

Har qadam o'tgan darsdagi qoidalar bilan:

  • lock_timeout = '3s' — qulf navbatida qolib hamma yozuvni to'smasin; vaqt tugasa — migratsiya qayta ishga tushiriladi.
  • VALIDATE — eski qatorlarni tekshiradi, SHARE UPDATE EXCLUSIVE (yozuvlar o'tadi).
  • SET NOT NULL — validatsiyalangan CHECK (toliq_ism IS NOT NULL …) bor, PostgreSQL 12+ jadvalni qayta skanerlamaydi.
  • DROP COLUMN ism — metama'lumot; ism ning CHECK i ham u bilan birga o'chadi.

Bu yerda VALIDATE va boshqalar bitta tranzaksiyada — foydalanuvchilar kichik jadval, ACCESS EXCLUSIVE qisqa. Katta jadvalda VALIDATE ni alohida tranzaksiyaga ajratgan ma'qul (Xavfli migratsiyalar 3-mashqidagi kabi).

Kanon fayli psql -1 bilan qo'llanadi — SET LOCAL shu tranzaksiyada ishlaydi. Prisma varianti (prisma/migrations/20261025120100_toliq_ism_contract) esa SET lock_timeout (LOCAL siz) ishlatadi: Prisma migrate deploy migratsiyani tranzaksiyaga o'ramaydi — kanon buni o'lchagan (keyingi bo'lim).

6.3 Kechikkan eski nusxa

Contract'dan keyin hali yangilanmagan eski nusxa qolgan bo'lsa:

sql
INSERT INTO foydalanuvchilar (email, ism)
VALUES ('kech@example.com', 'Kechikkan');
INSERT INTO foydalanuvchilar (email, toliq_ism)
VALUES ('ok@example.com', 'Hammasi joyida')
RETURNING toliq_ism;

Natija:

text
ERROR:  column "ism" of relation "foydalanuvchilar" does not exist
LINE 1: INSERT INTO foydalanuvchilar (email, ism)
                                             ^
   toliq_ism    
----------------
 Hammasi joyida
(1 row)

INSERT 0 1

Eski kod sindi, yangisi ishladi. Ya'ni contract'ni erta qilish — boshida ko'rgan RENAME xatosining o'zi, faqat kechiktirilgan. Shuning uchun contract — hamma nusxa yangilangach.

Tekshirib ko'ring: Expand va contract'ni bitta deploy'da, bitta migratsiyada qilsak nima bo'ladi?

Javob

Hech qanday foyda qolmaydi: migratsiya tugashi bilan ism yo'q — deploy davomidagi eski nusxalar sinadi. Expand-contract'ning butun ma'nosi — bosqichlar orasida eski va yangi kod birga ishlaydigan oraliq. Shuning uchun kamida ikki deploy: (1) expand + yangi kod, (2) contract.

7. Prisma bilan: tranzaksiyasiz deploy

Kanon 107 va 108 ni Prisma migratsiyalari sifatida ham yozdi (20261025120000_toliq_ism_expand, …120100_toliq_ism_contract) va bir muhim narsani o'lchadi: prisma migrate deploy faylni tranzaksiyaga o'ramaydi. Tajriba — yarmida 1/0 bor migratsiya:

text
Applying migration `1_yarim`
Error: P3018
A migration failed to apply. New migrations cannot be applied before the error is recovered from. Read more about how to resolve migration issues in a production database: https://pris.ly/d/migrate-resolve
Migration name: 1_yarim
Database error code: 22012

Shundan keyin bazada birinchi_qadam ustuni qoldi (birinchi ALTER bajarilgan), _prisma_migrations da 1_yarim — tugallanmagan, keyingi deploy — P3009. Xulosa:

  • Prisma migratsiyasida SET LOCAL ta'sirsiz — SET ishlating (kanon 108 shunday).
  • Har migratsiya kichik bo'lsin — yarmida yiqilsa, tozalash oson.
  • Xohlasangiz, faylning o'zini BEGIN; … COMMIT; bilan o'rang (CONCURRENTLY yo'q bo'lsa).

Kanon yana ikkita deploy'ni bir vaqtda ishga tushirdi: biri migratsiyani qo'lladi, ikkinchisi "No pending migrations" dedi — Prisma advisory lock ishlatadi. Bu haqda — Migratsiyani deploy jarayonida qo'llash.

8. Rollback va feature flag

8.1 Har bosqichda orqaga

  • Expand'dan keyin muammo (masalan trigger sekin): eski kod hali ishlaydi, DROP TRIGGER, DROP FUNCTION, DROP COLUMN toliq_ism — oldingi holat.
  • Yangi kod deploy'idan keyin muammo: eski kodni qayta deploy qilasiz — trigger tufayli ism doim yangi, eski kod yangi foydalanuvchilarni ham to'g'ri ko'radi. Bu expand-contract'ning eng katta foydasi.
  • Contract'dan keyin: eski kod endi ishlamaydi. Orqaga — faqat yangi expand (ustunni qayta qo'shish va backfill). Shuning uchun contract'ni shoshilmasdan, alohida, kuzatuvdan keyin.

8.2 Feature flag

Feature flag (funksiya bayrog'i) — kodda yangi xatti-harakatni yoqib-o'chiradigan sozlama (env yoki bazadagi yozuv). Expand-contract'da u "yangi kod qaysi ustundan o'qisin?" degan savolni deploy'siz almashtirish imkonini beradi: avval toliq_ism ga yozish, lekin ism dan o'qish; tekshiruvdan keyin bayroqni almashtirib toliq_ism dan o'qish. Muammo bo'lsa — bayroqni qaytarish, deploy kerak emas. Bizning holatda trigger ikkala ustunni doim bir xil saqlagani uchun bayroq shart emas edi; ilova darajasidagi dual write'da (trigger'siz) esa ko'pincha kerak bo'ladi. Feature flag'larni kasb qismida batafsil ko'ramiz.

8.3 Trigger yoki ilova

Trigger (kanon) Ilova kodida dual write
Boshqa yozuvchilar (skript, boshqa servis) Qamrab oladi Qamrab olmaydi
Kod soddaligi Ilova o'zgarmaydi Ikki ustunga yozish kodi
Ko'rinadiganlik Yashirin (bazada) Kodda ko'rinadi
Tezlik Har yozuvga kichik qo'shimcha Shuncha

Kichik va o'rta loyihada trigger — ishonchli tanlov: bazaga kim yozmasin, ikkala ustun mos qoladi.

9. Ko'p uchraydigan xatolar

1. Contract'ni erta qilish. column "ism" … does not exist eski nusxada. Tuzatish: contract — hamma nusxa yangilangach, kamida bitta to'liq deploy keyin.

2. Bitta katta backfill UPDATE. Uzun qulf, shishgan jadval. Tuzatish: partiyalar, har biri alohida COMMIT, qayta boshlashga chidamli shart (WHERE … IS NULL).

3. Trigger'siz expand. Yangi kod faqat toliq_ism ga yozadi, eski kod uni ko'rmaydi (yoki aksincha) — ikki ustun ajralib ketadi. Tuzatish: dual write (trigger yoki ilovada) — expand'ning ajralmas qismi.

4. SET LOCAL Prisma migratsiyasida. Ta'sirsiz — lock_timeout qo'yilmay qoladi. Tuzatish: SET lock_timeout (yoki faylni BEGIN … COMMIT ga o'rash).

5. Boshqa o'quvchilarni unutish. Hisobot skripti, BI vositasi, boshqa servis hali ism ni o'qiydi. Tuzatish: contract'dan oldin pg_stat_statements va kod qidiruvi.

6. invalid transaction termination. COMMIT li procedure tranzaksiya ichidan chaqirildi. Tuzatish: CALL ni tranzaksiyadan tashqarida (alohida skript).

10. Mashqlar

Yechimlar tekshirilgan (biz ishga tushirdik, PostgreSQL 17.6).

1-mashq (oson)

Har bosqichni joyiga qo'ying (expand / migrate / contract): (a) DROP COLUMN ism; (b) yangi kodni deploy qilish; (c) ADD COLUMN toliq_ism; (d) partiyalab backfill; (e) VALIDATE CONSTRAINT; (f) ikki tomonlama trigger yaratish; (g) trigger'ni o'chirish.

Yechim

Expand: (c), (f). Migrate: (b), (d). Contract: (e), (g), (a) — shu tartibda: avval tekshiruv va NOT NULL, keyin trigger va funksiya, oxirida ustun. Trigger ism ga ishora qiladi — uni ustundan oldin o'chirish kerak, aks holda DROP COLUMN bog'liqlik xatosi beradi.

2-mashq (o'rta)

vazifalar jadvalida muddat ustunini tugash_vaqti ga expand-contract bilan o'zgartirmoqchisiz. Faqat expand qismini yozing: ustun, ikki tomonlama trigger, backfill. Keyin eski kod (muddat yozadi) va yangi kod (tugash_vaqti yozadi) sifatida bittadan UPDATE qilib, ikkala ustun mos ekanini tekshiring. Oxirida hammasini qaytaring (bitta BEGIN … ROLLBACK ichida). Ishora: 107 dagi ism_sinxron() naqshi; vazifa id'si — 1.

Yechim
sql
BEGIN;
ALTER TABLE vazifalar ADD COLUMN tugash_vaqti timestamptz;
CREATE FUNCTION muddat_sinxron() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
  IF TG_OP = 'INSERT' THEN
    NEW.tugash_vaqti := coalesce(NEW.tugash_vaqti, NEW.muddat);
    NEW.muddat := coalesce(NEW.muddat, NEW.tugash_vaqti);
  ELSIF NEW.muddat IS DISTINCT FROM OLD.muddat THEN
    NEW.tugash_vaqti := NEW.muddat;
  ELSIF NEW.tugash_vaqti IS DISTINCT FROM OLD.tugash_vaqti THEN
    NEW.muddat := NEW.tugash_vaqti;
  END IF;
  RETURN NEW;
END $$;
CREATE TRIGGER vazifalar_muddat_sinxron_trg
  BEFORE INSERT OR UPDATE OF muddat, tugash_vaqti ON vazifalar
  FOR EACH ROW EXECUTE FUNCTION muddat_sinxron();
UPDATE vazifalar SET tugash_vaqti = muddat
WHERE tugash_vaqti IS NULL AND muddat IS NOT NULL;
UPDATE vazifalar SET muddat = '2026-11-01 18:00+05'
WHERE id = 1;
UPDATE vazifalar SET tugash_vaqti = '2026-11-02 09:00+05'
WHERE id = 2;
SELECT id, muddat = tugash_vaqti AS mos FROM vazifalar
WHERE id IN (1, 2) ORDER BY id;
SELECT count(*) FILTER (
  WHERE muddat IS DISTINCT FROM tugash_vaqti) AS farqli
FROM vazifalar;
ROLLBACK;

Natija:

text
BEGIN
ALTER TABLE
CREATE FUNCTION
CREATE TRIGGER
UPDATE 16
UPDATE 1
UPDATE 1
 id | mos 
----+-----
  1 | t
  2 | t
(2 rows)

 farqli 
--------
      0
(1 row)

ROLLBACK

UPDATE 16 — backfill: muddati bor 16 ta vazifa. Backfill'dagi muddat IS NOT NULL sharti — NULL ni NULL ga yozish behuda UPDATE. Oxirgi so'rov — butun jadval bo'yicha nazorat: farqli 0.

3-mashq (qiyin)

«Procedure va COMMIT» bo'limidagi backfill ni o'zgartiring: (1) partiyalar sonini va jami yangilangan qatorlarni oxirida RAISE NOTICE bilan chiqarsin; (2) 7 000 qatorli jadvalda 3 000 lik partiyalar bilan sinang. Natijada nechta partiya bo'ladi va oxirgisi necha qatorli?

Yechim
sql
CREATE TABLE nusxa2 AS
  SELECT g AS id, 'F ' || g AS ism, NULL::text AS toliq_ism
  FROM generate_series(1, 7000) g;
ALTER TABLE nusxa2 ADD PRIMARY KEY (id);
CREATE PROCEDURE backfill2(partiya int) LANGUAGE plpgsql AS $$
DECLARE n int; soni int := 0; jami int := 0;
BEGIN
  LOOP
    UPDATE nusxa2 SET toliq_ism = ism
    WHERE id IN (SELECT id FROM nusxa2 WHERE toliq_ism IS NULL
                 ORDER BY id LIMIT partiya);
    GET DIAGNOSTICS n = ROW_COUNT;
    EXIT WHEN n = 0;
    soni := soni + 1; jami := jami + n;
    RAISE NOTICE '%-partiya: % qator', soni, n;
    COMMIT;
  END LOOP;
  RAISE NOTICE 'tugadi: % partiya, % qator', soni, jami;
END $$;
CALL backfill2(3000);
DROP PROCEDURE backfill2;
DROP TABLE nusxa2;

Natija:

text
SELECT 7000
ALTER TABLE
CREATE PROCEDURE
NOTICE:  1-partiya: 3000 qator
NOTICE:  2-partiya: 3000 qator
NOTICE:  3-partiya: 1000 qator
NOTICE:  tugadi: 3 partiya, 7000 qator
CALL
DROP PROCEDURE
DROP TABLE

Uch partiya: 3 000 + 3 000 + 1 000. To'rtinchi aylanishda UPDATE 0 qatorga tegdi va EXIT ishladi — u partiya sifatida sanalmadi. % — RAISE dagi o'rin belgisi: uning o'rniga qiymat qo'yiladi (%-partiya → 1-partiya).

11. Loyiha qadami

11.1 Holat 38

Kanon commit'i:

bash
git add sql/107-toliq-ism-expand.sql sql/108-toliq-ism-contract.sql
git add prisma/migrations prisma/schema.prisma test
M='feat(db): foydalanuvchilar.ism → toliq_ism — expand-contract'
M+=' (107 expand: ikki tomonlama trigger, 108 contract)'
git commit -m "$M"

O'zgarishlar:

  • sql/107-…expand.sql, sql/108-…contract.sql — yuqoridagi fayllar (izohlari bilan).
  • prisma/migrations/20261025120000_toliq_ism_expand/ va …120100_toliq_ism_contract/ — xuddi shu SQL; contract'da SET lock_timeout = '3s'; (LOCAL siz — Prisma tranzaksiyasi yo'q).
  • schema.prisma — ism String o'rniga toliqIsm String @map("toliq_ism").
  • Testlar — foydalanuvchi yaratishda (email, toliq_ism).
  • Kanon o'zining 500 000 qatorli o'lchov to'plamini ham toliq_ism ga o'tkazdi — bu fayl sizning repoda yo'q, unga tegmaysiz.

API'ning o'zi ism ni ishlatmaydi — shuning uchun vazifalar-api kodida bundan boshqa o'zgarish yo'q. O'quv loyihamizda ikkala migratsiya bitta holatda; haqiqiy loyihada ular ikki alohida reliz bo'lardi: 107 + yangi kod, keyinroq 108.

11.2 Tekshiruv

Kanon tekshiruvi: tip/lint 0, testlar 99/99, 3 migratsiya deploy == sql/ dan qurilgan baza, migrate dev --create-only — bo'sh migratsiya (drift yo'q), jonli API 9/9 × 2. O'zingizda:

bash
npm run db:migrate
npx prisma migrate status
npm run check

12. Real ishda

  • Katta kompaniyalar (to'lov tizimlari, marketpleyslar) har sxema o'zgarishini expand-contract bilan qiladi — ularda "texnik ishlar uchun sayt yopiq" degan sahifa yo'q.
  • Ustun turini o'zgartirish (int → bigint id'lar tugaganda), jadvalni bo'lish, bazani ko'chirish — hammasi shu naqshning variantlari: yangi tuzilma, sinxronlash, ko'chirish, eski tuzilmani olib tashlash.
  • API versiyalash bilan bog'liq: 25-qismdagi Deprecation sarlavhasi ham "kengaytir, keyin qisqartir" g'oyasining HTTP'dagi shakli.
  • Intervyuda: "Ishlab turgan tizimda ustun nomini qanday o'zgartirasiz?" — expand, dual write, backfill, contract va rollback'ni tilga olsangiz, kuchli javob.

Xulosa

  • RENAME COLUMN bir zumda, lekin deploy paytidagi eski kodni darhol sindiradi.
  • Expand: yangi ustun (default'siz), ikki tomonlama trigger, CHECK … NOT VALID — eski va yangi kod birga (farqli 0/10).
  • Backfill — partiyalab, har biri alohida COMMIT, qayta boshlashga chidamli (procedure 4 × 5 000).
  • Contract — hamma nusxa yangilangach: lock_timeout, VALIDATE, SET NOT NULL, trigger va eski ustunni o'chirish; bu qadam qaytarilmas.
  • Prisma migrate deploy tranzaksiyasiz — migratsiyalar kichik, SET LOCAL emas SET.

Keyingi dars: Seeding va test ma'lumotlari — dev va test bazasini bir buyruqda to'ldirish: idempotent, deterministik seed, faker va "prodakshnda ishlamaydi" himoyasi.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Zero-downtime migratsiya: expand-contract naqshi PostgreSQL'da — IlmHamroh