Mundarija (36)
- Bu darsda
- 1. Nega bu kerak?
- 2. Deklarativ partitioning
- 2.1 Ota jadval va bo'limlar
- 2.2 Qator qaysi bo'limga tushadi
- 2.3 Bo'lim yo'q bo'lsa
- 2.4 RANGE, LIST va HASH
- 3. Partition pruning
- 3.1 EXPLAIN bilan ko'ramiz
- 3.2 Bo'lim kalitisiz so'rov
- 3.3 Kanon o'lchovi: 3 mln hodisa
- 3.4 Natijani halol o'qish
- 4. Eski ma'lumotni o'chirish: DELETE va DETACH
- 4.1 Nega DELETE sekin
- 4.2 DETACH + DROP
- 5. Cheklovlar
- 5.1 Noyoblik bo'lim kalitisiz bo'lmaydi
- 5.2 Boshqa cheklovlar
- 6. Bo'limlarni avtomatik yaratish
- 7. Prisma va bo'limli jadval
- 8. Ko'p uchraydigan xatolar
- 9. Mashqlar
- 1-mashq (oson): bo'shliqlarni to'ldiring
- 2-mashq (o'rta): bo'lim nomi va chegaralari
- 3-mashq (qiyin): qaysi bo'limlar o'chadi
- 10. Loyiha qadami: vazifa hodisalari jadvali
- 10.1 Nima qo'shamiz
- 10.2 Jadval va indekslar
- 10.3 Bo'lim yaratuvchi funksiyalar
- 10.4 Eski bo'limlarni tozalash
- 10.5 Trigger va huquqlar
- 10.6 DEFAULT bo'lim tuzog'i
- 10.7 Migratsiya va Prisma
- 11. Real ishda
- Xulosa
- Manbalar
Partitioning: PostgreSQL'da katta jadvalni bo'limlarga bo'lish
Qisqacha: Partitioning (bo'limlash) — bitta katta jadvalni ko'p kichik jadvalga (bo'limga) bo'lish. Tashqaridan u hamon bitta jadval:
INSERTvaSELECTodatdagidek. Postgres qatorni kalit bo'yicha (masalan, oy) kerakli bo'limga qo'yadi va so'rovda faqat kerakli bo'limlarni o'qiydi. Asosiy yutuq — eski ma'lumotni o'chirish: kanon o'lchovida bir oylik qatorlarniDELETE442.8 ms da, bo'limniDETACH+DROPesa 26.4 ms da o'chirdi. Bu darsdavazifalar-apiga oyma-oy bo'limli birinchi yangi jadvalni qo'shamiz.
Bu darsda
- Ota jadval, bo'lim va bo'lim kaliti nima ekanini tushuntirasiz.
- RANGE, LIST va HASH bo'limlashni o'z bazangizda yaratasiz.
- Partition pruning'ni
EXPLAINda ko'rasiz va qachon ishlamasligini bilasiz. - Eski oyni
DELETEemas,DETACH+DROPbilan o'chirasiz. - Bo'limli jadvalning cheklovlarini (noyoblik, DEFAULT bo'lim tuzog'i) bilasiz.
vazifalar-apigavazifa_hodisalarijadvalini migratsiya bilan qo'shasiz va Prisma'ga unga tegmaslikni aytasiz.
Oldin bilishingiz kerak: EXPLAIN ANALYZE'ni o'qish, Indeks turlari: GIN, GiST, BRIN va pg_trgm, VACUUM, autovacuum va bloat, Triggerlar, Prisma Migrate chuqur.
Oldingi ikki darsda bazani boshqa serverlarga ko'chirdik: replika va zaxira. Endi bitta server ichiga qaytamiz. Savol: jadval juda kattalashsa, uni qanday boshqaramiz?
1. Nega bu kerak?
vazifalar-api ga yangi talab keldi: foydalanuvchi o'z faolligini grafikda ko'rsin. "Bu hafta nechta vazifa qo'shdim, nechtasini bajardim?" Buning uchun har hodisani yozib borish kerak: vazifa yaratildi, bajarildi, o'chirildi. Bunday jadval faqat o'sadi. Qator yangilanmaydi, faqat qo'shiladi.
Hisoblab ko'ramiz. 2 000 faol foydalanuvchi kuniga 4–5 tadan hodisa qoldirsa, bir yilda 3 mln qator bo'ladi. Bu hali katta emas. Lekin ikki muammo darhol paydo bo'ladi:
- So'rovlar. Deyarli hamma so'rov oxirgi hafta yoki oyni so'raydi. Bir yillik jadvalning 98 % i esa hech kimga kerak emas.
- Tozalash. Maxfiylik siyosati "hodisalar 13 oy saqlanadi" deydi. Har oy eng eski oyni o'chirish kerak. 250 000 qatorni
DELETEqilish — har qator WAL'ga yoziladi, keyin VACUUM tozalashi kerak (VACUUM, autovacuum va bloat darsi).
O'xshatish: buxgalterning javoni. Hamma hujjatlarni bitta qutiga tashlasa, kerakli varaqni qidirish uchun butun qutini titkilaydi. Har oy uchun alohida jild bo'lsa — kerakli jildni oladi. Eski yil tugasa, uning jildlarini butunlay arxivga olib chiqadi, varaqma-varaq emas.
2. Deklarativ partitioning
2.1 Ota jadval va bo'limlar
Partitioning (bo'limlash) — jadvalni kalit bo'yicha qismlarga ajratish, hammasi bitta server ichida. Uch atama:
- Ota jadval (partitioned table) — o'zida qator saqlamaydi. U faqat "qoida": qaysi ustun bo'yicha va qanday bo'linadi.
- Bo'lim (partition) — oddiy jadval. Qatorlar aynan shu yerda turadi.
- Bo'lim kaliti (partition key) — qator qaysi bo'limga tushishini hal qiladigan ustun. Bizda —
vaqt.
PostgreSQL 10 dan beri bu deklarativ: siz faqat "qanday bo'lish kerak" deb aytasiz, qatorlarni Postgres o'zi tarqatadi. Undan oldin buni meros (inheritance) va triggerlar bilan qo'lda qilishardi — eski maqolalarda uchratishingiz mumkin.
Kichik misol. Oyma-oy bo'linadigan jurnal (biz ishga tushirdik, PostgreSQL 17.6):
CREATE TABLE jurnal (
vaqt timestamptz NOT NULL,
egasi_id bigint NOT NULL,
tur text NOT NULL
) PARTITION BY RANGE (vaqt);
CREATE TABLE jurnal_2026_09 PARTITION OF jurnal
FOR VALUES FROM ('2026-09-01') TO ('2026-10-01');
CREATE TABLE jurnal_2026_10 PARTITION OF jurnal
FOR VALUES FROM ('2026-10-01') TO ('2026-11-01');
CREATE TABLE jurnal_2026_11 PARTITION OF jurnal
FOR VALUES FROM ('2026-11-01') TO ('2026-12-01');CREATE TABLE
CREATE TABLE
CREATE TABLE
CREATE TABLEPARTITION BY RANGE (vaqt) — ota jadval qoidasi: vaqt oraliqlari bo'yicha. Har bo'lim FOR VALUES FROM (...) TO (...) bilan o'z oralig'ini oladi. Chegara: FROM — kiradi, TO — kirmaydi. Shuning uchun 2026-10-01 00:00 aynan oktyabr bo'limiga tushadi, sentyabrga emas. Bo'limlar orasida teshik ham, ustma-ust tushish ham bo'lmasligi kerak — ustma-ust oraliqni Postgres rad etadi.
2.2 Qator qaysi bo'limga tushadi
Uch oy davomida har soatda bitta yozuv qo'shamiz va har qator qayerga tushganini so'raymiz. tableoid — har jadvalda bor yashirin ustun: qator aslida qaysi jadvalda turibdi.
INSERT INTO jurnal
SELECT timestamptz '2026-09-01' + g * interval '1 hour',
1 + g % 3, 'yaratildi'
FROM generate_series(0, 2183) g;
SELECT tableoid::regclass AS bolim, count(*)
FROM jurnal GROUP BY 1 ORDER BY 1;INSERT 0 2184
bolim | count
----------------+-------
jurnal_2026_09 | 720
jurnal_2026_10 | 744
jurnal_2026_11 | 720
(3 rows)Biz faqat ota jadvalga yozdik. Postgres har qatorni vaqt bo'yicha o'z bo'limiga qo'ydi: sentyabr 30 kun × 24 = 720, oktyabr 31 × 24 = 744, noyabr 720.
flowchart LR
I["INSERT jurnal"] --> O{"jurnal: vaqt qaysi oraliqda?"}
O -->|"sentyabr"| B1[("jurnal_2026_09")]
O -->|"oktyabr"| B2[("jurnal_2026_10")]
O -->|"noyabr"| B3[("jurnal_2026_11")]
O -->|"hech biri"| X["XATO yoki DEFAULT"]Diagrammadagi romb — ota jadval. U o'zida qator saqlamaydi, faqat yo'naltiradi. Oxirgi strelka — eng ko'p uchraydigan tuzoq, keyingi bo'limda.
2.3 Bo'lim yo'q bo'lsa
Dekabr uchun bo'lim yaratmadik. Dekabr vaqtli qator yozib ko'ramiz:
INSERT INTO jurnal VALUES ('2026-12-01 00:00+00', 1, 'yaratildi');ERROR: no partition of relation "jurnal" found for row
DETAIL: Partition key of the failing row contains (vaqt) = (2026-12-01 00:00:00+00)."jurnal" munosabati uchun bu qatorga mos bo'lim topilmadi. DETAIL qaysi kalit qiymati mos kelmaganini ko'rsatadi. Ilovada bu — 500 xatosi va yo'qolgan hodisa. Oy almashgan kechasi bo'lim tayyor bo'lmasa, aynan shu bo'ladi.
Ikki yechim bor. Birinchisi — bo'limlarni oldindan yaratish (loyihada har kuni ikki oy oldinga). Ikkinchisi — DEFAULT bo'lim: hech qaysi oraliqqa mos kelmagan qator shu yerga tushadi.
CREATE TABLE jurnal_boshqa PARTITION OF jurnal DEFAULT;
INSERT INTO jurnal VALUES ('2026-12-01 00:00+00', 1, 'yaratildi');CREATE TABLE
INSERT 0 1Endi yozuv yo'qolmaydi. DEFAULT — xavfsizlik to'ri, asosiy yo'l emas. Uning tuzog'ini «Loyiha qadami» bo'limida o'z jadvalimizda ko'ramiz.
2.4 RANGE, LIST va HASH
| Usul | Kalit | Qachon |
|---|---|---|
| RANGE | oraliq (sana, son) | vaqt qatorlari, loglar, hodisalar |
| LIST | aniq qiymatlar ro'yxati | viloyat, mamlakat, holat |
| HASH | kalitning xesh qiymati | tekis taqsimlash, aniq tartib kerak emas |
LIST misoli — to'lovlar viloyat bo'yicha:
CREATE TABLE tolovlar (
viloyat text NOT NULL,
summa bigint NOT NULL
) PARTITION BY LIST (viloyat);
CREATE TABLE tolovlar_toshkent PARTITION OF tolovlar
FOR VALUES IN ('Toshkent', 'Toshkent viloyati');
CREATE TABLE tolovlar_vodiy PARTITION OF tolovlar
FOR VALUES IN ('Andijon', 'Farg''ona', 'Namangan');
CREATE TABLE tolovlar_boshqa PARTITION OF tolovlar DEFAULT;
INSERT INTO tolovlar VALUES ('Toshkent', 3500000),
('Namangan', 280000), ('Xorazm', 50000);
SELECT tableoid::regclass AS bolim, viloyat FROM tolovlar;CREATE TABLE
CREATE TABLE
CREATE TABLE
CREATE TABLE
INSERT 0 3
bolim | viloyat
-------------------+----------
tolovlar_vodiy | Namangan
tolovlar_toshkent | Toshkent
tolovlar_boshqa | Xorazm
(3 rows)HASH misoli — kalit xeshining 4 ga bo'linishidan qolgan qoldiq bo'yicha:
CREATE TABLE kirishlar (egasi_id bigint NOT NULL)
PARTITION BY HASH (egasi_id);
CREATE TABLE kirishlar_0 PARTITION OF kirishlar
FOR VALUES WITH (MODULUS 4, REMAINDER 0);
CREATE TABLE kirishlar_1 PARTITION OF kirishlar
FOR VALUES WITH (MODULUS 4, REMAINDER 1);
CREATE TABLE kirishlar_2 PARTITION OF kirishlar
FOR VALUES WITH (MODULUS 4, REMAINDER 2);
CREATE TABLE kirishlar_3 PARTITION OF kirishlar
FOR VALUES WITH (MODULUS 4, REMAINDER 3);
INSERT INTO kirishlar SELECT g FROM generate_series(1, 10000) g;
SELECT tableoid::regclass AS bolim, count(*)
FROM kirishlar GROUP BY 1 ORDER BY 1;CREATE TABLE
CREATE TABLE
CREATE TABLE
CREATE TABLE
CREATE TABLE
INSERT 0 10000
bolim | count
-------------+-------
kirishlar_0 | 2489
kirishlar_1 | 2527
kirishlar_2 | 2530
kirishlar_3 | 2454
(4 rows)10 000 kalit to'rtta bo'limga taxminan teng tarqaldi, lekin aniq teng emas — xesh shunday ishlaydi. HASH bo'limlashda eski ma'lumotni oyma-oy tashlab bo'lmaydi: har bo'limda har xil vaqt aralash. U bitta katta jadvalni bir necha kichik jadvalga bo'lib, VACUUM va indekslarni yengillatish uchun. Xesh bo'yicha bo'lish g'oyasi keyingi darsda — Sharding da — serverlar darajasida qaytadi.
Tekshirib ko'ring:
vazifa_hodisalariuchun qaysi usul to'g'ri — RANGE, LIST yoki HASH? Nega?
Javob
RANGE (vaqt bo'yicha). Ikki sabab: so'rovlarning deyarli hammasi vaqt oralig'ini so'raydi ("oxirgi 7 kun") — RANGE'da bu bitta yoki ikkita bo'lim. Tozalash ham vaqt bo'yicha ("13 oydan eskisi") — RANGE'da bu butun bo'limni tashlash. HASH bilan har bo'limda hamma oylar aralash bo'lardi, eski oyni yana DELETE bilan o'chirishga to'g'ri kelardi.
3. Partition pruning
3.1 EXPLAIN bilan ko'ramiz
Partition pruning (bo'limlarni kesish) — rejalovchi so'rov shartiga qarab keraksiz bo'limlarni umuman o'qimasligi. Shart bo'lim kalitiga tegishi kerak. Rejada Seq Scan on ... qatorlarini sanang — har biri bitta o'qilgan bo'lim:
EXPLAIN (COSTS OFF)
SELECT count(*) FROM jurnal
WHERE vaqt >= '2026-10-15' AND vaqt < '2026-10-20'; QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------------------
Aggregate
-> Seq Scan on jurnal_2026_10 jurnal
Filter: ((vaqt >= '2026-10-15 00:00:00+00'::timestamp with time zone) AND (vaqt < '2026-10-20 00:00:00+00'::timestamp with time zone))
(3 rows)Rejada faqat jurnal_2026_10. Qolgan uchtasi (DEFAULT bilan) ro'yxatda ham yo'q — rejalovchi ularni rejadan oldin tashladi.
3.2 Bo'lim kalitisiz so'rov
Endi shart vaqt ga emas, egasi_id ga:
EXPLAIN (COSTS OFF)
SELECT count(*) FROM jurnal WHERE egasi_id = 2; QUERY PLAN
-------------------------------------------------
Aggregate
-> Append
-> Seq Scan on jurnal_2026_09 jurnal_1
Filter: (egasi_id = 2)
-> Seq Scan on jurnal_2026_10 jurnal_2
Filter: (egasi_id = 2)
-> Seq Scan on jurnal_2026_11 jurnal_3
Filter: (egasi_id = 2)
-> Seq Scan on jurnal_boshqa jurnal_4
Filter: (egasi_id = 2)
(10 rows)Append — natijalarni ketma-ket ulash. Rejalovchi hamma bo'limni o'qiydi: egasi_id dan bo'lim haqida hech narsa bilib bo'lmaydi. Kanon 3 mln qatorli tajribada xuddi shuni ko'rdi: vazifa_id = 77 sharti bilan 12 bo'limning hammasida Parallel Seq Scan. Qoida: bo'limli jadvaldagi har tez-tez so'rovda bo'lim kaliti sharti bo'lsin. Aks holda bo'limlash yordam bermaydi, aksincha, rejalashni sekinlashtiradi: rejalovchi har bo'limni ko'rib chiqadi.
3.3 Kanon o'lchovi: 3 mln hodisa
Kanon bir xil 3 000 000 sintetik hodisani (12 oy, 2 000 foydalanuvchi) ikki jadvalga yozdi: oddiy jadval (btree indekslar) va oyma-oy bo'limli jadval (BRIN + btree). PostgreSQL 18.6, bitta mashina, Docker konteyneri. Natijalar:
| O'lchov | Oddiy | Bo'limli |
|---|---|---|
vaqt indeksi hajmi |
64 MB (btree) | 288 kB (BRIN, 12 bo'lim) |
| oxirgi 7 kun, bitta foydalanuvchi | 0.753 ms | 0.229 ms |
| bir oylik hisobot (hamma) | 40.939 ms | 33.160 ms |
| eng eski oyni o'chirish | DELETE 442.788 ms |
DETACH 9.703 + DROP 16.745 ms |
Bo'limli jadvalda oxirgi 7 kunlik so'rov rejasining yadrosi (kanon chiqishidan qisqartirildi — Buffers qatorlari olib tashlandi):
GroupAggregate (actual rows=1.00 loops=1)
-> Sort (actual rows=33.00 loops=1)
-> Bitmap Heap Scan on bolimli_2026_10 bolimli
Recheck Cond: ((egasi_id = 42) AND (vaqt >= '2026-10-24 00:00:00+00'::timestamp with time zone))
-> Bitmap Index Scan on bolimli_2026_10_egasi_id_vaqt_idx
Execution Time: 0.229 msBitta bo'lim — bolimli_2026_10. Indeks ham o'sha bo'limning o'z indeksi: ota jadvalda yaratilgan indeks har bo'limda alohida nusxa bo'lib yaratiladi.
Grafikda — so'rov vaqti oddiy jadvalga nisbatan (oddiy = 100 %):
- 7 kun, 1 foydalanuvchi30 %
- 1 oylik hisobot81 %
- eski oyni o'chirish6 %
Manba: IlmHamroh kanon o'lchovi, 2026-11-02: PostgreSQL 18.6, 3 000 000 sintetik hodisa, 12 oy; 0.229/0.753 ms, 33.160/40.939 ms, (9.703+16.745)/442.788 ms; bitta mashina, bitta yugurish
Eng katta farq — o'chirishda: 17 baravar. So'rovlarda ham yutuq bor, lekin kamroq. Keyingi bo'limda bu raqamlarni sinchiklab o'qiymiz.
3.4 Natijani halol o'qish
Raqamlarni ko'r-ko'rona olmang. Kanon chiqishida to'rtta narsa bor.
7 kunlik so'rovdagi farqning bir qismi — kesh. Oddiy jadval rejasida Buffers: shared hit=13 read=26: 26 sahifa diskdan o'qilgan. Bo'limli jadvalda — hit=33 read=3, deyarli hammasi xotiradan. Ikkala so'rov ham indeks bilan 33 qatorni topdi. 0.753 va 0.229 ms orasidagi farqning qanchasi pruning'dan, qanchasi keshdan — bu o'lchovdan ajratib bo'lmaydi.
Oylik hisobotda BRIN ishlatilmadi. Bo'limli rejada Parallel Seq Scan on bolimli_2026_03 — butun mart bo'limi o'qildi. Bu xato emas: so'rov butun oyni so'radi, bo'limdagi hamma qator kerak edi. Indeks bu yerda ortiqcha. Oddiy jadval esa 64 MB btree bo'ylab yurdi (Index Only Scan).
Skriptda kosmetik xato bor. Chiqishda bir necha marta unterminated quoted string qatori uchraydi, sarlavhalar kesilgan ("=== 2) indeks hajmlari (vaqt"). Sabab: psql'ning \echo buyrug'i bitta qo'shtirnoqni (') satr boshi deb tushunadi, "bo'lim" so'zidagi apostrof uni buzdi. So'rovlarning o'zi to'g'ri bajarildi — xabarlar faqat sarlavhalarda.
Ma'lumot sintetik va tekis. 3 mln qator vaqt bo'yicha bir tekis taqsimlangan, hammasi bitta mashinada, bitta yugurishda. Mutlaq millisekundlarni emas, nisbatni oling: o'chirish — 17 marta tez, indeks hajmi — 200 marta kichik.
Tekshirib ko'ring: Nega BRIN indeksi 64 MB o'rniga atigi 288 kB?
Javob
BRIN (Block Range INdex) har qatorni emas, har sahifalar guruhini (sukut bo'yicha 128 sahifa) eslaydi: "bu guruhdagi eng kichik va eng katta vaqt". Hodisalar vaqt tartibida yoziladi, shuning uchun har guruhning oralig'i tor va so'rov keraksiz guruhlarni tashlay oladi. btree esa har qator uchun yozuv saqlaydi. Agar qatorlar vaqt bo'yicha aralash yozilsa, BRIN foydasiz bo'lib qoladi — har guruhning oralig'i keng bo'ladi.
4. Eski ma'lumotni o'chirish: DELETE va DETACH
4.1 Nega DELETE sekin
DELETE FROM oddiy WHERE vaqt < '2025-12-01' — kanon o'lchovida 246 576 qator, 442.788 ms. Bu vaqtda Postgres har qatorni topdi, har qatorni "o'chirilgan" deb belgiladi va har o'zgarishni WAL'ga yozdi. Disk joyi bo'shamaydi: o'lik qatorlar (MVCC: Postgres ichida versiyalar) keyin VACUUM tozalaguncha turadi. Replikalar ham shu WAL'ni qayta o'ynaydi.
4.2 DETACH + DROP
Bo'limli jadvalda eng eski oy — alohida jadval. Uni ota jadvaldan uzamiz va o'chiramiz:
ALTER TABLE jurnal DETACH PARTITION jurnal_2026_09;
DROP TABLE jurnal_2026_09;
SELECT count(*) FROM jurnal;ALTER TABLE
DROP TABLE
count
-------
1465
(1 row)DETACH PARTITION — bo'lim ota jadvaldan ajraladi va oddiy mustaqil jadvalga aylanadi. Shu paytda uni o'chirish o'rniga arxivga ham chiqarish mumkin (COPY ... TO) — buni Ma'lumot saqlash muddati, arxivlash va anonimlashtirish darsida ko'ramiz. DROP TABLE esa fayllarni butunlay o'chiradi: VACUUM kerak emas, joy darhol bo'shaydi.
Oddiy DETACH PARTITION ota jadvalga qisqa, lekin kuchli qulf qo'yadi: shu lahzada jadvalga boshqa so'rovlar kutib turadi. PostgreSQL 14 dan yumshoqroq variant bor — DETACH PARTITION ... CONCURRENTLY. Hujjatga ko'ra u ichida ikki tranzaksiya ishlatadi va o'qish-yozishni to'xtatmaydi. Lekin ikki sharti bor. Bizning jurnal jadvalimizda DEFAULT bo'lim bor — sinab ko'ramiz:
ALTER TABLE jurnal DETACH PARTITION jurnal_2026_10 CONCURRENTLY;ERROR: cannot detach partitions concurrently when a default partition exists"DEFAULT bo'lim bor ekan, bo'limni parallel uzib bo'lmaydi." Biz ikkinchi shartni ham alohida jadvalda tekshirdik: BEGIN ichida bu buyruq cannot run inside a transaction block xatosini beradi (PostgreSQL 17.6). Ya'ni DEFAULT bo'limli jadvalda tanlov — oddiy DETACH va uni yuk kam paytda bajarish.
Diqqat: Oddiy
DETACHqulfi qisqa, lekin oldida uzoq so'rov turgan bo'lsa, u o'sha so'rov tugashini kutadi — orqasida esa boshqa hamma so'rovlar navbatga turadi. Qulflar valock_timeouthaqida — Xavfli migratsiyalar va qulflar darsi.
5. Cheklovlar
5.1 Noyoblik bo'lim kalitisiz bo'lmaydi
Odatda har jadvalga id PRIMARY KEY qo'yamiz. Bo'limli jadvalda bu ishlamaydi:
CREATE UNIQUE INDEX ON jurnal (egasi_id);ERROR: unique constraint on partitioned table must include all partitioning columns
DETAIL: UNIQUE constraint on table "jurnal" lacks column "vaqt" which is part of the partition key.Noyob cheklov bo'lim kalitining hamma ustunlarini o'z ichiga olishi shart. Sabab: har bo'limning indeksi alohida. Postgres "bu egasi_id boshqa bo'limda bormi?" deb tekshira oladigan global indeks qurmaydi. Kanon 3 mln qatorli jadvalda ham aynan shu xatoni oldi (lacks column "vaqt"). Kalit qo'shilsa — ishlaydi, chunki endi har qator faqat bitta bo'limga tushishi mumkin:
CREATE UNIQUE INDEX ON jurnal (egasi_id, vaqt);CREATE INDEXShuning uchun vazifa_hodisalari da PRIMARY KEY yo'q. Hodisaga tashqaridan murojaat qilinmaydi — u faqat sanaladi. Agar id bo'yicha murojaat kerak bo'lsa, PRIMARY KEY (id, vaqt) bo'ladi.
5.2 Boshqa cheklovlar
- Bo'lim kalitini o'zgartirish —
UPDATEqatorni boshqa bo'limga ko'chirishi mumkin (Postgres buni ichida DELETE + INSERT qiladi). Ishlaydi, lekin qimmat. Hodisalar jadvalidavaqthech qachon o'zgarmaydi. - Tashqi kalit — bo'limli jadvaldan boshqa jadvalga FK qo'yish mumkin. Kanon
vazifa_idga FK qo'ymadi: vazifa butunlay o'chsa ham statistika qolsin. - Ko'p bo'lim — har bo'lim alohida jadval, rejalovchi ularni ko'rib chiqadi. Minglab bo'lim rejalashni sekinlashtiradi. Oyma-oy 13 oy — 14 ta jadval (DEFAULT bilan), bu juda kam.
6. Bo'limlarni avtomatik yaratish
Bo'limlar o'z-o'zidan paydo bo'lmaydi. Kimdir har oy yangisini yaratishi va eskisini tashlashi kerak. Ikki yo'l bor.
pg_partman — shu ishni qiladigan mashhur kengaytma. Hujjatiga ko'ra u bo'limlarni oldindan yaratadi va saqlash muddati o'tganlarini o'chiradi; fon ishchisi bilan ishlaydi (sinalmagan, 2026-11). Kanon uni ishlatmadi: kengaytma pgvector/pgvector:pg18 image'ida yo'q, managed xizmatlarda esa har doim ham bor emas.
O'z funksiyalarimiz — kanon tanlovi. Uchta kichik PL/pgSQL funksiyasi va ilovadagi rejali vazifa (Cron va rejali vazifalar). Afzalligi — hech qanday kengaytma kerak emas, hamma narsa migratsiyada ko'rinadi. Funksiyalarni «Loyiha qadami» bo'limida yozamiz.
| pg_partman | O'z funksiyalarimiz | |
|---|---|---|
| O'rnatish | kengaytma kerak | yo'q |
| Imkoniyat | ko'p (sub-bo'lim, turli interval) | faqat bizga keragi |
| Kod | tayyor | ~50 qator SQL + cron |
7. Prisma va bo'limli jadval
Prisma sxemasi bo'limlashni bilmaydi: schema.prisma da PARTITION BY yozib bo'lmaydi. Jadvalni migratsiya SQL'i bilan yaratamiz. Lekin bu yerda tuzoq bor. Prisma bazani sxema bilan solishtirganda (migrate diff) sxemada yo'q jadvalni "ortiqcha" deb hisoblaydi.
Biz o'z PostgreSQL 17.6 bazamizda tekshirdik: loyihaning hamma migratsiyalari (yangi jadval bilan) qo'llandi, keyin konfiguratsiyada «tashqi jadval» belgisi olib tashlandi:
npx prisma migrate diff --from-config-datasource \
--to-schema prisma/schema.prisma --scriptLoaded Prisma config from prisma.config.ts.
-- DropTable
DROP TABLE "vazifa_hodisalari";Prisma keyingi migratsiyada bizning jadvalimizni o'chirishni taklif qildi. Kanon ham xuddi shu natijani olgan. Yechim — Prisma 7.10 dagi tables.external sozlamasi (hozircha eksperimental): bu jadval tashqarida boshqariladi, unga tegma.
experimental: { externalTables: true },
tables: { external: ["public.vazifa_hodisalari"] },Shu ikki qator bilan o'sha buyruq natijasi: -- This is an empty migration. — farq yo'q.
Kanon bo'lim jadvallarini alohida bolimlar sxemasiga qo'ydi, izohida "Prisma faqat public ni ko'radi" deb. Biz qo'shimcha sinab ko'rdik: public da qo'lda bo'lim yaratdik — Prisma 7.10.1 uni ham diff'ga qo'shmadi. Ya'ni alohida sxema Prisma uchun shart emas ekan. Lekin u baribir foydali: public jadvallar ro'yxati toza qoladi, bo'limlarga huquqlarni alohida boshqarish oson.
8. Ko'p uchraydigan xatolar
| Belgi | Sabab | Davosi |
|---|---|---|
no partition of relation ... found for row |
shu vaqt uchun bo'lim yo'q | bo'limlarni oldindan yaratish + DEFAULT |
| Rejada hamma bo'limlar | so'rovda bo'lim kaliti sharti yo'q | shartga vaqt >= ... qo'shish |
unique constraint on partitioned table must include all partitioning columns |
noyob indeksda kalit yo'q | indeksga kalit ustunini qo'shish |
updated partition constraint for default partition ... would be violated |
DEFAULT'da shu oy qatorlari bor | qatorlarni ko'chirib, keyin bo'lim yaratish |
Prisma DROP TABLE taklif qiladi |
sxemada yo'q jadval | tables.external |
DELETE sekin, disk bo'shamaydi |
eski oy qatorma-qator o'chirilyapti | DETACH + DROP |
To'rtinchi qatordagi xatoni «Loyiha qadami» bo'limida o'z ko'zimiz bilan ko'ramiz.
9. Mashqlar
Mashqlar kurs/mashqlar/31/05-partitioning/ da. 2- va 3-mashq yechimlari Node 24.21.0 da tekshirilgan (biz ishga tushirdik).
1-mashq (oson): bo'shliqlarni to'ldiring
- Qator qaysi bo'limga tushishini hal qiladigan ustun — bo'lim
- Hech qaysi oraliqqa mos kelmagan qator tushadigan bo'lim —
- Rejalovchining keraksiz bo'limlarni o'qimasligi — partition
- Bo'limni ota jadvaldan uzish buyrug'i —
ALTER TABLE ... PARTITION - Kanon o'lchovida
DELETEvaDETACH+DROPvaqti nisbati — taxminan baravar
Yechim
- kaliti — bizda
vaqt;PARTITION BY RANGE (vaqt). - DEFAULT —
PARTITION OF ... DEFAULT. - pruning — shart bo'lim kalitiga tegsa ishlaydi.
- DETACH — keyin
DROP TABLEyoki arxiv. - 17 — 442.788 / (9.703 + 16.745) ≈ 16.7.
2-mashq (o'rta): bo'lim nomi va chegaralari
Loyihadagi hodisa_bolimi_yarat(oy) funksiyasi bo'lim nomini vazifa_hodisalari_YYYY_MM shaklida tuzadi, chegaralarni esa UTC oy boshida qo'yadi. Xuddi shuni JavaScript'da yozing: bolimRejasi(sana) — Date oladi va { nom, dan, gacha } qaytaradi (dan, gacha — ISO satr). Dekabrdan keyingi oy — keyingi yil yanvari. Ishora: Date.UTC(yil, oy, 1) — oy raqami 0 dan boshlanadi va 12 bo'lsa keyingi yilga o'tadi; getUTCFullYear(), getUTCMonth(); padStart (Kesish, bo'lish va to'ldirish).
Yechim
function bolimRejasi(sana) {
const yil = sana.getUTCFullYear();
const oy = sana.getUTCMonth();
const dan = new Date(Date.UTC(yil, oy, 1));
const gacha = new Date(Date.UTC(yil, oy + 1, 1));
const mm = String(oy + 1).padStart(2, "0");
return {
nom: `vazifa_hodisalari_${yil}_${mm}`,
dan: dan.toISOString(),
gacha: gacha.toISOString(),
};
}
console.log(bolimRejasi(new Date("2026-11-03T10:00:00Z")));
console.log(bolimRejasi(new Date("2026-12-31T23:59:59Z")).gacha);
console.log(bolimRejasi(new Date("2026-12-31T20:00:00-05:00")).nom);Konsolda:
{
nom: 'vazifa_hodisalari_2026_11',
dan: '2026-11-01T00:00:00.000Z',
gacha: '2026-12-01T00:00:00.000Z'
}
2027-01-01T00:00:00.000Z
vazifa_hodisalari_2027_01Uchinchi qatorga qarang: Nyu-York vaqti bilan 31-dekabr kechqurun — UTC'da allaqachon 1-yanvar. Shuning uchun bo'lim 2027-yil yanvari. Loyiha funksiyasi ham chegarani UTC'da qo'yadi: bo'lim kaliti timestamptz, u sessiya vaqt zonasiga bog'liq emas.
3-mashq (qiyin): qaysi bo'limlar o'chadi
hodisa_bolimlari_tozala(saqlash) mantig'ini modellang. tozalanadi(nomlar, hozir, saqlashOy) — bo'lim nomlari ro'yxatini (vazifa_hodisalari_YYYY_MM), hozirgi vaqtni va saqlash muddatini (oyda) oladi. Bo'lim o'chadi, agar uning yuqori chegarasi (keyingi oyning 1-kuni) hozir dan saqlashOy oy oldingi vaqtdan katta bo'lmasa. Faqat shu shablonga mos nomlar hisobga olinadi (..._boshqa — yo'q). Ishora: nomdan yil va oyni RegExp bilan oling (RegExp), filter (filter).
Yechim
const SHABLON = /^vazifa_hodisalari_(\d{4})_(\d{2})$/;
function tozalanadi(nomlar, hozir, saqlashOy) {
const chegara = new Date(hozir);
chegara.setUTCMonth(chegara.getUTCMonth() - saqlashOy);
return nomlar.filter((nom) => {
const m = nom.match(SHABLON);
if (!m) return false;
const yuqori = Date.UTC(Number(m[1]), Number(m[2]), 1);
return yuqori <= chegara.getTime();
});
}
const bolimlar = [
"vazifa_hodisalari_2025_09",
"vazifa_hodisalari_2025_10",
"vazifa_hodisalari_2025_11",
"vazifa_hodisalari_2026_11",
"vazifa_hodisalari_boshqa",
];
const hozir = new Date("2026-11-03T03:00:00Z");
console.log(tozalanadi(bolimlar, hozir, 13));
console.log(tozalanadi(bolimlar, hozir, 12));Konsolda:
[ 'vazifa_hodisalari_2025_09' ]
[ 'vazifa_hodisalari_2025_09', 'vazifa_hodisalari_2025_10' ]Date.UTC(yil, Number(m[2]), 1) — nomdagi oy 1 dan boshlanadi, Date.UTC esa 0 dan. Shuning uchun 2025_10 uchun Date.UTC(2025, 10, 1) — 1-noyabr, ya'ni aynan bo'limning yuqori chegarasi. 13 oy: chegara 2025-10-03; oktyabr bo'limi 1-noyabrda tugaydi — hali saqlanadi. 12 oyda chegara 2025-11-03 — endi oktyabr ham o'chadi. Loyiha funksiyasi xuddi shu solishtirishni SQL'da qiladi.
10. Loyiha qadami: vazifa hodisalari jadvali
10.1 Nima qo'shamiz
Bu dars va Time-series va analitik DB'lar darsi birga loyihaning bitta bosqichini quradi. Bu darsda — ma'lumotlar bazasi qismi:
| Fayl | Nima |
|---|---|
sql/301-vazifa-hodisalari.sql |
jadval, bo'limlar, funksiyalar, trigger, huquqlar |
prisma/migrations/20261102100000_vazifa_hodisalari/migration.sql |
aynan shu SQL |
prisma.config.ts |
tables.external |
API kodi o'zgarmaydi: hodisalarni trigger yozadi. Kanon faylidagi uzun qatorlarni telefon uchun bo'ldik va izohlarni qisqartirdik (kod ma'nosi aynan). Bo'lingan nusxani o'z PostgreSQL 17.6 bazamizda bajarib tekshirdik — quyidagi har blok ostida haqiqiy chiqish. Bloklarni ketma-ket bitta 301-vazifa-hodisalari.sql fayliga yig'ing.
10.2 Jadval va indekslar
-- 301-vazifa-hodisalari.sql — vazifa hodisalari oqimi.
-- Faqat qo'shiladigan (append-only) vaqt qatori: kim, qachon, qaysi
-- vazifa, nima bo'ldi. vazifa_tarixi (010) — audit (eski/yangi
-- qiymat), bu jadval — analitika. Oyma-oy RANGE bo'limlar: eski
-- oy — DROP (DELETE emas), so'rov — faqat kerakli bo'limlar.
-- Bo'limlar ALOHIDA `bolimlar` sxemasida.
CREATE SCHEMA bolimlar;
CREATE TABLE vazifa_hodisalari (
vaqt timestamptz NOT NULL DEFAULT now(),
egasi_id bigint NOT NULL,
-- FK YO'Q: vazifa butunlay o'chsa ham hodisa (statistika) qoladi
vazifa_id bigint,
tur text NOT NULL,
malumot jsonb NOT NULL DEFAULT '{}'::jsonb,
CONSTRAINT vazifa_hodisalari_tur_check CHECK (tur IN (
'yaratildi', 'bajarildi', 'qaytarildi', 'ochirildi',
'tiklandi', 'ai_qadamlar', 'ai_savol', 'semantik_qidiruv'))
-- PRIMARY KEY yo'q: bo'limli jadvalda noyob indeks bo'lim
-- kalitini (vaqt) o'z ichiga olishi SHART
) PARTITION BY RANGE (vaqt);
-- BRIN — vaqt tartibida yoziladigan jadval uchun: kichik, oraliq
-- so'rovlari uchun yetarli
CREATE INDEX vazifa_hodisalari_vaqt_brin
ON vazifa_hodisalari USING brin (vaqt);
CREATE INDEX vazifa_hodisalari_egasi_vaqt
ON vazifa_hodisalari (egasi_id, vaqt);CREATE SCHEMA
CREATE TABLE
CREATE INDEX
CREATE INDEXtur ro'yxatida hozircha kerak bo'lmagan qiymatlar ham bor (ai_qadamlar, ai_savol, semantik_qidiruv). Ular qismning AI darslarida ishlatiladi — CHECK'ni keyin o'zgartirmaslik uchun kanon ularni oldindan qo'shgan. (egasi_id, vaqt) indeksi — "bitta foydalanuvchining oxirgi N kuni" so'rovi uchun.
10.3 Bo'lim yaratuvchi funksiyalar
-- bo'lim nomi: bolimlar.vazifa_hodisalari_YYYY_MM; chegaralar UTC
-- oy boshida (bo'lim kaliti — timestamptz, sessiya zonasidan
-- mustaqil)
CREATE FUNCTION hodisa_bolimi_yarat(oy date) RETURNS text
LANGUAGE plpgsql SECURITY DEFINER
SET search_path = pg_catalog, public AS $$
DECLARE
bosh timestamptz :=
(date_trunc('month', oy)::timestamp AT TIME ZONE 'UTC');
nom text :=
format('vazifa_hodisalari_%s', to_char(oy, 'YYYY_MM'));
BEGIN
IF to_regclass(format('bolimlar.%I', nom)) IS NULL THEN
EXECUTE format('CREATE TABLE bolimlar.%I PARTITION OF '
|| 'public.vazifa_hodisalari FOR VALUES FROM (%L) TO (%L)',
nom, bosh, bosh + interval '1 month');
END IF;
RETURN nom;
END $$;
-- joriy oy + `oldinga` oy (cron har kuni chaqiradi — idempotent);
-- DEFAULT bo'lim — kutilmagan vaqt (masalan soat noto'g'ri)
-- yo'qolmasin
CREATE FUNCTION hodisa_bolimlari_tayyorla(oldinga integer DEFAULT 2)
RETURNS SETOF text
LANGUAGE sql SECURITY DEFINER
SET search_path = pg_catalog, public AS $$
SELECT hodisa_bolimi_yarat((date_trunc('month',
now() AT TIME ZONE 'UTC') + make_interval(months => g))::date)
FROM generate_series(0, oldinga) g
$$;
CREATE TABLE bolimlar.vazifa_hodisalari_boshqa
PARTITION OF vazifa_hodisalari DEFAULT;CREATE FUNCTION
CREATE FUNCTION
CREATE TABLEUch narsaga e'tibor bering:
SECURITY DEFINER— funksiya uni yaratgan rol (migratsiya roli) huquqi bilan ishlaydi. Ilova roli jadval yarata olmaydi (Rollar va huquqlar), lekin shu funksiyani chaqirib bo'lim yarata oladi. Bunday funksiyadasearch_pathdoim qotiriladi — aks holda chaqiruvchi o'z sxemasidagi soxtaformatfunksiyasini qo'yib, uni begona huquq bilan bajartirishi mumkin.format('%I', ...)va%L— identifikator va qiymatni xavfsiz qo'shtirnoqqa oladi. Dinamik SQL'da satrni oddiy ulash — injection eshigi (Parametrli so'rovlar va SQL injection).- Idempotentlik —
to_regclass(...) IS NULLtekshiruvi: bo'lim bor bo'lsa, qayta yaratilmaydi. Cron funksiyani har kuni chaqiraveradi.
Kanon faylining oxirgi qatori — SELECT hodisa_bolimlari_tayyorla(2);. Uning natijasi sanaga bog'liq. Bizda, 2026-11-03 da:
SELECT hodisa_bolimlari_tayyorla(2); hodisa_bolimlari_tayyorla
---------------------------
vazifa_hodisalari_2026_11
vazifa_hodisalari_2026_12
vazifa_hodisalari_2027_01
(3 rows)Siz boshqa oyda bajarsangiz, boshqa nomlarni ko'rasiz. Funksiya idempotent ekanini tekshiramiz — ikkinchi chaqiruv yangi jadval yaratmaydi, faqat o'sha uchta nomni qaytaradi:
SELECT count(*) AS bolimlar FROM hodisa_bolimlari_tayyorla(2); bolimlar
----------
3
(1 row)10.4 Eski bo'limlarni tozalash
-- saqlash muddati: yuqori chegarasi `saqlash` dan eski bo'limlar —
-- DETACH + DROP (DELETE — har qator WAL'ga, VACUUM kerak)
CREATE FUNCTION hodisa_bolimlari_tozala(
saqlash interval DEFAULT interval '13 months')
RETURNS SETOF text
LANGUAGE plpgsql SECURITY DEFINER
SET search_path = pg_catalog, public AS $$
DECLARE b record;
BEGIN
FOR b IN
SELECT c.relname, pg_get_expr(c.relpartbound, c.oid) AS chegara
FROM pg_inherits i
JOIN pg_class c ON c.oid = i.inhrelid
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE i.inhparent = 'public.vazifa_hodisalari'::regclass
AND n.nspname = 'bolimlar'
AND c.relname ~ '^vazifa_hodisalari_\d{4}_\d{2}$'
ORDER BY c.relname
LOOP
IF to_date(right(b.relname, 7), 'YYYY_MM') + interval '1 month'
<= (now() AT TIME ZONE 'UTC') - saqlash THEN
EXECUTE format('ALTER TABLE public.vazifa_hodisalari '
|| 'DETACH PARTITION bolimlar.%I', b.relname);
EXECUTE format('DROP TABLE bolimlar.%I', b.relname);
RETURN NEXT b.relname;
END IF;
END LOOP;
END $$;CREATE FUNCTIONpg_inherits — "kimning bo'limi kim" katalogi. Funksiya faqat bolimlar sxemasidagi va nomi shablonga mos bo'limlarni ko'radi — DEFAULT bo'lim (..._boshqa) hech qachon o'chmaydi. Bo'lim nomidan oyni oladi va 3-mashqdagi solishtirishni qiladi. Kichik kuzatuv: so'rov chegara ustunini ham o'qiydi, lekin funksiya uni ishlatmaydi — kanon faylida shunday qolgan, zarari yo'q.
Sinab ko'ramiz: 2024-yil yanvari uchun bo'lim yaratamiz va 13 oylik tozalashni chaqiramiz:
SELECT hodisa_bolimi_yarat(date '2024-01-01');
SELECT * FROM hodisa_bolimlari_tozala('13 months'); hodisa_bolimi_yarat
---------------------------
vazifa_hodisalari_2024_01
(1 row)
hodisa_bolimlari_tozala
---------------------------
vazifa_hodisalari_2024_01
(1 row)Faqat muddati o'tgan bo'lim o'chdi. Joriy va kelgusi oylar, DEFAULT bo'lim — joyida.
10.5 Trigger va huquqlar
-- Trigger: vazifalar o'zgarishi → hodisa (API kodi o'zgarmaydi;
-- tranzaksiya bilan birga — hodisa yo'qolmaydi)
CREATE FUNCTION vazifa_hodisasi_yoz() RETURNS trigger
LANGUAGE plpgsql AS $$
DECLARE t text;
BEGIN
IF TG_OP = 'INSERT' THEN t := 'yaratildi';
ELSIF NEW.ochirilgan_vaqt IS NOT NULL
AND OLD.ochirilgan_vaqt IS NULL THEN t := 'ochirildi';
ELSIF NEW.ochirilgan_vaqt IS NULL
AND OLD.ochirilgan_vaqt IS NOT NULL THEN t := 'tiklandi';
ELSIF NEW.bajarildi AND NOT OLD.bajarildi THEN t := 'bajarildi';
ELSIF OLD.bajarildi AND NOT NEW.bajarildi THEN t := 'qaytarildi';
ELSE RETURN NULL;
END IF;
INSERT INTO vazifa_hodisalari (egasi_id, vazifa_id, tur)
VALUES (NEW.egasi_id, NEW.id, t);
RETURN NULL;
END $$;
CREATE TRIGGER vazifalar_hodisa
AFTER INSERT OR UPDATE OF bajarildi, ochirilgan_vaqt ON vazifalar
FOR EACH ROW EXECUTE FUNCTION vazifa_hodisasi_yoz();
-- Huquqlar va RLS: ilova faqat o'z hodisalarini o'qiydi/yozadi;
-- bo'lim jadvallariga to'g'ridan huquq yo'q (faqat ota orqali —
-- siyosat ishlaydi)
GRANT SELECT, INSERT ON vazifa_hodisalari TO vazifalar_ilova;
GRANT SELECT ON vazifa_hodisalari TO vazifalar_ishchi;
ALTER TABLE vazifa_hodisalari ENABLE ROW LEVEL SECURITY;
CREATE POLICY vazifa_hodisalari_egasi ON vazifa_hodisalari
TO vazifalar_ilova
USING (egasi_id = joriy_foydalanuvchi())
WITH CHECK (egasi_id = joriy_foydalanuvchi());
REVOKE EXECUTE ON FUNCTION hodisa_bolimi_yarat(date),
hodisa_bolimlari_tayyorla(integer),
hodisa_bolimlari_tozala(interval) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION hodisa_bolimi_yarat(date),
hodisa_bolimlari_tayyorla(integer)
TO vazifalar_ilova, vazifalar_ishchi;
GRANT EXECUTE ON FUNCTION hodisa_bolimlari_tozala(interval)
TO vazifalar_ishchi;
COMMENT ON TABLE vazifa_hodisalari IS
'vazifa hodisalari vaqt qatori — oylik RANGE bo''limlar '
'(bolimlar sxemasi), BRIN, 13 oy saqlanadi';CREATE FUNCTION
CREATE TRIGGER
GRANT
GRANT
ALTER TABLE
CREATE POLICY
REVOKE
GRANT
GRANT
COMMENTTrigger AFTER va FOR EACH ROW: vazifa qatori yozilgandan keyin, o'sha tranzaksiya ichida hodisa qo'shiladi. Tranzaksiya bekor bo'lsa, hodisa ham yo'qoladi — "vazifa yo'q, lekin hodisa bor" holati bo'lmaydi. UPDATE OF bajarildi, ochirilgan_vaqt — trigger faqat shu ustunlar o'zgarganda ishlaydi. Matnni tahrirlash hodisa yozmaydi. COMMENT matnidan kanondagi ichki raqamlarni olib tashladik — bu bazaga ta'sir qilmaydi. Ikki qatorga bo'lingan satr — SQL qoidasi: yangi qator bilan ajratilgan ikki satr bitta satrga qo'shiladi.
Trigger ishlayotganini namuna bazasida tekshiramiz. 1-vazifani bajarilgan qilamiz, matnini o'zgartiramiz va o'chiramiz (yumshoq o'chirish):
UPDATE vazifalar SET bajarildi = true, bajarilgan_vaqt = now()
WHERE id = 1;
UPDATE vazifalar SET matn = 'JavaScript kursi #o''qish' WHERE id = 1;
UPDATE vazifalar SET ochirilgan_vaqt = now() WHERE id = 1;
SELECT tur, vazifa_id,
tableoid::regclass::text LIKE '%boshqa' AS defaultda
FROM vazifa_hodisalari ORDER BY vaqt;UPDATE 1
UPDATE 1
UPDATE 1
tur | vazifa_id | defaultda
-----------+-----------+-----------
bajarildi | 1 | f
ochirildi | 1 | f
(2 rows)Uchta UPDATE — ikkita hodisa: matn o'zgarishi hodisa yozmadi. Ikkalasi ham joriy oy bo'limiga tushdi (defaultda = f).
10.6 DEFAULT bo'lim tuzog'i
Endi xavfli vaziyatni sinaymiz. Server ikki oydan ko'proq o'chiq turdi yoki soati noto'g'ri — hodisa bo'limi yo'q oyga keldi. U DEFAULT bo'limga tushadi. Keyin cron shu oy uchun bo'lim yaratmoqchi bo'ladi:
\set VERBOSITY terse
INSERT INTO vazifa_hodisalari (vaqt, egasi_id, tur)
VALUES ('2030-06-01', 1, 'yaratildi');
SELECT hodisa_bolimi_yarat(date '2030-06-01');INSERT 0 1
ERROR: updated partition constraint for default partition "vazifa_hodisalari_boshqa" would be violated by some rowPostgres yangi bo'lim yaratganda DEFAULT bo'limni tekshiradi: unda yangi oraliqqa tegishli qator bo'lmasligi kerak. Bor ekan — rad etdi. Kanondagi Nest xizmati (uni Time-series darsida yozamiz) bu xatoni ushlab, faqat ogohlantirish yozadi. Natija: shu oyning hamma hodisalari DEFAULT bo'limga tushaveradi, cron har kuni ogohlantirish yozaveradi. Pruning bu qatorlar uchun ishlamaydi, tozalash funksiyasi DEFAULT'ga tegmaydi.
Bu — loyihaning ochiq xatosi, uni yashirmaymiz. Ilova to'xtamasligi to'g'ri, lekin xatoni jim yutish noto'g'ri: log'dagi ogohlantirishni hech kim o'qimasligi mumkin. To'g'ri yo'l ikki qadam. Birinchisi — xato metrika yoki alert bo'lib chiqsin (buni Observability darsida o'rganamiz). Ikkinchisi — quyidagi qo'lda tuzatish bitta tranzaksiyali avtomatik amalga aylansin. Kanonda bu hali tuzatilmagan — hozircha yo'l: qo'lda tuzatish va monitoring.
Tuzatish — qo'lda, bitta tranzaksiyada: DEFAULT'ni uzamiz, bo'limni yaratamiz, qatorlarni ko'chiramiz, DEFAULT'ni qaytaramiz.
BEGIN;
ALTER TABLE vazifa_hodisalari
DETACH PARTITION bolimlar.vazifa_hodisalari_boshqa;
SELECT hodisa_bolimi_yarat(date '2030-06-01');
INSERT INTO vazifa_hodisalari
SELECT * FROM bolimlar.vazifa_hodisalari_boshqa;
TRUNCATE bolimlar.vazifa_hodisalari_boshqa;
ALTER TABLE vazifa_hodisalari ATTACH PARTITION
bolimlar.vazifa_hodisalari_boshqa DEFAULT;
COMMIT;
SELECT tableoid::regclass AS bolim, tur
FROM vazifa_hodisalari WHERE vaqt >= '2030-01-01';BEGIN
ALTER TABLE
hodisa_bolimi_yarat
---------------------------
vazifa_hodisalari_2030_06
(1 row)
INSERT 0 1
TRUNCATE TABLE
ALTER TABLE
COMMIT
bolim | tur
------------------------------------+-----------
bolimlar.vazifa_hodisalari_2030_06 | yaratildi
(1 row)Nima bo'ldi? DETACH dan keyin DEFAULT oddiy jadvalga aylandi va endi yangi bo'limga xalaqit bermadi. INSERT ... SELECT qatorni ota jadval orqali qayta yozdi — Postgres uni o'zi yangi bo'limga qo'ydi. Hammasi bitta tranzaksiyada: o'rtada xato bo'lsa, hech narsa o'zgarmaydi. Narxi — DETACH va ATTACH paytida jadvalga qisqa kuchli qulf, shuning uchun buni yuk kam paytda qiling.
Qator o'z bo'limiga ko'chdi. Saboq: DEFAULT bo'lim bo'sh turishi kerak. Monitoringga bitta so'rov qo'shing — SELECT count(*) FROM bolimlar.vazifa_hodisalari_boshqa — va noldan katta bo'lsa ogohlantirsin (Monitoring va favqulodda vaziyatlar).
Tajribadan keyin tozalaymiz:
DROP TABLE bolimlar.vazifa_hodisalari_2030_06;
DELETE FROM vazifa_hodisalari;DROP TABLE
DELETE 210.7 Migratsiya va Prisma
Shu SQL'ning aynan nusxasi — yangi migratsiya: prisma/migrations/20261102100000_vazifa_hodisalari/migration.sql. sql/ papkasi va migratsiyalar bir xil sxemani qurishi kerak — 27-qismdan beri shu qoida. prisma.config.ts ga, migrations qatoridan keyin, ikki qator (izoh bilan):
+ // bo'limli jadval (sql/301) — Prisma bo'limlashni boshqarmaydi:
+ // jadval migratsiya SQL'i bilan yaratiladi, Prisma unga TEGMAYDI
+ // (migrate diff uni "ortiqcha" deb DROP yozmaydi). Bo'limlar —
+ // `bolimlar` sxemasida.
+ experimental: { externalTables: true },
+ tables: { external: ["public.vazifa_hodisalari"] },
...(url === undefined ? {} : { datasource: { url } }),Docker'siz tekshiruv (o'z lokal Postgres'ingiz, 26-qismdagi): yangi bazaga npx prisma migrate deploy, keyin «Prisma va bo'limli jadval» bo'limidagi migrate diff buyrug'i. Kutilgan natija — -- This is an empty migration. Kanon tekshiruvida ham shunday: migratsiyalar 21, farq [], sql/ bilan farq 0 qator.
Hali commit qilmang. Bo'limlarni har kuni oldindan yaratadigan Nest xizmati va GET /v1/statistika/faollik yo'li — Time-series va analitik DB'lar darsida. Commit — o'sha darsda.
11. Real ishda
- Qayerda uchraydi: loglar, audit, to'lov tarixi, IoT o'lchovlari, sahifa ko'rishlar — vaqt bo'yicha o'sadigan har jadval. Katta SaaS'larda oyma-oy yoki kunma-kun bo'limlar odatiy.
- Intervyu savollari: "Partition pruning nima va qachon ishlamaydi?", "Nega bo'limli jadvalda global unique yo'q?", "Eski ma'lumotni qanday tez o'chirasiz?", "DEFAULT bo'limning xavfi?"
- Vositalar: pg_partman (bo'limlarni avtomatlashtirish), TimescaleDB (avtomatik vaqt bo'limlari — Time-series va analitik DB'lar darsida).
- Kod review'da: "bu so'rovda
vaqtsharti yo'q — hamma bo'limni o'qiydi", "DEFAULT bo'lim bo'sh ekanini kim tekshiradi?"
Xulosa
- Partitioning — bitta mantiqiy jadval, ichida ko'p jismoniy bo'lim; qatorni bo'lim kaliti joylaydi.
- RANGE — vaqt qatorlari uchun, LIST — aniq qiymatlar, HASH — tekis taqsimlash.
- Pruning faqat so'rovda bo'lim kaliti sharti bo'lsa ishlaydi.
- Eski oy —
DETACH+DROP: kanondaDELETEdan ~17 baravar tez, VACUUM kerak emas. - Noyob indeks bo'lim kalitini o'z ichiga olishi shart; DEFAULT bo'lim bo'sh turishi kerak.
- Prisma bo'limli jadvalni bilmaydi —
tables.externalbilan unga tegmaslikni aytamiz.
Keyingi dars: Sharding va taqsimlangan SQL — ma'lumot bitta serverga sig'may qolsa, uni serverlarga bo'lamiz va bu qanchalik qimmat ekanini o'lchaymiz.
Manbalar
- PostgreSQL 18 hujjati — Table Partitioning: https://www.postgresql.org/docs/18/ddl-partitioning.html
- PostgreSQL 18 hujjati — ALTER TABLE (ATTACH/DETACH PARTITION, CONCURRENTLY): https://www.postgresql.org/docs/18/sql-altertable.html
- PostgreSQL 18 hujjati — BRIN Indexes: https://www.postgresql.org/docs/18/brin.html
- pg_partman hujjati: https://github.com/pgpartman/pg_partman/blob/master/doc/pg_partman.md (sinalmagan, 2026-11)
- Prisma hujjati — External tables (Preview,
experimental.externalTables): https://www.prisma.io/docs/orm/v7/prisma-schema/data-model/externally-managed-tables - Kanon o'lchovi (IlmHamroh, 2026-11-02): PostgreSQL 18.6 (
pgvector/pgvector:pg18, Docker), 3 000 000 sintetik hodisa, Prisma 7.10.1, Node 24.21.0. SQL bloklari,migrate diffva mashq yechimlari — biz ishga tushirdik (2026-11-03, PostgreSQL 17.6, Prisma 7.10.1, Node 24.21.0).
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!