Mundarija (31)
- Bu darsda
- 1. Nega bu kerak?
- 2. Oddiy RENAME — qanday sinadi
- 3. Uch bosqich
- 4. Expand: yangi ustun va ikki tomonlama sinxronlash
- 4.1 Migratsiya 107
- 4.2 Eski va yangi kod birga
- 4.3 Eski cheklov hali ishlaydi
- 5. Backfill: katta jadvalda partiyalab
- 5.1 Nega bitta UPDATE yomon
- 5.2 Procedure va COMMIT
- 6. Contract: eski ustunni olib tashlash
- 6.1 Qachon
- 6.2 Migratsiya 108
- 6.3 Kechikkan eski nusxa
- 7. Prisma bilan: tranzaksiyasiz deploy
- 8. Rollback va feature flag
- 8.1 Har bosqichda orqaga
- 8.2 Feature flag
- 8.3 Trigger yoki ilova
- 9. Ko'p uchraydigan xatolar
- 10. Mashqlar
- 1-mashq (oson)
- 2-mashq (o'rta)
- 3-mashq (qiyin)
- 11. Loyiha qadami
- 11.1 Holat 38
- 11.2 Tekshiruv
- 12. Real ishda
- Xulosa
- Manbalar
Zero-downtime migratsiya: expand-contract naqshi PostgreSQL'da
Qisqacha: Ishlab turgan tizimda
RENAME COLUMNbir zumda bajariladi — lekin eski kod darholcolumn does not existbilan 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
RENAMEishlab turgan tizimni sindirishini haqiqiy xatoda ko'rasiz. - Expand bosqichini yozasiz: yangi ustun, ikki tomonlama trigger,
NOT VALIDcheklov. - 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 (
ismni o'qiydi) va yangi nusxa (toliq_ismni 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:
BEGIN;
ALTER TABLE foydalanuvchilar RENAME COLUMN ism TO toliq_ism;
SELECT ism FROM foydalanuvchilar LIMIT 1;
ROLLBACK;Natija:
BEGIN
ALTER TABLE
ERROR: column "ism" does not exist
LINE 1: SELECT ism FROM foydalanuvchilar LIMIT 1;
^
ROLLBACKRENAME 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:
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:
BEGIN
ALTER TABLE
CREATE FUNCTION
CREATE TRIGGER
UPDATE 8
ALTER TABLE
COMMITHar qism nima qiladi:
ADD COLUMN toliq_ism text— default'siz,NULLga ruxsat: faqat metama'lumot (kanon o'lchovi — 14 ms, Xavfli migratsiyalar).ism_sinxron()trigger'i — ikki tomonlama yozish (dual write). Eski kodismni yozsa —toliq_ismham yangilanadi. Yangi kodtoliq_ismni yozsa —ismham.INSERTda qaysi biri berilgan bo'lsa, ikkinchisi undan to'ldiriladi (coalesce).IS DISTINCT FROM—NULLbilan 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'daVALIDATE).
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_ismuchun yozilgan. Nega oddiyBEFORE UPDATEemas?
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:
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:
ism | toliq_ism
------------+------------
Eski Ilova | Eski Ilova
(1 row)
INSERT 0 1
ism | toliq_ism
-------------+-------------
Yangi Ilova | Yangi Ilova
(1 row)
INSERT 0 1Ikkalasi ham ishladi va har qatorda ikkala ustun to'ldi. Tahrirlash ham:
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:
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?
\set VERBOSITY terse
INSERT INTO foydalanuvchilar (email, toliq_ism)
VALUES ('bosh@example.com', ' ');Natija:
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:
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:
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 TABLETo'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, qaytaCALLqilganda qolgan joyidan davom etadi.GET DIAGNOSTICS n = ROW_COUNT— oxirgiUPDATEnechta 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:
COMMITli procedure'ni tranzaksiya ichidan (BEGIN; CALL …) chaqirib bo'lmaydi —invalid transaction terminationxatosi. Prismamigrate deployham, 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:
- Yangi kod hamma nusxada deploy qilingan, eski nusxa qolmagan.
- Backfill tugagan (
toliq_ism IS NULL— 0 qator). - Hech kim (hisobot, boshqa servis, analitika so'rovi)
ismni o'qimayapti — kodda qidiruv (grep), bazada esapg_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):
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:
BEGIN
SET
ALTER TABLE
ALTER TABLE
DROP TRIGGER
DROP FUNCTION
ALTER TABLE
COMMITHar 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— validatsiyalanganCHECK (toliq_ism IS NOT NULL …)bor, PostgreSQL 12+ jadvalni qayta skanerlamaydi.DROP COLUMN ism— metama'lumot;ismningCHECKi 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:
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:
ERROR: column "ism" of relation "foydalanuvchilar" does not exist
LINE 1: INSERT INTO foydalanuvchilar (email, ism)
^
toliq_ism
----------------
Hammasi joyida
(1 row)
INSERT 0 1Eski 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:
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: 22012Shundan keyin bazada birinchi_qadam ustuni qoldi (birinchi ALTER bajarilgan), _prisma_migrations da 1_yarim — tugallanmagan, keyingi deploy — P3009. Xulosa:
- Prisma migratsiyasida
SET LOCALta'sirsiz —SETishlating (kanon 108 shunday). - Har migratsiya kichik bo'lsin — yarmida yiqilsa, tozalash oson.
- Xohlasangiz, faylning o'zini
BEGIN; … COMMIT;bilan o'rang (CONCURRENTLYyo'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
ismdoim 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
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:
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)
ROLLBACKUPDATE 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
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:
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 TABLEUch 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:
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'daSET lock_timeout = '3s';(LOCALsiz — Prisma tranzaksiyasi yo'q).schema.prisma—ism Stringo'rnigatoliqIsm String @map("toliq_ism").- Testlar — foydalanuvchi yaratishda
(email, toliq_ism). - Kanon o'zining 500 000 qatorli o'lchov to'plamini ham
toliq_ismga 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:
npm run db:migrate
npx prisma migrate status
npm run check12. 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→bigintid'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
Deprecationsarlavhasi 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 COLUMNbir 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 deploytranzaksiyasiz — migratsiyalar kichik,SET LOCALemasSET.
Keyingi dars: Seeding va test ma'lumotlari — dev va test bazasini bir buyruqda to'ldirish: idempotent, deterministik seed, faker va "prodakshnda ishlamaydi" himoyasi.
Manbalar
- PostgreSQL 18 hujjati — CREATE PROCEDURE va tranzaksiya boshqaruvi (procedure ichida COMMIT): https://www.postgresql.org/docs/18/plpgsql-transactions.html
- PostgreSQL 18 hujjati — ALTER TABLE (
VALIDATE,SET NOT NULLva CHECK): https://www.postgresql.org/docs/18/sql-altertable.html - Prisma hujjati — Expand and contract pattern; Patching and hotfixing: https://www.prisma.io/docs/guides
- Kanon o'lchovi (IlmHamroh, 2026-10): holat 38,
sql/107,sql/108(PostgreSQL 18.6, Prisma 7.10.1, 2026-10-25); darsdagi SQL — biz ishga tushirdik (PostgreSQL 17.6, Windows 11, holat 36), 2026-10-27.
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!