IlmHamroh
JavaScript Full-stack/26-qism. SQL va PostgreSQL23/64-dars22 daqiqa
Mundarija (36)

PostgreSQL UPSERT va MERGE: ON CONFLICT, EXCLUDED va poyga holatisiz yozish

Qisqacha: UPSERT — "bor bo'lsa yangila, yo'q bo'lsa qo'sh" amali. PostgreSQL'da u INSERT ... ON CONFLICT (ustunlar) DO NOTHING yoki DO UPDATE SET ... = EXCLUDED.... bilan yoziladi va bitta buyruqda, poyga holatisiz (race condition) ishlaydi. Shart: ON CONFLICT dagi ustunlarda UNIQUE cheklov bo'lishi kerak. Bir nechta jadvalni solishtirib, qo'shish, yangilash va o'chirishni birga qilish uchun — MERGE (PG 15+), PG 17 dan esa MERGE ... RETURNING merge_action().

Bu darsda

  • "Avval tekshir, keyin qo'sh" usuli nega ikki parallel so'rovda buzilishini ko'rasiz.
  • ON CONFLICT DO NOTHING va DO UPDATE bilan UPSERT yozasiz, EXCLUDED nima ekanini tushuntirasiz.
  • Conflict target, DO UPDATE ... WHERE va "bir buyruqda ikki marta" xatosini bilasiz.
  • MERGE bilan import jadvalini asosiy jadvalga sinxronlaysiz.
  • vazifalar bazasiga teglar jadvalini qo'shasiz (holat 23).

Oldin bilishingiz kerak: INSERT va RETURNING, UPDATE va DELETE xavfsiz, CREATE TABLE va cheklovlar, Ifodalar va o'rnatilgan funksiyalar.

1. Nega bu kerak?

Oldingi darsda ifodalar va funksiyalar bilan ma'lumotni SQL ichida qayta ishladik. Endi vazifalar ga yangi imkoniyat qo'shamiz: teglar. Frontendda (22-qism) vazifa matnidagi #uy, #to'lov so'zlari teg bo'lardi. Endi teglar bazada alohida jadvalda turadi.

Aziz "Kranni almashtirish #uy" deb yozdi. Server #uy tegini teglar jadvaliga qo'shishi kerak. Lekin Azizda #uy allaqachon bor bo'lishi mumkin. Birinchi xayolga keladigan yo'l:

  1. SELECT — Azizda #uy bormi?
  2. Yo'q bo'lsa — INSERT.

Bitta foydalanuvchi, bitta so'rov bo'lsa — ishlaydi. Endi tasavvur qiling: Aziz bir vaqtda telefondan ham, noutbukdan ham #sport tegli vazifa yozdi. Ikki so'rov deyarli bir paytda keldi. Biz buni ikki psql oynasida sinadik (PostgreSQL 17.6, o'z kompyuterimizda). Har oyna — bazaga alohida ulanish, ya'ni alohida seans. Vaqt bo'yicha:

Qadam A seans (telefon) B seans (noutbuk) Natija
1 BEGIN; INSERT … '#sport' — A qo'shdi, lekin COMMIT yo'q
2 — SELECT count(*) … '#sport' 0 — A ning qatori B ga ko'rinmaydi
3 — INSERT … '#sport' B A tugashini kutib qoladi
4 COMMIT; (~1.3 s dan keyin) — A ning tegi saqlandi
5 — — B xato oladi: duplicate key

B seansidagi buyruqlar (ikkinchi seans kerakligi uchun bitta oynada takrorlanmaydi):

sql
SELECT count(*) FROM teglar WHERE egasi_id = 1 AND nom = '#sport';
INSERT INTO teglar (egasi_id, nom) VALUES (1, '#sport');

B seansida:

text
 count
-------
     0
(1 row)

ERROR:  duplicate key value violates unique constraint "teglar_egasi_id_nom_key"
DETAIL:  Key (egasi_id, nom)=(1, #sport) already exists.

B "teg yo'q" deb ko'rdi — A hali COMMIT qilmagani uchun uning qatori B ga ko'rinmaydi. B INSERT qildi, A tugashini ~1.3 soniya kutdi va xato oldi. Foydalanuvchi "Xato yuz berdi" degan oynani ko'radi. Bu — poyga holati (race condition): natija qaysi so'rov birinchi "yetib kelgani"ga bog'liq. Xuddi ikki kassir bir paytda omborga qarab "oxirgi qop un bor" deb, ikkalasi ham uni sotib yuborgandek.

sequenceDiagram
  participant A as Telefon (A)
  participant DB as PostgreSQL
  participant B as Noutbuk (B)
  A->>DB: INSERT #sport (COMMIT yo'q)
  B->>DB: SELECT #sport bormi?
  DB-->>B: 0 ta
  B->>DB: INSERT #sport
  Note over B,DB: A tugashini kutadi
  A->>DB: COMMIT
  DB-->>B: ERROR duplicate key

Diagrammada asosiy narsa — B ning SELECT i va INSERT i orasidagi "teshik". Tekshiruv bilan yozuv ikki alohida qadam bo'lsa, ular orasiga boshqa so'rov kirib qoladi. Yechim — tekshiruv va yozuvni bitta buyruqqa birlashtirish. Bu darsning mavzusi aynan shu.

2. Teglar jadvali va conflict target

2.1 Jadval

vazifalar bazasining 23-holatida ikki yangi jadval bor. Bu yerda teglar ning asosiy qismi:

sql
CREATE TABLE teglar (
  id       bigint GENERATED ALWAYS AS IDENTITY,
  egasi_id bigint NOT NULL,
  nom      text   NOT NULL,
  CONSTRAINT teglar_pkey PRIMARY KEY (id),
  CONSTRAINT teglar_egasi_id_fkey FOREIGN KEY (egasi_id)
    REFERENCES foydalanuvchilar (id) ON DELETE CASCADE,
  -- teg kichik harfda saqlanadi, # bilan boshlanadi
  CONSTRAINT teglar_nom_check CHECK (nom ~ '^#'
    AND nom = lower(nom) AND char_length(nom) <= 50),
  -- ON CONFLICT (egasi_id, nom) shu cheklovga tayanadi
  CONSTRAINT teglar_egasi_id_nom_key UNIQUE (egasi_id, nom)
);

Teg har foydalanuvchiniki: Azizning #uy i — Dilnozaniki emas. Shuning uchun noyoblik bitta ustunda emas, juftlikda: UNIQUE (egasi_id, nom). Aziz ikkita #uy yarata olmaydi, lekin Aziz va Dilnoza har biri o'z #uy iga ega bo'la oladi.

Ikkinchi jadval — vazifa_teglari, qaysi vazifada qaysi teg borligini bog'laydi. U haqida Bog'lanishlar: 1:1, 1:N, N:M darsida batafsil gaplashamiz. Hozircha bilish yetarli: bitta vazifada bir nechta teg, bitta tegda bir nechta vazifa bo'lishi mumkin.

2.2 Conflict target nima

UPSERT'ning kaliti — conflict target (to'qnashuv nishoni): "qaysi noyoblik buzilsa, bu to'qnashuv deb hisoblansin". ON CONFLICT (egasi_id, nom) — "agar (egasi_id, nom) juftligi band bo'lsa". Postgres bu ustunlarga mos UNIQUE cheklov yoki noyob indeksni qidiradi va to'qnashuvni o'sha indeks orqali aniqlaydi.

Indeks muhim: u bir vaqtda ikki seans bir xil qiymatni yozayotganini "ko'radi". SELECT esa hali tasdiqlanmagan qatorni ko'rmaydi — biz buni yuqorida ko'rdik. Indeks qanday ishlashini Indeks nima: B-tree ichidan darsida o'rganamiz.

Mos indeks bo'lmasa — xato:

sql
INSERT INTO teglar (egasi_id, nom) VALUES (1, '#uy')
ON CONFLICT (nom) DO NOTHING;
text
ERROR:  there is no unique or exclusion constraint matching the ON CONFLICT specification

Tarjimasi: "ON CONFLICT ko'rsatmasiga mos noyob yoki istisno cheklovi yo'q". Bizda UNIQUE (nom) yo'q — faqat (egasi_id, nom) bor. Postgres "nom bo'yicha to'qnashuv"ni aniqlay olmaydi.

Ustunlar o'rniga cheklov nomini ham yozish mumkin:

sql
INSERT INTO teglar (egasi_id, nom) VALUES (1, '#uy')
ON CONFLICT ON CONSTRAINT teglar_egasi_id_nom_key DO NOTHING;
text
INSERT 0 0

INSERT 0 0 — hech narsa qo'shilmadi (ikkinchi raqam — qo'shilgan qatorlar soni). Ustunlar bilan yozish odatda afzal: cheklov nomi o'zgarsa ham so'rov buzilmaydi.

Tekshirib ko'ring: foydalanuvchilar jadvalida email uchun UNIQUE INDEX ... (lower(email)) bor (ifoda ustidagi indeks). ON CONFLICT (email) ishlaydimi?

Javob

Yo'q — email ustunining o'zida noyob indeks yo'q, indeks lower(email) ifodasida. Conflict target indeksga aynan mos bo'lishi kerak: ON CONFLICT (lower(email)). Postgres ifodali indeksni ham conflict target sifatida qabul qiladi, faqat ifoda indeksdagi bilan bir xil yozilishi shart.

3. ON CONFLICT DO NOTHING

3.1 Oddiy INSERT va DO NOTHING

Avval oddiy INSERT — Azizda #uy allaqachon bor:

sql
INSERT INTO teglar (egasi_id, nom) VALUES (1, '#uy');
text
ERROR:  duplicate key value violates unique constraint "teglar_egasi_id_nom_key"
DETAIL:  Key (egasi_id, nom)=(1, #uy) already exists.

Tarjimasi: "takror kalit qiymati teglar_egasi_id_nom_key noyoblik cheklovini buzadi. Tafsilot: (1, #uy) kaliti allaqachon bor". Bu xatoni CREATE TABLE va cheklovlar darsida ko'rgansiz.

Endi xuddi shu buyruq ON CONFLICT ... DO NOTHING bilan:

sql
INSERT INTO teglar (egasi_id, nom) VALUES (1, '#uy')
ON CONFLICT (egasi_id, nom) DO NOTHING
RETURNING id;
text
 id
----
(0 rows)

INSERT 0 0

Xato yo'q. Teg bor edi — Postgres jim o'tib ketdi. RETURNING bo'sh: hech qanday qator qo'shilmadi va o'zgarmadi, qaytaradigan narsa yo'q.

Poyga holati ham shu bilan yo'qoladi. Birinchi bo'limdagi tajribani B seansida ON CONFLICT DO NOTHING bilan takrorladik (PostgreSQL 17.6). B yana A ning COMMIT ini kutdi, lekin xato o'rniga INSERT 0 0 oldi. Bazada #sport — bitta.

3.2 DO NOTHING ning kamchiligi: id yo'q

Server tegni bog'lash uchun uning id sini bilishi kerak. DO NOTHING esa bor tegning id sini qaytarmaydi. Ikki yo'l bor:

  1. UPSERT'dan keyin alohida SELECT id FROM teglar WHERE ... (ikkinchi so'rov, lekin endi xavfsiz — teg albatta bor).
  2. DO UPDATE ni "hech narsani o'zgartirmaydigan" qilib yozish — keyingi bo'limda.

Tekshirib ko'ring: ON CONFLICT DO NOTHING ni conflict target'siz yozsa bo'ladimi: INSERT ... ON CONFLICT DO NOTHING?

Javob

Ha, DO NOTHING uchun target ixtiyoriy: unda istalgan noyoblik cheklovi buzilsa ham qator jim tashlab yuboriladi. Bu qulay, lekin xavfli: siz kutmagan cheklov (masalan boshqa UNIQUE ustun) buzilsa ham xato chiqmaydi va muammo yashirinadi. DO UPDATE da esa target majburiy.

4. ON CONFLICT DO UPDATE va EXCLUDED

4.1 EXCLUDED — "qo'shilmoqchi bo'lgan qator"

DO UPDATE — to'qnashuv bo'lsa, mavjud qatorni yangilash. Yangilashda ikki qator kerak bo'ladi: jadvaldagi eski qator va biz qo'shmoqchi bo'lgan yangi qator. Yangisi — maxsus EXCLUDED (chetlatilgan) nomi bilan. "Chetlatilgan", chunki u jadvalga kira olmay, eshik oldida qoldi.

Tegning id sini doim olish uchun naqsh (sinov BEGIN ... ROLLBACK ichida — bazani o'zgartirmaslik uchun):

sql
BEGIN;
INSERT INTO teglar (egasi_id, nom)
VALUES (1, '#uy'), (1, '#yangi')
ON CONFLICT (egasi_id, nom) DO UPDATE SET nom = EXCLUDED.nom
RETURNING id, nom, xmax <> 0 AS yangilandi;
ROLLBACK;
text
BEGIN
 id |  nom   | yangilandi
----+--------+------------
  8 | #uy    | t
 37 | #yangi | f
(2 rows)

INSERT 0 2
ROLLBACK

SET nom = EXCLUDED.nom — nomni o'ziga tenglash, qiymat o'zgarmaydi. Lekin endi bu qator "yangilangan" hisoblanadi va RETURNING uni qaytaradi. Natijada ikkala tegning id si bor: #uy — eski (8), #yangi — yangi (37).

xmax <> 0 — kichik hiyla. xmax — har qatorda bor yashirin tizim ustuni. Yangilangan qatorda u noldan farqli, yangi qo'shilganda — nol. Uning to'liq ma'nosini MVCC: Postgres ichida versiyalar darsida o'rganamiz. Hozir bilish shart emas — faqat "yangimi yoki borimi" ni ajratish uchun ishlatiladi.

Diqqat: "o'zgarmaydigan" DO UPDATE bepul emas. U baribir qatorning yangi versiyasini yozadi va trigger'lar ishlaydi. Tez-tez chaqiriladigan joyda DO NOTHING + alohida SELECT arzonroq bo'lishi mumkin.

4.2 Nega #yangi ning id si 37?

Bazada 32 ta teg bor edi, keyingi yangi teg 33 olishi kerak edi. Lekin shu darsdagi oldingi uch buyruqni eslang: ON CONSTRAINT ... DO NOTHING, xato bergan oddiy INSERT va DO NOTHING ... RETURNING. Ularning birortasi ham qator qo'shmadi, lekin har biri raqam oldi: 33, 34, 35. Shu buyruqdagi #uy ham 36 ni oldi. #yangi ga 37 qoldi.

Qoida: Postgres raqamni qatorni yozishdan oldin oladi, to'qnashuv esa keyin aniqlanadi. Olingan raqam qaytarilmaydi. Primary key tanlovi darsida ROLLBACK id'da "teshik" qoldirishini ko'rgansiz — bu ham xuddi shunday. Id'lar ketma-ket bo'lishiga hech qachon tayanmang.

4.3 Hisoblagich: eski qiymatga qo'shish

UPSERT'ning eng mashhur ishlatilishi — hisoblagich (counter). Masalan vazifa necha marta ko'rilganini sanash. Sinov uchun vaqtinchalik jadval (TEMP TABLE — seans tugaganda o'zi o'chadi):

sql
CREATE TEMP TABLE korishlar (
  vazifa_id bigint PRIMARY KEY,
  soni      int NOT NULL,
  oxirgi    timestamptz
);
INSERT INTO korishlar VALUES (1, 1, '2026-10-20 10:00+05')
ON CONFLICT (vazifa_id) DO UPDATE
  SET soni = korishlar.soni + 1, oxirgi = EXCLUDED.oxirgi;
INSERT INTO korishlar VALUES (1, 1, '2026-10-20 11:00+05')
ON CONFLICT (vazifa_id) DO UPDATE
  SET soni = korishlar.soni + 1, oxirgi = EXCLUDED.oxirgi;
SELECT * FROM korishlar;
text
CREATE TABLE
INSERT 0 1
INSERT 0 1
 vazifa_id | soni |         oxirgi
-----------+------+------------------------
         1 |    2 | 2026-10-20 06:00:00+00
(1 row)

Birinchi buyruq qatorni qo'shdi (soni = 1), ikkinchisi to'qnashdi va soni ni oshirdi. korishlar.soni — jadvaldagi eski qiymat, EXCLUDED.oxirgi — yangi qatordan kelgan vaqt. Vaqt UTC'da ko'rinyapti: 11:00 Toshkent = 06:00 UTC (Ma'lumot turlari darsidagi timestamptz).

Bu buyruq parallel so'rovlarda ham to'g'ri ishlaydi. Ikki seans bir vaqtda soni + 1 qilsa, ikkinchisi birinchisi tugashini kutadi va yangilangan qiymatga qo'shadi — natija 2 emas, 3 bo'ladi.

4.4 DO UPDATE ... WHERE: shart bilan yangilash

Ba'zan eski ma'lumot yangisini bosib ketmasligi kerak. Masalan telefon internetsiz edi va eski "ko'rish" hodisasini kechikib yubordi. WHERE bilan faqat yangiroq vaqt yozilsin:

sql
CREATE TEMP TABLE korishlar (
  vazifa_id bigint PRIMARY KEY,
  soni      int NOT NULL,
  oxirgi    timestamptz
);
INSERT INTO korishlar VALUES (1, 1, '2026-10-20 11:00+05');
-- eskiroq vaqt keldi: yangilanmasin
INSERT INTO korishlar VALUES (1, 1, '2026-10-20 09:00+05')
ON CONFLICT (vazifa_id) DO UPDATE
  SET oxirgi = EXCLUDED.oxirgi
  WHERE korishlar.oxirgi < EXCLUDED.oxirgi;
SELECT * FROM korishlar;
text
CREATE TABLE
INSERT 0 1
INSERT 0 0
 vazifa_id | soni |         oxirgi
-----------+------+------------------------
         1 |    1 | 2026-10-20 06:00:00+00
(1 row)

INSERT 0 0 — to'qnashuv bo'ldi, lekin WHERE sharti yolg'on chiqdi va qator o'zgarmadi. Vaqt 11:00 (06:00 UTC) bo'lib qoldi. Biz buni o'zimiz ishga tushirdik (PostgreSQL 17.6).

4.5 Bitta buyruqda bir kalit ikki marta

sql
INSERT INTO teglar (egasi_id, nom)
VALUES (1, '#ikki'), (1, '#ikki')
ON CONFLICT (egasi_id, nom) DO UPDATE SET nom = EXCLUDED.nom;
text
ERROR:  ON CONFLICT DO UPDATE command cannot affect row a second time
HINT:  Ensure that no rows proposed for insertion within the same command have duplicate constrained values.

Tarjimasi: "ON CONFLICT DO UPDATE buyrug'i bitta qatorga ikkinchi marta ta'sir qila olmaydi. Maslahat: bitta buyruqda qo'shilayotgan qatorlarda cheklangan qiymatlar takrorlanmasin". Postgres qaysi qiymat "oxirgi" ekanini o'zi tanlamaydi — tanlovni sizga qoldiradi.

Bu ko'pincha matndan teg ajratganda chiqadi: "#uy ... #uy" matnida teg ikki marta. Yechim — kiritishdan oldin takrorni olib tashlash (DISTINCT):

sql
BEGIN;
INSERT INTO teglar (egasi_id, nom)
SELECT DISTINCT 1, nom FROM (VALUES ('#ikki'), ('#ikki')) AS t(nom)
ON CONFLICT (egasi_id, nom) DO UPDATE SET nom = EXCLUDED.nom
RETURNING id, nom;
ROLLBACK;
text
BEGIN
 id |  nom
----+-------
 40 | #ikki
(1 row)

INSERT 0 1
ROLLBACK

DO NOTHING da bu xato chiqmaydi — ikkinchi nusxa jim tashlab yuboriladi.

Tekshirib ko'ring: hisoblagich misolida SET soni = soni + 1 deb (jadval nomisiz) yozsa nima bo'ladi?

Javob

soni ikki ma'noli: jadvaldagi korishlar.soni mi yoki EXCLUDED.soni mi? SET ning chap tomoni har doim jadval ustuni. O'ng tomonida esa Postgres ERROR: column reference "soni" is ambiguous deydi. Shuning uchun o'ng tomonda doim korishlar.soni yoki EXCLUDED.soni deb aniq yozing.

5. MERGE: ikki jadvalni solishtirish

5.1 Qachon UPSERT yetmaydi

Dilnoza boshqa ilovadan ro'yxatini import qilmoqchi. Import jadvalida uchta qator bor. Kerak:

  • matni bir xil vazifa bor va holati farq qilsa — yangilash;
  • bunday vazifa yo'q bo'lsa — qo'shish;
  • importda yo'q, lekin bazada bor vazifa — o'chirish (to'liq sinxron).

ON CONFLICT faqat qo'shish yoki yangilashni biladi, o'chirishni emas. Bundan tashqari unga UNIQUE cheklov kerak, vazifalar.matn esa noyob emas. Bu ish — MERGE (birlashtirish) uchun. U SQL standartida bor va Postgres'da 15-versiyadan beri ishlaydi.

flowchart TD
  M["MERGE: manba qatori"] --> Q{"ON sharti bo'yicha<br/>juft bormi?"}
  Q -->|ha| U["WHEN MATCHED<br/>UPDATE yoki DELETE"]
  Q -->|yo'q| I["WHEN NOT MATCHED<br/>INSERT"]
  T["Jadvalda juftsiz qolgan<br/>qator"] --> D["WHEN NOT MATCHED<br/>BY SOURCE (PG 17+)"]

Diagrammaga qarang: har manba qatori uchun Postgres juft qidiradi va uch tarmoqdan biriga yuboradi. Jadvalda juftsiz qolgan qatorlar uchun alohida tarmoq bor.

5.2 Birinchi MERGE

Kanon tajribasi (PostgreSQL 18.6), BEGIN ... ROLLBACK ichida:

sql
BEGIN;
CREATE TEMP TABLE import (matn text, bajarildi boolean);
INSERT INTO import VALUES
  ('Bozordan meva olish #bozor', false),
  ('Yangi vazifa importdan #bozor', false),
  ('Rezyume yangilash #ish', true);
MERGE INTO vazifalar v
USING import i ON v.egasi_id = 2 AND v.matn = i.matn
WHEN MATCHED AND v.bajarildi <> i.bajarildi THEN
  UPDATE SET bajarildi = i.bajarildi,
    bajarilgan_vaqt = CASE WHEN i.bajarildi
      THEN '2026-10-20 12:00+05'::timestamptz END
WHEN NOT MATCHED THEN
  INSERT (egasi_id, matn, bajarildi)
  VALUES (2, i.matn, i.bajarildi)
RETURNING merge_action(), v.id, v.matn, v.bajarildi;
ROLLBACK;
text
BEGIN
CREATE TABLE
INSERT 0 3
 merge_action | id |             matn              | bajarildi
--------------+----+-------------------------------+-----------
 UPDATE       | 16 | Bozordan meva olish #bozor    | f
 INSERT       | 48 | Yangi vazifa importdan #bozor | f
 UPDATE       | 36 | Rezyume yangilash #ish        | t
(3 rows)

MERGE 3
ROLLBACK

Qatorma-qator o'qiymiz:

Qism Ma'nosi
MERGE INTO vazifalar v nishon — o'zgaradigan jadval
USING import i ON ... manba va juftlik sharti
WHEN MATCHED AND ... THEN UPDATE juft bor va holat farqli — yangilash
WHEN NOT MATCHED THEN INSERT juft yo'q — qo'shish

RETURNING merge_action() — PG 17 dan: har qatorda qaysi amal bajarilgani (INSERT, UPDATE, DELETE). bajarilgan_vaqt dagi CASE Ifodalar darsidan: bajarildi yolg'on bo'lsa, ELSE yo'q — NULL. Bu bajarildi = (bajarilgan_vaqt IS NOT NULL) cheklovini saqlaydi.

5.3 WHEN NOT MATCHED BY SOURCE (PG 17+)

To'liq sinxron uchun importda yo'q vazifalarni o'chiramiz. Biz o'zimiz sinadik (PostgreSQL 17.6), oxirida ROLLBACK — bazani buzmaslik uchun. Natijada merge_action ustuniga qarang — endi DELETE qatorlari ham bor:

sql
BEGIN;
CREATE TEMP TABLE import (matn text, bajarildi boolean);
INSERT INTO import VALUES
  ('Bozordan meva olish #bozor', false),
  ('Yangi vazifa importdan #bozor', false),
  ('Rezyume yangilash #ish', true);
MERGE INTO vazifalar v
USING import i ON v.egasi_id = 2 AND v.matn = i.matn
WHEN MATCHED AND v.bajarildi <> i.bajarildi THEN
  UPDATE SET bajarildi = i.bajarildi,
    bajarilgan_vaqt = CASE WHEN i.bajarildi
      THEN '2026-10-20 12:00+05'::timestamptz END
WHEN NOT MATCHED THEN
  INSERT (egasi_id, matn, bajarildi)
  VALUES (2, i.matn, i.bajarildi)
WHEN NOT MATCHED BY SOURCE AND v.egasi_id = 2 THEN
  DELETE
RETURNING merge_action(), v.id, v.matn;
ROLLBACK;
text
BEGIN
CREATE TABLE
INSERT 0 3
 merge_action | id |                     matn
--------------+----+-----------------------------------------------
 UPDATE       | 16 | Bozordan meva olish #bozor
 INSERT       | 49 | Yangi vazifa importdan #bozor
 UPDATE       | 36 | Rezyume yangilash #ish
 DELETE       | 26 | Dugonamning tug'ilgan kuniga sovg'a #oila
 DELETE       | 29 | Gaz to'lovi #to'lov
 DELETE       | 14 | IELTS writing task 2 mashqi #ingliz
 DELETE       |  5 | Ingliz tili: 20 ta yangi so'z #o'qish #ingliz
(7 rows)

MERGE 7
ROLLBACK

Dilnozaning importda yo'q to'rt vazifasi o'chdi. Yangi vazifa id si — 49: 48 ni oldingi, ROLLBACK qilingan MERGE olgan edi. Muhim: AND v.egasi_id = 2 sharti. Usiz boshqa foydalanuvchilarning hamma vazifalari ham "manbada juftsiz" hisoblanib o'chib ketardi. ON sharti juftlikni aniqlaydi, lekin nishon jadvalini cheklamaydi.

Diqqat: MERGE UPSERT'ning o'rnini bosmaydi. PostgreSQL hujjatiga ko'ra, ikki seans bir vaqtda bir xil qatorni MERGE bilan qo'shsa, ikkinchisi noyoblik xatosini oladi — MERGE ON CONFLICT kabi qayta urinmaydi. Parallel "bor bo'lsa yangila" uchun — INSERT ... ON CONFLICT. MERGE — import, sinxronlash, fon vazifalari uchun.

Tekshirib ko'ring: MERGE dagi ON v.egasi_id = 2 AND v.matn = i.matn shartini ON v.matn = i.matn ga almashtirsak, Bozordan meva olish #bozor qatori uchun nima o'zgarishi mumkin?

Javob

Boshqa foydalanuvchida ham shu matnli vazifa bo'lsa, u ham "juft" bo'lib qoladi va yangilanadi — Dilnozaning importi begona vazifani o'zgartiradi. Bitta manba qatoriga ikki nishon qatori mos kelsa, Postgres buni ruxsat beradi (har biri bir marta yangilanadi). Teskarisi — bitta nishon qatoriga ikki manba qatori — xato beradi. Shuning uchun ON shartida egasini doim ko'rsating.

6. Matndan teglar: SQL qadami

6.1 Teglarni ajratish funksiyasi

Teglar vazifa matnidan olinadi. Frontendda buni JavaScript regex qilardi (22-qism, TEG). Bazada xuddi shu qoida — PostgreSQL regex bilan yozilgan funksiya:

sql
CREATE FUNCTION teglarni_ajrat(matn text) RETURNS SETOF text
LANGUAGE sql IMMUTABLE PARALLEL SAFE AS $$
  SELECT DISTINCT lower(m[1])
  FROM regexp_matches(matn, '(?<![[:alnum:]_#])(#[[:alnum:]_]+(?:[''‘’][[:alnum:]_]+)*)', 'g') AS m
$$;

O'z funksiyalarimizni (trigger funksiyalari bilan) Denormalizatsiya va hisoblangan maydonlar darsida yozamiz. Hozir uni "matn berasan — teglar ro'yxatini olasan" qutisi deb qabul qiling. Regex'ning ma'nosi: # dan oldin harf, raqam, _ yoki # bo'lmasin. # dan keyin harflar keladi, apostrof (') faqat harflar orasida bo'ladi. DISTINCT takrorni olib tashlaydi — «Bitta buyruqda bir kalit ikki marta» xatosiga yo'l qo'ymaydi.

Sinab ko'ramiz:

sql
SELECT * FROM teglarni_ajrat(
  'Oshxonadagi jo''mrakni almashtirish #ta''mir #UY Non#uy ##x #o'''
);
text
 teglarni_ajrat
----------------
 #uy
 #o
 #ta'mir
(3 rows)

#UY kichik harfga o'tdi. Non#uy teg emas — # dan oldin harf bor. ##x ham teg emas. #o' — #o bo'ldi: oxiridagi apostrofdan keyin harf yo'q. SQL satrida apostrof ikkilanadi ('') — SQL tili darsidan.

6.2 Mavjud vazifalardan teglar

teglar jadvali bo'sh yaratiladi. Mavjud 40 dan ortiq vazifaning teglarini bir buyruqda to'ldiramiz — INSERT ... SELECT va ON CONFLICT DO NOTHING:

sql
INSERT INTO teglar (egasi_id, nom)
SELECT DISTINCT v.egasi_id, t.nom
FROM vazifalar v CROSS JOIN LATERAL teglarni_ajrat(v.matn) AS t(nom)
ORDER BY 1, 2
ON CONFLICT (egasi_id, nom) DO NOTHING;

CROSS JOIN LATERAL — "har vazifa uchun uning teglarini yonma-yon qo'y". Uni JOIN'lar va Subquery va LATERAL darslarida o'rganamiz. Hozir muhimi — ON CONFLICT DO NOTHING. Faylni qayta ishga tushirsangiz ham, teglar takrorlanmaydi. Natija: 32 teg va 46 bog'lanish.

Natijani tekshiramiz — Azizning teglari va har biri nechta vazifada:

sql
SELECT t.nom, count(vt.vazifa_id) AS vazifalar
FROM teglar t LEFT JOIN vazifa_teglari vt ON vt.teg_id = t.id
WHERE t.egasi_id = 1
GROUP BY t.nom ORDER BY 2 DESC, 1;
text
    nom     | vazifalar
------------+-----------
 #o'qish    |         3
 #bozor     |         1
 #docker    |         1
 #oila      |         1
 #portfolio |         1
 #ta'mir    |         1
 #to'lov    |         1
 #uy        |         1
(8 rows)

6.3 Halol eslatma

Teglar hozircha bir martalik to'ldirildi. Aziz ertaga yangi #sport tegli vazifa yozsa, teglar o'zi yangilanmaydi. Buni avtomatlashtirish — trigger bilan, Denormalizatsiya va hisoblangan maydonlar darsida. Shungacha teglar qo'lda (yoki server tomonidan UPSERT bilan) yoziladi.

7. Ko'p uchraydigan xatolar

7.1 there is no unique or exclusion constraint matching the ON CONFLICT specification

Conflict target'dagi ustunlarga aynan mos UNIQUE cheklov yo'q. Tekshiring: ustunlar to'plami bir xilmi ((egasi_id, nom) va (nom) — boshqa-boshqa), ifodali indeksda ifoda bir xilmi. Kerak bo'lsa avval cheklovni qo'shing.

7.2 ON CONFLICT DO UPDATE command cannot affect row a second time

Bitta INSERT da bir kalit ikki marta. Manbani DISTINCT (yoki DISTINCT ON va tartib) bilan tozalang.

7.3 column reference "..." is ambiguous

DO UPDATE SET ning o'ng tomonida ustun nomi jadval nomisiz yozilgan. jadval.ustun yoki EXCLUDED.ustun deb aniq yozing.

7.4 "DO NOTHING ishladi, lekin id kelmadi"

Bu xato emas — DO NOTHING o'zgarmagan qatorni qaytarmaydi. DO UPDATE SET x = EXCLUDED.x naqshi yoki keyingi SELECT kerak.

7.5 MERGE boshqalarning qatorlarini o'zgartirdi

WHEN NOT MATCHED BY SOURCE yoki keng ON sharti. O'chiradigan MERGE ni avval BEGIN ichida, RETURNING merge_action() bilan ishga tushirib ko'ring, keyin COMMIT.

8. Mashqlar

Mashq fayllari kurs/mashqlar/26/23-upsert/ da. Hammasi vazifalar bazasining 23-holatida bajariladi.

1-mashq (oson): bor tegni qayta qo'shish

Madinaga (egasi_id = 4) #oila va #sayohat teglarini qo'shing. #oila unda allaqachon bor — xato chiqmasin. RETURNING nom bilan faqat haqiqatan qo'shilgan teg chiqsin. Yordam: ON CONFLICT ... DO NOTHING — «ON CONFLICT DO NOTHING» bo'limi.

Yechim
sql
INSERT INTO teglar (egasi_id, nom)
VALUES (4, '#oila'), (4, '#sayohat')
ON CONFLICT (egasi_id, nom) DO NOTHING
RETURNING nom;
text
   nom
----------
 #sayohat
(1 row)

INSERT 0 1

#oila to'qnashdi va jim tashlandi — RETURNING faqat yangi qatorni qaytardi. INSERT 0 1 — bitta qator qo'shildi.

2-mashq (o'rta): id'lar har doim

Gulnoraga (egasi_id = 6) #bozor (bor) va #retsept (yo'q) teglarini yozing. Natijada ikkala tegning nom i va "yangimi" belgisi chiqsin. Yordam: DO UPDATE SET nom = EXCLUDED.nom va xmax = 0 — yangi qator belgisi.

Yechim
sql
INSERT INTO teglar (egasi_id, nom)
VALUES (6, '#bozor'), (6, '#retsept')
ON CONFLICT (egasi_id, nom) DO UPDATE SET nom = EXCLUDED.nom
RETURNING nom, xmax = 0 AS yangi;
text
   nom    | yangi
----------+-------
 #bozor   | f
 #retsept | t
(2 rows)

INSERT 0 2

INSERT 0 2 — Postgres yangilangan qatorni ham "ta'sir qilingan" deb sanaydi. xmax = 0 yangi qatorni ajratadi.

3-mashq (qiyin): MERGE bilan bajarilganlarni sinxronlash

Bekzod (egasi_id = 7) boshqa ilovada ikki vazifasini bajardi: Universitet kontrakti #to'lov va Yangi kitob o'qish #o'qish (ikkinchisi bazada yo'q). Vaqtinchalik bajarilganlar (matn text) jadvalini yarating. MERGE bilan: bor vazifa bajarilmagan bo'lsa — bajarildi = true va bajarilgan_vaqt = '2026-10-21 09:00+05'; yo'q bo'lsa — shu holatda qo'shilsin. Hammasi BEGIN ... ROLLBACK ichida, RETURNING merge_action(), v.matn bilan. Yordam: CHECK (bajarildi = (bajarilgan_vaqt IS NOT NULL)) — ikkala ustunni birga yozing.

Yechim
sql
BEGIN;
CREATE TEMP TABLE bajarilganlar (matn text);
INSERT INTO bajarilganlar VALUES
  ('Universitet kontrakti #to''lov'),
  ('Yangi kitob o''qish #o''qish');
MERGE INTO vazifalar v
USING bajarilganlar b ON v.egasi_id = 7 AND v.matn = b.matn
WHEN MATCHED AND NOT v.bajarildi THEN
  UPDATE SET bajarildi = true,
    bajarilgan_vaqt = '2026-10-21 09:00+05'
WHEN NOT MATCHED THEN
  INSERT (egasi_id, matn, bajarildi, bajarilgan_vaqt)
  VALUES (7, b.matn, true, '2026-10-21 09:00+05')
RETURNING merge_action(), v.matn;
ROLLBACK;
text
BEGIN
CREATE TABLE
INSERT 0 2
 merge_action |             matn
--------------+-------------------------------
 UPDATE       | Universitet kontrakti #to'lov
 INSERT       | Yangi kitob o'qish #o'qish
(2 rows)

MERGE 2
ROLLBACK

WHEN MATCHED AND NOT v.bajarildi — allaqachon bajarilgan vazifaga tegilmaydi (vaqti o'zgarmaydi). Yangi qatorda bajarilgan_vaqt ni ham berdik — aks holda vazifalar_bajarilgan_vaqt_check buzilardi. Diqqat: yangi vazifaning #o'qish tegi teglar ga o'zi tushmaydi («Halol eslatma»).

4-mashq: SQL qadami — teglar (holat 23)

  1. Loyihangizning sql/ papkasiga 006-teglar.sql faylini yarating: teglar, vazifa_teglari (tarkibiy PK (vazifa_id, teg_id), ikkala FK ON DELETE CASCADE, teg_id indeksi), teglarni_ajrat() va ikki INSERT ... ON CONFLICT DO NOTHING.
  2. Bazani noldan quring (001–006) — xatosiz o'tsin.
  3. Tekshiring: SELECT count(*) FROM teglar → 32, vazifa_teglari → 46.
  4. Commit: db: teglar (N:M) — matndagi #teglardan, ON CONFLICT bilan (006).
Yechim

Fayl tuzilmasi — «Teglar jadvali» va «Matndan teglar» bo'limlaridagi bloklar. vazifa_teglari ning to'liq ko'rinishi:

sql
CREATE TABLE vazifa_teglari (
  vazifa_id bigint NOT NULL,
  teg_id    bigint NOT NULL,
  CONSTRAINT vazifa_teglari_pkey PRIMARY KEY (vazifa_id, teg_id),
  CONSTRAINT vazifa_teglari_vazifa_id_fkey FOREIGN KEY (vazifa_id)
    REFERENCES vazifalar (id) ON DELETE CASCADE,
  CONSTRAINT vazifa_teglari_teg_id_fkey FOREIGN KEY (teg_id)
    REFERENCES teglar (id) ON DELETE CASCADE
);
CREATE INDEX vazifa_teglari_teg_id_idx ON vazifa_teglari (teg_id);

INSERT INTO vazifa_teglari (vazifa_id, teg_id)
SELECT v.id, tg.id
FROM vazifalar v
CROSS JOIN LATERAL teglarni_ajrat(v.matn) AS t(nom)
JOIN teglar tg ON tg.egasi_id = v.egasi_id AND tg.nom = t.nom
ON CONFLICT DO NOTHING;

Kanon holatida faylni ikki marta ishga tushirish ham xatosiz — ON CONFLICT DO NOTHING takroriy ishga tushirishni xavfsiz qiladi. Fayl qo'llangandan keyin o'zgartirilmaydi: keyingi o'zgarish — yangi raqamli fayl (007-...).

9. Real ishda

  • Teglar, "like"lar, ko'rishlar soni — deyarli har ilovada UPSERT bor. "Avval tekshir, keyin qo'sh" kod review'da xato deb belgilanadi.
  • Idempotent API: to'lov yoki buyurtmada idempotency_key ustuni UNIQUE bo'ladi. Takroriy so'rov ON CONFLICT DO NOTHING bilan ikkinchi marta bajarilmaydi (25 va 28-qismlarda).
  • ORM'lar: Prisma'dagi upsert va Drizzle'dagi onConflictDoUpdate ichida aynan shu SQL turadi (27-qism). SQL'ni bilsangiz, ORM nima qilayotganini tushunasiz.
  • MERGE — ma'lumot omborlari (ETL), import va kechki sinxron vazifalarida. Boshqa bazalardan (SQL Server, Oracle) ko'chib kelgan dasturchilar uni yaxshi biladi.
  • Intervyu savoli: "UPSERT'ni qanday yozasiz va nega SELECT + INSERT yomon?" — javob: poyga holati va UNIQUE indeks orqali atomik tekshiruv.

Xulosa

  • "Tekshir, keyin yoz" ikki so'rovda poyga holatini beradi — SELECT hali tasdiqlanmagan qatorni ko'rmaydi.
  • INSERT ... ON CONFLICT (target) DO NOTHING | DO UPDATE — bitta atomik buyruq; target'ga UNIQUE indeks kerak.
  • EXCLUDED — kira olmagan yangi qator; DO UPDATE ... WHERE — shart bilan yangilash.
  • UPSERT identity raqamlarini sarflaydi — id'lardagi teshiklar normal.
  • MERGE — qo'shish, yangilash va o'chirish birga; PG 17 da RETURNING merge_action() va NOT MATCHED BY SOURCE. Parallel UPSERT uchun emas.
  • vazifalar holat 23: teglar (32), vazifa_teglari (46), teglarni_ajrat().

Keyingi dars: JOIN'lar — teglar, vazifalar va foydalanuvchilarni bitta natijada birlashtiramiz.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
PostgreSQL UPSERT va MERGE: ON CONFLICT, EXCLUDED va poyga holatisiz yozish — IlmHamroh