IlmHamroh
JavaScript Full-stack/31-qism. Prodakshn backend va AI integratsiya5/34-dars29 daqiqa
Mundarija (36)

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: INSERT va SELECT odatdagidek. 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 qatorlarni DELETE 442.8 ms da, bo'limni DETACH + DROP esa 26.4 ms da o'chirdi. Bu darsda vazifalar-api ga 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 EXPLAIN da ko'rasiz va qachon ishlamasligini bilasiz.
  • Eski oyni DELETE emas, DETACH + DROP bilan o'chirasiz.
  • Bo'limli jadvalning cheklovlarini (noyoblik, DEFAULT bo'lim tuzog'i) bilasiz.
  • vazifalar-api ga vazifa_hodisalari jadvalini 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:

  1. So'rovlar. Deyarli hamma so'rov oxirgi hafta yoki oyni so'raydi. Bir yillik jadvalning 98 % i esa hech kimga kerak emas.
  2. Tozalash. Maxfiylik siyosati "hodisalar 13 oy saqlanadi" deydi. Har oy eng eski oyni o'chirish kerak. 250 000 qatorni DELETE qilish — 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):

sql
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');
text
CREATE TABLE
CREATE TABLE
CREATE TABLE
CREATE TABLE

PARTITION 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.

sql
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;
text
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:

sql
INSERT INTO jurnal VALUES ('2026-12-01 00:00+00', 1, 'yaratildi');
text
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.

sql
CREATE TABLE jurnal_boshqa PARTITION OF jurnal DEFAULT;
INSERT INTO jurnal VALUES ('2026-12-01 00:00+00', 1, 'yaratildi');
text
CREATE TABLE
INSERT 0 1

Endi 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:

sql
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;
text
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:

sql
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;
text
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_hodisalari uchun 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:

sql
EXPLAIN (COSTS OFF)
SELECT count(*) FROM jurnal
 WHERE vaqt >= '2026-10-15' AND vaqt < '2026-10-20';
text
                                                                   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:

sql
EXPLAIN (COSTS OFF)
SELECT count(*) FROM jurnal WHERE egasi_id = 2;
text
                   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):

text
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 ms

Bitta 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 %):

Bo'limli jadval so'rov vaqti: oddiy jadval = 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:

sql
ALTER TABLE jurnal DETACH PARTITION jurnal_2026_09;
DROP TABLE jurnal_2026_09;
SELECT count(*) FROM jurnal;
text
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:

sql
ALTER TABLE jurnal DETACH PARTITION jurnal_2026_10 CONCURRENTLY;
text
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 DETACH qulfi 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 va lock_timeout haqida — 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:

sql
CREATE UNIQUE INDEX ON jurnal (egasi_id);
text
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:

sql
CREATE UNIQUE INDEX ON jurnal (egasi_id, vaqt);
text
CREATE INDEX

Shuning 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 — UPDATE qatorni boshqa bo'limga ko'chirishi mumkin (Postgres buni ichida DELETE + INSERT qiladi). Ishlaydi, lekin qimmat. Hodisalar jadvalida vaqt hech qachon o'zgarmaydi.
  • Tashqi kalit — bo'limli jadvaldan boshqa jadvalga FK qo'yish mumkin. Kanon vazifa_id ga 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:

bash
npx prisma migrate diff --from-config-datasource \
  --to-schema prisma/schema.prisma --script
text
Loaded 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.

ts
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 DELETE va DETACH + DROP vaqti 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 TABLE yoki 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
js
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:

text
{
  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_01

Uchinchi 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
js
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:

text
[ '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

sql
-- 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);
text
CREATE SCHEMA
CREATE TABLE
CREATE INDEX
CREATE INDEX

tur 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

sql
-- 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;
text
CREATE FUNCTION
CREATE FUNCTION
CREATE TABLE

Uch 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 funksiyada search_path doim qotiriladi — aks holda chaqiruvchi o'z sxemasidagi soxta format funksiyasini 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 NULL tekshiruvi: 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:

sql
SELECT hodisa_bolimlari_tayyorla(2);
text
 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:

sql
SELECT count(*) AS bolimlar FROM hodisa_bolimlari_tayyorla(2);
text
 bolimlar
----------
        3
(1 row)

10.4 Eski bo'limlarni tozalash

sql
-- 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 $$;
text
CREATE FUNCTION

pg_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:

sql
SELECT hodisa_bolimi_yarat(date '2024-01-01');
SELECT * FROM hodisa_bolimlari_tozala('13 months');
text
    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

sql
-- 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';
text
CREATE FUNCTION
CREATE TRIGGER
GRANT
GRANT
ALTER TABLE
CREATE POLICY
REVOKE
GRANT
GRANT
COMMENT

Trigger 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):

sql
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;
text
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:

sql
\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');
text
INSERT 0 1
ERROR:  updated partition constraint for default partition "vazifa_hodisalari_boshqa" would be violated by some row

Postgres 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.

sql
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';
text
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:

sql
DROP TABLE bolimlar.vazifa_hodisalari_2030_06;
DELETE FROM vazifa_hodisalari;
text
DROP TABLE
DELETE 2

10.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):

diff
+  // 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 vaqt sharti 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: kanonda DELETE dan ~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.external bilan 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

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Partitioning: PostgreSQL'da katta jadvalni bo'limlarga bo'lish — IlmHamroh