Mundarija (32)
- Bu darsda
- 1. Nega bu kerak?
- 2. OLTP va OLAP
- 2.1 Ikki xil yuk
- 2.2 Postgres qachon yetadi
- 3. Kunlik agregat
- 3.1 Namuna ma'lumot
- 3.2 Kun qaysi zonada
- 3.3 Kunlik sanash
- 3.4 Bo'sh kunlarni to'ldirish
- 3.5 Harakatlanuvchi o'rtacha
- 4. Jamlanma (rollup) jadval
- 4.1 G'oya
- 4.2 Kanon o'lchovi
- 4.3 Natijani halol o'qish
- 4.4 Jamlanmani yangilab turish
- 5. Ixtisoslashgan bazalar
- 6. Ko'p uchraydigan xatolar
- 7. Mashqlar
- 1-mashq (oson): bo'shliqlarni to'ldiring
- 2-mashq (o'rta): API javobidagi bo'sh kunlarni to'ldirish
- 3-mashq (qiyin): inkremental jamlanma
- 8. Loyiha qadami: statistika moduli va commit
- 8.1 Nima qo'shamiz
- 8.2 statistika.module.ts
- 8.3 Ulash
- 8.4 Test
- 8.5 Shu bosqichdagi test tuzatishlari
- 8.6 Tekshiruv va commit
- 9. Real ishda
- Xulosa
- Manbalar
Time-series va analitik DB'lar: hodisalardan faollik grafigi
Qisqacha: Time-series (vaqt qatori) — har yozuvi vaqt belgisi bilan keladigan, faqat qo'shiladigan ma'lumot: hodisalar, o'lchovlar, sahifa ko'rishlar. Unda deyarli har so'rov "vaqt oralig'i bo'yicha guruhlab sanash". Kanon o'lchovida butun tizimning 11 oylik kunlik grafigi xom jadvaldan 2 296 ms, oldindan yig'ilgan kunlik jamlanmadan 196 ms oldi. Lekin bitta foydalanuvchi grafigida jamlanma aksincha sekinroq chiqdi (7.1 va 3.2 ms) — shuning uchun loyihada u yo'q. Bu darsda
GET /v1/statistika/faollikyo'lini qo'shamiz va loyihaning shu bosqichini commit qilamiz.
Bu darsda
- OLTP va OLAP farqini va analitik bazalar nima uchun boshqacha qurilishini tushuntirasiz.
- Kunlik agregatni to'g'ri vaqt zonasida, bo'sh kunlari bilan hisoblaysiz.
- Jamlanma (rollup) jadval qachon kerak va qachon ortiqcha ekanini kanon o'lchovi bilan asoslaysiz.
- TimescaleDB, ClickHouse va DuckDB qayerda kerak bo'lishini bilasiz.
vazifalar-apiga statistika moduli va bo'limlarni tayyorlovchi cron xizmatini qo'shasiz.
Oldin bilishingiz kerak: Partitioning, SQL'da sana va vaqt zonalari, Window funksiyalar 2: qiymat va frame'lar, View va materialized view, Rejalashtirish: @nestjs/schedule.
Partitioning darsida vazifa_hodisalari jadvalini qurdik va trigger hodisalarni yoza boshladi. Hali hech kim ularni o'qimaydi. Bu darsda o'qiymiz — grafik uchun.
1. Nega bu kerak?
Aziz ilovada "Faolligim" sahifasini ochadi: oxirgi 30 kun, har kun nechta vazifa qo'shgan va bajargan. Ustunli grafik. Bu oddiy talab ikki savol tug'diradi:
- Qayerdan sanaymiz?
vazifalarjadvali joriy holatni saqlaydi: vazifa hozir bajarilganmi. Lekin "o'tgan seshanba nechta vazifa bajarilgan edi" — bu tarix. Uni faqat hodisalardan bilish mumkin. - Qanchalik tez? Bitta foydalanuvchi uchun — oson. Admin uchun "butun tizim, butun yil, kunma-kun" — millionlab qator.
O'xshatish: do'kon kassasi. Har sotuv — chekka yoziladi (hodisa). Kechqurun kassir kun yakunini hisoblaydi (agregat). Oy oxirida buxgalter har kunning yakunini oladi, millionta chekni qayta sanamaydi (jamlanma).
2. OLTP va OLAP
2.1 Ikki xil yuk
| OLTP | OLAP | |
|---|---|---|
| To'liq nomi | Online Transaction Processing | Online Analytical Processing |
| Tipik so'rov | bitta yozuvni o'qish/yozish | millionlab qatorni guruhlash |
| Misol | "Aziz vazifa qo'shdi" | "oy bo'yicha jami bajarilganlar" |
| Saqlash | qatorlar (row-oriented) | ustunlar (column-oriented) |
Postgres — OLTP bazasi: har qator diskda yaxlit turadi. "Bitta vazifani o'qish" uchun bu ideal — bitta sahifa. "Hamma qatorning tur ustunini sanash" uchun esa yomon: har qatorning hamma ustunlari o'qiladi, kerak bo'lgani bitta.
Ustunli saqlash (columnar storage) — har ustun alohida saqlanadi. Hisobot so'rovi faqat kerakli ustunlarni o'qiydi, bir xil qiymatlar yonma-yon bo'lgani uchun yaxshi siqiladi. ClickHouse, DuckDB va boshqa analitik bazalar shunday quriladi.
Farqni kichik modelda ko'ramiz. Oltita hodisa va bitta savol: "nechta vazifa bajarilgan?" Chiqishda "o'qilgan qiymat" soniga qarang:
const qatorlar = [
{ vaqt: "09:00", egasi: 1, tur: "yaratildi", vazifa: 11 },
{ vaqt: "09:37", egasi: 1, tur: "yaratildi", vazifa: 12 },
{ vaqt: "10:14", egasi: 2, tur: "yaratildi", vazifa: 13 },
{ vaqt: "10:51", egasi: 1, tur: "bajarildi", vazifa: 11 },
{ vaqt: "11:28", egasi: 2, tur: "bajarildi", vazifa: 13 },
{ vaqt: "12:05", egasi: 1, tur: "bajarildi", vazifa: 12 },
];
// qatorli: "bajarildi"larni sanash uchun har qator to'liq o'qiladi
let oqildi = 0;
let soni = 0;
for (const q of qatorlar) {
oqildi += Object.keys(q).length;
if (q.tur === "bajarildi") soni++;
}
console.log("qatorli:", soni, "ta, o'qilgan qiymat:", oqildi);
// ustunli: har ustun — alohida massiv, faqat `tur` o'qiladi
const turlar = qatorlar.map((q) => q.tur);
console.log("ustunli:",
turlar.filter((t) => t === "bajarildi").length,
"ta, o'qilgan qiymat:", turlar.length);
// yonma-yon bir xil qiymatlar — "nechta ketma-ket" bilan siqiladi
const siqilgan = [];
for (const t of turlar) {
const oxirgi = siqilgan.at(-1);
if (oxirgi && oxirgi[0] === t) oxirgi[1]++;
else siqilgan.push([t, 1]);
}
console.log("siqilgan tur ustuni:", siqilgan);Konsolda:
qatorli: 3 ta, o'qilgan qiymat: 24
ustunli: 3 ta, o'qilgan qiymat: 6
siqilgan tur ustuni: [ [ 'yaratildi', 3 ], [ 'bajarildi', 3 ] ]Nima bo'ldi? Javob bir xil — 3 ta. Lekin qatorli saqlashda 24 ta qiymat o'qildi, ustunlida — 6 ta: to'rt baravar kam. Millionlab qatorda bu farq disk va xotirada soniyalarga aylanadi. Oxirgi qator — siqish: oltita satr o'rniga ikki juft "qiymat va soni". Haqiqiy ustunli bazalar ancha murakkab siqish ishlatadi, lekin g'oya shu.
Tekshirib ko'ring: "Aziz 11-vazifani qachon bajardi?" — bu so'rovga qaysi saqlash qulayroq?
Javob
Qatorli. Bu bitta yozuvni topib, uning bir nechta maydonini o'qish — OLTP so'rovi. Ustunli bazada esa bitta "qator"ni yig'ish uchun to'rtta alohida ustundan bittadan qiymat olish kerak bo'ladi. Shuning uchun ilovaning asosiy bazasi qatorli (Postgres), hisobot bazasi esa ustunli bo'lishi mumkin.
2.2 Postgres qachon yetadi
Yaxshi xabar: kichik va o'rta loyihalarda Postgres analitikani ham ko'taradi — to'g'ri tuzilgan bo'lsa. Ya'ni: vaqt bo'yicha bo'limlar (pruning — Partitioning darsidagi "keraksiz bo'limlarni o'qimaslik"), to'g'ri indeks va kerak bo'lsa oldindan yig'ilgan jamlanma. Alohida analitik baza — yangi xizmat, yangi sinxronlash (CDC va ma'lumot sinxronlash), yangi zaxira. Unga sabab bo'lishi kerak.
3. Kunlik agregat
3.1 Namuna ma'lumot
Loyiha sxemasi qurilgan bazada (sql/ fayllari, shu jumladan 301) oktyabr oyi uchun bo'lim yaratamiz va Aziz uchun sintetik hodisalar yozamiz (biz ishga tushirdik, PostgreSQL 17.6):
SELECT hodisa_bolimi_yarat(date '2026-10-01');
INSERT INTO vazifa_hodisalari (vaqt, egasi_id, tur)
SELECT timestamptz '2026-10-01 04:00+00'
+ (d - 1) * interval '1 day' + k * interval '37 minutes',
1,
CASE WHEN k % 3 = 0 THEN 'bajarildi' ELSE 'yaratildi' END
FROM generate_series(1, 31) d,
generate_series(0, 5) k
WHERE d % 6 <> 0 AND k < 1 + d % 5;
SELECT count(*) FROM vazifa_hodisalari; hodisa_bolimi_yarat
---------------------------
vazifa_hodisalari_2026_10
(1 row)
INSERT 0 77
count
-------
77
(1 row)Har kuni bir nechta hodisa: soat 09:00 dan (Toshkent vaqti) boshlab har 37 daqiqada. Har oltinchi kun (6, 12, 18 …) — bo'sh: Aziz ilovani ochmagan.
3.2 Kun qaysi zonada
Kun chegarasi muhim. Toshkent UTC'dan 5 soat oldinda. UTC bo'yicha 14-oktyabr 20:30 — Toshkentda allaqachon 15-oktyabr, 01:30:
SELECT (t AT TIME ZONE 'UTC')::date AS utc_kun,
(t AT TIME ZONE 'Asia/Tashkent')::date AS toshkent_kun
FROM (VALUES (timestamptz '2026-10-14 20:30+00')) x(t); utc_kun | toshkent_kun
------------+--------------
2026-10-14 | 2026-10-15
(1 row)Foydalanuvchi uchun "kun" — uning kuni. Kanon har agregatda vaqt AT TIME ZONE <mintaqa> ishlatadi; mintaqa ilova sozlamasidan (APP_TZ, sukut Asia/Tashkent). Nega '+05:00' emas, mintaqa nomi — SQL'da sana va vaqt zonalari darsidan: Postgres '+05:00' ni POSIX qoidasiga ko'ra teskari ishora bilan o'qiydi.
Tekshirib ko'ring: Malika har kuni soat 23:30 da (Toshkent vaqti) vazifalarini yopadi. Agregat UTC kuni bo'yicha qilinsa, uning grafigida nima ko'rinadi?
Javob
Toshkentda 23:30 — UTC bo'yicha 18:30, o'sha kunning o'zi. Bu holatda kun to'g'ri chiqadi. Muammo tunda ishlaganda boshlanadi: Toshkentda 01:30 da yopilgan vazifa UTC bo'yicha hali oldingi kunning 20:30 i — grafikda kechagi kunga tushadi. Ya'ni Toshkentdagi 00:00–05:00 oralig'idagi har hodisa bir kun oldinga siljiydi. Shuning uchun agregat doim foydalanuvchi mintaqasida.
3.3 Kunlik sanash
SELECT (vaqt AT TIME ZONE 'Asia/Tashkent')::date AS kun,
count(*) FILTER (WHERE tur = 'yaratildi') AS yaratildi,
count(*) FILTER (WHERE tur = 'bajarildi') AS bajarildi
FROM vazifa_hodisalari
WHERE egasi_id = 1
AND vaqt >= '2026-10-01' AND vaqt < '2026-10-08'
GROUP BY 1 ORDER BY 1; kun | yaratildi | bajarildi
------------+-----------+-----------
2026-10-01 | 1 | 1
2026-10-02 | 2 | 1
2026-10-03 | 2 | 2
2026-10-04 | 3 | 2
2026-10-05 | 0 | 1
2026-10-07 | 2 | 1
(6 rows)count(*) FILTER (WHERE ...) — bitta o'tishda bir nechta shartli sanoq. 6-oktyabr ro'yxatda yo'q: o'sha kuni hodisa bo'lmagan. Grafik uchun bu muammo — ustun tushib qoladi va o'qi siljiydi.
3.4 Bo'sh kunlarni to'ldirish
Yechim — kunlar ro'yxatini generate_series bilan yasab, hodisalarni unga LEFT JOIN qilish:
WITH kunlar AS (
SELECT d::date AS kun
FROM generate_series(date '2026-10-01', date '2026-10-07',
interval '1 day') d
), sanoq AS (
SELECT (vaqt AT TIME ZONE 'Asia/Tashkent')::date AS kun,
count(*) AS n
FROM vazifa_hodisalari
WHERE egasi_id = 1
AND vaqt >= '2026-10-01' AND vaqt < '2026-10-08'
GROUP BY 1
)
SELECT k.kun, coalesce(s.n, 0) AS hodisalar
FROM kunlar k LEFT JOIN sanoq s USING (kun)
ORDER BY k.kun; kun | hodisalar
------------+-----------
2026-10-01 | 2
2026-10-02 | 3
2026-10-03 | 4
2026-10-04 | 5
2026-10-05 | 1
2026-10-06 | 0
2026-10-07 | 3
(7 rows)Endi 6-oktyabr bor — nol bilan. Kanon API'si bu ishni qilmaydi: u faqat hodisa bo'lgan kunlarni qaytaradi va bo'sh kunlarni to'ldirish mijozga qoladi. Bu 2-mashq.
3.5 Harakatlanuvchi o'rtacha
Kunlik raqamlar sakraydi. Tendensiyani ko'rish uchun harakatlanuvchi o'rtacha (moving average) — oxirgi 7 kunning o'rtachasi. Window funksiyalar 2 darsidagi ROWS BETWEEN:
WITH kunlik AS (
SELECT (vaqt AT TIME ZONE 'Asia/Tashkent')::date AS kun,
count(*) AS n
FROM vazifa_hodisalari WHERE egasi_id = 1
GROUP BY 1
)
SELECT kun, n,
round(avg(n) OVER (ORDER BY kun
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW), 2) AS ortacha_7
FROM kunlik
ORDER BY kun DESC LIMIT 5; kun | n | ortacha_7
------------+---+-----------
2026-10-31 | 2 | 3.00
2026-10-29 | 5 | 3.14
2026-10-28 | 4 | 2.71
2026-10-27 | 3 | 2.29
2026-10-26 | 2 | 2.57
(5 rows)Halol eslatma: ROWS — "oldingi 6 qator", "6 kun" emas. Bo'sh kunlar qatorlar ichida yo'q, shuning uchun bu oyna ba'zan 7 kundan uzunroq davrni qamraydi. Aniq 7 kalendar kun kerak bo'lsa — avval bo'sh kunlarni to'ldiring yoki RANGE BETWEEN interval '6 days' PRECEDING AND CURRENT ROW yozing.
Tekshirib ko'ring: Yuqoridagi natijada 31-oktyabr qatorining 7 kunlik oynasi qaysi kunlarni qamrab oldi?
Javob
Oxirgi 7 ta qatorni: 31, 29, 28, 27, 26, 25 va 24-oktyabr. 30-oktyabr — bo'sh kun (har oltinchi kun), u qator sifatida yo'q. Demak oyna 8 kalendar kunni qamradi va o'rtacha 7 ta qiymatdan olindi — 3.00. Bo'sh kunni 0 deb hisoblasak, haqiqiy 7 kunlik o'rtacha boshqacha chiqardi.
4. Jamlanma (rollup) jadval
4.1 G'oya
Jamlanma (rollup) — xom hodisalarni oldindan kunma-kun yig'ib, alohida jadvalda saqlash. Grafik endi millionlab hodisani emas, kuniga bir necha qatorni o'qiydi. Narxi: qo'shimcha jadval, uni yangilab turish va xom ma'lumot bilan farq qilib qolish xavfi.
4.2 Kanon o'lchovi
Kanon partitioning tajribasidagi 3 mln hodisa ustida ikki so'rovni o'lchadi: butun tizim grafigi va bitta foydalanuvchi grafigi. Jamlanma — (kun, egasi_id, tur) bo'yicha count:
CREATE TABLE kunlik AS
SELECT (vaqt AT TIME ZONE 'Asia/Tashkent')::date AS kun,
egasi_id, tur, count(*)::int AS soni
FROM bolimli GROUP BY 1, 2, 3;
CREATE INDEX ON kunlik (egasi_id, kun);Kanon chiqishi (qisqartirildi):
=== 8) #09 time-series: 12 oylik kunlik grafik — xom agregat vs kunlik jamlanma (rollup) jadval
kunlar
--------
1344
Time: 2295.949 ms (00:02.296)
SELECT 671712
Time: 5244.923 ms (00:05.245)
kunlar
--------
1344
Time: 196.393 ms
kunlik jamlanma: 671712 qator, 59 MB| So'rov | Xom hodisalar | Jamlanma |
|---|---|---|
| butun tizim, kunma-kun | 2 296 ms | 196 ms |
| bitta foydalanuvchi, kunma-kun | 3.172 ms | 7.084 ms |
| jamlanmani qurish | — | 5 245 ms, 59 MB |
- butun tizim grafigi9 %
- bitta foydalanuvchi grafigi223 %
Manba: IlmHamroh kanon o'lchovi, 2026-11-02: PostgreSQL 18.6, ~2.75 mln sintetik hodisa (11 oy), 196.393/2295.949 ms va 7.084/3.172 ms; bitta mashina, bitta yugurish
Tizim grafigida jamlanma 12 baravar tez. Foydalanuvchi grafigida esa ikki baravardan ko'proq sekin. Kanon xulosasi: foydalanuvchi grafigi uchun jamlanma kerak emas, shuning uchun loyihada yo'q. Tizim hisobotlari uchun — kerak.
4.3 Natijani halol o'qish
"12 oylik" aslida 11 oy. Skript bu o'lchovdan oldin eng eski bo'limni (2025-yil noyabr) DETACH + DROP bilan o'chirgan edi. kunlar = 1344 — bu 336 kun × 4 hodisa turi. Ustun nomi ham chalg'itadi: u kunlar sonini emas, (kun, tur) guruhlari sonini ko'rsatadi. Shuning uchun grafik izohida "~2.75 mln hodisa, 11 oy" yozdik: 3 mln dan bir oy (246 576 qator) ayrildi.
Nega foydalanuvchi grafigi jamlanmada sekin? Ikki rejani solishtiramiz (kanon chiqishidan). Xom hodisalarda — 11 bo'limning har birida Index Only Scan: (egasi_id, vaqt) indeksi so'rovga kerakli hamma narsani beradi, jadvalga murojaat yo'q (Heap Fetches: 0), jami 44 sahifa. Jamlanmada esa:
-> Bitmap Heap Scan on kunlik (actual rows=336.00 loops=1)
Recheck Cond: (egasi_id = 42)
Heap Blocks: exact=331
Buffers: shared hit=155 read=181336 ta qator uchun 331 ta jadval sahifasi o'qildi — har qator alohida sahifada. Jamlanma GROUP BY natijasidan qurilgan, qatorlar egasi bo'yicha emas, kun bo'yicha joylashgan. Foydalanuvchining qatorlari jadval bo'ylab sochilib ketgan. Ya'ni bu jamlanma g'oyasining emas, aynan shu jadval tuzilishining narxi. Indeksga soni ni qo'shish (INCLUDE (tur, soni)) yoki jadvalni egasi bo'yicha tartiblash natijani o'zgartirishi mumkin edi — kanon buni sinamagan.
Bitta yugurish, bitta mashina. Mutlaq ms emas, nisbat va xulosa muhim.
Tekshirib ko'ring: Admin paneli uchun "butun tizim, oxirgi 12 oy, kunma-kun" grafigi kerak. Kanon o'lchoviga qarab — jamlanma qurasizmi?
Javob
Ha. Bu aynan kanondagi "butun tizim" so'rovi: xom hodisalardan 2.3 soniya, jamlanmadan 0.2 soniya. Admin sahifasi har ochilganda millionlab qatorni qayta sanash isrof. Jamlanmani kuniga bir marta (yoki soatda bir marta, oxirgi 2 kunni) yangilash yetarli — o'tgan kunlar o'zgarmaydi. Foydalanuvchi grafigida esa jamlanma kerak emas: u bir necha yuz qatorni o'qiydi va indeks yetarli.
4.4 Jamlanmani yangilab turish
Jamlanmani har kecha noldan qurish (CREATE TABLE AS yoki materialized view + REFRESH) — oddiy, lekin katta ma'lumotda sekin. Ko'proq ishlatiladigan usul — inkremental: faqat yangi kunni qo'shish yoki yangilash. Kichik namuna:
CREATE TABLE faollik_kunlik (
kun date, egasi_id bigint, tur text, soni int NOT NULL,
PRIMARY KEY (kun, egasi_id, tur)
);
INSERT INTO faollik_kunlik
SELECT (vaqt AT TIME ZONE 'Asia/Tashkent')::date,
egasi_id, tur, count(*)
FROM vazifa_hodisalari
WHERE vaqt >= '2026-10-01' AND vaqt < '2026-10-11'
GROUP BY 1, 2, 3
ON CONFLICT (kun, egasi_id, tur)
DO UPDATE SET soni = EXCLUDED.soni;
SELECT count(*) AS qatorlar, sum(soni) AS hodisalar
FROM faollik_kunlik;CREATE TABLE
INSERT 0 16
qatorlar | hodisalar
----------+-----------
16 | 28
(1 row)ON CONFLICT ... DO UPDATE — UPSERT va MERGE darsidan: kun allaqachon bor bo'lsa, qiymat yangilanadi. Shu so'rovni cron har soatda "oxirgi 2 kun" uchun bajarsa, kech kelgan hodisalar ham hisobga tushadi. Diqqat: so'rov oralig'i (UTC) va kun chegarasi (Toshkent) mos bo'lishi kerak. Bu misolda 1-oktyabr UTC 00:00 — Toshkentda 05:00, demak birinchi kunning 00:00–05:00 orasidagi hodisalari tushmay qolishi mumkin edi. Bizning namunamizda hodisalar 09:00 dan boshlanadi, shuning uchun farq yo'q. Haqiqiy kodda oraliq chegarasini ham mintaqa bo'yicha hisoblang.
5. Ixtisoslashgan bazalar
Quyidagilar rasmiy hujjat bo'yicha (hujjatlar 2026-11-03 da ochib ko'rildi; tizimlarning o'zi kursda sinalmagan). Kanon ularni o'rnatmadi: oddiy bo'limlash va BRIN yetarli bo'ldi.
| Tizim | Nima | Qachon |
|---|---|---|
| TimescaleDB | Postgres kengaytmasi | Postgres'dan chiqmasdan katta vaqt qatorlari |
| ClickHouse | alohida ustunli baza | milliardlab hodisa, real vaqtli analitika |
| DuckDB | jarayon ichidagi ustunli baza | fayllar (CSV, Parquet) ustida tahlil, noutbukda |
TimescaleDB — "hypertable": oddiy jadvalni avtomatik vaqt bo'laklariga ajratadi (biz hodisa_bolimi_yarat bilan qo'lda qilgan ish). Continuous aggregates — avtomatik yangilanadigan jamlanma: biz «Jamlanmani yangilab turish» bo'limida qo'lda yozgan UPSERT o'rniga. Ikkalasi ham Postgres ichida, SQL o'sha.
ClickHouse — ustunli saqlash va kuchli siqish bilan millionlab qatorni soniyalarda guruhlaydi. Lekin u OLTP emas: bitta qatorni yangilash yoki o'chirish qimmat. Odatda Postgres'dan ma'lumot unga oqib keladi.
DuckDB — server emas, kutubxona (SQLite'ning analitik "ukasi" — SQLite darsini eslang). Node'dan ham ishlatiladi. Eksport qilingan Parquet fayllar ustida hisobot uchun qulay.
Sahifa ko'rishlar analitikasi — klassik misol. Har ko'rish — hodisa (vaqt, sahifa, foydalanuvchi, qurilma). Kuniga million qator, so'rovlar — "kunlik noyob tashrifchilar", "eng ko'p o'qilgan sahifalar". Kichik saytda — Postgres + bo'limlar + kunlik jamlanma. Katta saytda — ClickHouse yoki tayyor analitika xizmati.
6. Ko'p uchraydigan xatolar
| Belgi | Sabab | Davosi |
|---|---|---|
| Kechki hodisalar ertangi kunga tushadi | agregat UTC kunida | AT TIME ZONE 'Asia/Tashkent' |
| Grafikda kunlar "yo'qolgan" | bo'sh kun qatori yo'q | generate_series + LEFT JOIN yoki mijozda to'ldirish |
| Grafik so'rovi butun jadvalni o'qiydi | so'rovda vaqt sharti yo'q | vaqt oralig'i sharti (pruning) |
| Jamlanma xomdan farq qiladi | kech kelgan hodisalar, noto'g'ri oraliq | oxirgi N kunni qayta hisoblash |
| "7 kunlik o'rtacha" noto'g'ri | ROWS oynasi bo'sh kunlarni bilmaydi |
RANGE ... interval yoki to'ldirish |
7. Mashqlar
Mashqlar kurs/mashqlar/31/09-time-series/ da. 2- va 3-mashq yechimlari Node 24.21.0 da tekshirilgan (biz ishga tushirdik).
1-mashq (oson): bo'shliqlarni to'ldiring
- Bitta yozuvni o'qish-yozish yuki — ; katta hisobot yuki — OLAP
- Har ustun alohida saqlanadigan bazalar — saqlash
- Oldindan kunma-kun yig'ilgan jadval —
- Kanonda butun tizim grafigi jamlanmadan necha baravar tez —
- Bitta so'rovda bir nechta shartli sanoq —
count(*) (WHERE ...)
Yechim
- OLTP — Postgres asosan shu uchun.
- ustunli — ClickHouse, DuckDB.
- jamlanma — rollup; foydalanuvchi grafigida kanonda kerak bo'lmadi.
- 12 — 2 296 / 196 ≈ 11.7.
- FILTER —
count(*) FILTER (WHERE tur = 'bajarildi').
2-mashq (o'rta): API javobidagi bo'sh kunlarni to'ldirish
GET /v1/statistika/faollik faqat hodisa bo'lgan kunlarni qaytaradi: { tz, kunlar: [{ kun: "2026-10-01", yaratildi: 2 }, …] }. Frontend grafigi uchun toldir(kunlar, dan, gacha) funksiyasini yozing: dandan gachagacha (ikkalasi ham kiradi) har kun uchun obyekt qaytarsin, yo'q kunlarda { kun, yaratildi: 0, bajarildi: 0 }, bor kunlarda yo'q maydon ham 0 bo'lsin. Ishora: Map (Map), sanani Date.UTC bilan bir kunga oshirish va toISOString().slice(0, 10).
Yechim
function toldir(kunlar, dan, gacha) {
const bor = new Map(kunlar.map((k) => [k.kun, k]));
const natija = [];
const kun = new Date(`${dan}T00:00:00Z`);
const oxiri = new Date(`${gacha}T00:00:00Z`);
while (kun <= oxiri) {
const s = kun.toISOString().slice(0, 10);
const k = bor.get(s) ?? {};
natija.push({
kun: s,
yaratildi: k.yaratildi ?? 0,
bajarildi: k.bajarildi ?? 0,
});
kun.setUTCDate(kun.getUTCDate() + 1);
}
return natija;
}
const javob = [
{ kun: "2026-10-30", yaratildi: 2, bajarildi: 1 },
{ kun: "2026-11-01", bajarildi: 3 },
];
for (const k of toldir(javob, "2026-10-30", "2026-11-02")) {
console.log(k.kun, k.yaratildi, k.bajarildi);
}Konsolda:
2026-10-30 2 1
2026-10-31 0 0
2026-11-01 0 3
2026-11-02 0 0Oy almashishi (31-oktyabr → 1-noyabr) avtomatik: setUTCDate(32) keyingi oyga o'tadi. Sana satri — mintaqa kuni (API tz bo'yicha qaytargan), shuning uchun uni UTC yarim tunida tutib, faqat kalendar kun sifatida ishlatamiz — soat zonasiga bog'liq siljish bo'lmaydi.
3-mashq (qiyin): inkremental jamlanma
jamlanmaga(jamlanma, hodisalar, tz) — jamlanma — Map (kalit "kun|egasi|tur", qiymat — son), hodisalar — { vaqt, egasi, tur } massivi (vaqt — ISO satr). Har hodisani mintaqadagi kuni bo'yicha jamlanmaga qo'shsin. Kun — Intl.DateTimeFormat("en-CA", { timeZone: tz }) bilan (en-CA sanani YYYY-MM-DD shaklida beradi — loyiha testi ham shuni ishlatadi). Ishora: Map (Map), ??.
Yechim
function jamlanmaga(jamlanma, hodisalar, tz) {
const kunFmt = new Intl.DateTimeFormat("en-CA", { timeZone: tz });
for (const h of hodisalar) {
const kun = kunFmt.format(new Date(h.vaqt));
const kalit = `${kun}|${h.egasi}|${h.tur}`;
jamlanma.set(kalit, (jamlanma.get(kalit) ?? 0) + 1);
}
return jamlanma;
}
const j = new Map();
jamlanmaga(j, [
{ vaqt: "2026-10-14T18:00:00Z", egasi: 1, tur: "yaratildi" },
{ vaqt: "2026-10-14T20:30:00Z", egasi: 1, tur: "yaratildi" },
], "Asia/Tashkent");
jamlanmaga(j, [
{ vaqt: "2026-10-14T21:10:00Z", egasi: 1, tur: "yaratildi" },
{ vaqt: "2026-10-15T05:00:00Z", egasi: 1, tur: "bajarildi" },
], "Asia/Tashkent");
console.log([...j.entries()]);Konsolda:
[
[ '2026-10-14|1|yaratildi', 1 ],
[ '2026-10-15|1|yaratildi', 2 ],
[ '2026-10-15|1|bajarildi', 1 ]
]UTC bo'yicha 14-oktyabrdagi uchta hodisadan ikkitasi (20:30 va 21:10) Toshkentda 15-oktyabrga tushdi. Ikkinchi paket birinchisining ustiga qo'shildi — jamlanma noldan qurilmadi. SQL'dagi ON CONFLICT ... DO UPDATE ham shu ishni qiladi, faqat u qiymatni qayta hisoblaydi, biz esa qo'shamiz: bir hodisa ikki marta kelsa, bizning modelimiz uni ikki marta sanaydi. Haqiqiy kodda shuning uchun "qayta hisoblash" usuli ishonchliroq.
8. Loyiha qadami: statistika moduli va commit
8.1 Nima qo'shamiz
Partitioning darsida ma'lumotlar bazasi qismi qurildi. Endi Nest qismi:
| Fayl | Nima |
|---|---|
src/statistika/statistika.module.ts |
GET /v1/statistika/faollik, BolimlarXizmati (cron) |
src/tokenlar.ts |
STATISTIKA tokeni |
src/integratsiya.module.ts |
statistika bog'liqligi (testlar uchun) |
src/app.module.ts |
StatistikaModule.forRoot(...) |
test/statistika.test.ts |
2 ta test |
Butun oqim bitta rasmda:
flowchart LR
A["PATCH /v1/vazifalar/:id"] --> V[("vazifalar")]
V -->|"trigger"| H[("vazifa_hodisalari: oy bo'limi")]
C["Cron 03:00 UTC"] -->|"tayyorla(2)"| H
G["GET /v1/statistika/faollik"] -->|"vaqt sharti, RLS"| H
G --> M["mijoz: bo'sh kunlarni to'ldiradi"]Chapdan o'qing. API vazifani o'zgartiradi, trigger o'sha tranzaksiyada hodisa yozadi. Cron oldindan bo'lim yaratib turadi. Statistika yo'li faqat oxirgi kunlar bo'limlarini va faqat o'z hodisalarini o'qiydi. Grafikni chizish va bo'sh kunlarni to'ldirish — mijozda.
Kanon fayllaridagi uzun qatorlarni telefon uchun bo'ldik: izohlar qisqartirildi, takrorlanadigan tur ifodasi Tur nomiga olindi, SQL satri ko'p qatorga yoyildi (ma'no aynan). Bo'lingan nusxani biz loyihaning shu bosqichi nusxasida tsc --noEmit, ESLint va Vitest bilan tekshirdik.
8.2 statistika.module.ts
// statistika.module.ts — vazifa hodisalari (sql/301 — oyma-oy
// bo'limli vaqt qatori).
// GET /v1/statistika/faollik?kunlar=30 — kunlik hodisalar soni
// (foydalanuvchi mintaqasi bo'yicha kun; vaqt sharti — pruning).
// BolimlarXizmati — ishga tushishda va har kuni 03:00 (UTC) da
// keyingi 2 oy bo'limlarini oldindan yaratadi.
// Faqat SAQLASH=postgres|prisma (auth bilan) — boshqa rejimlarda
// controller ro'yxatdan o'tmaydi.
import {
Controller, Get, Global, Inject, Injectable, Module, Query,
} from "@nestjs/common";
import type {
DynamicModule, OnApplicationBootstrap, Provider, Type,
} from "@nestjs/common";
import { Cron } from "@nestjs/schedule";
import type { Pool } from "pg";
import { z } from "zod";
import { egasiSifatida } from "../umumiy/rls.ts";
import { tekshir } from "../umumiy/tekshir.ts";
import { Egasi } from "../umumiy/egasi.ts";
import { Cheklov } from "../auth/cheklov.guard.ts";
import { API_CHEKLOVI } from "../auth/qoriqchi.ts";
import { ILOVA_SOZLAMALARI, LOG, STATISTIKA } from "../tokenlar.ts";
import type { IlovaSozlamalari } from "../tokenlar.ts";
import type { Log } from "../umumiy/log.ts";
import { SAQLASH } from "../ombor.module.ts";
import type { Saqlash } from "../ombor-yarat.ts";
export type StatistikaBogliqliklari = { pool: Pool };
export const HODISA_TURLARI = [
"yaratildi", "bajarildi", "qaytarildi", "ochirildi", "tiklandi",
"ai_qadamlar", "ai_savol", "semantik_qidiruv",
] as const;
const FaollikSorovi = z.strictObject({
kunlar: z.coerce.number().int().min(1).max(366).default(30),
});
type Tur = (typeof HODISA_TURLARI)[number];
type Kun = { kun: string } & Partial<Record<Tur, number>>;
@Controller("statistika")
@Cheklov(API_CHEKLOVI)
export class StatistikaController {
constructor(
@Inject(STATISTIKA) private readonly b: StatistikaBogliqliklari,
@Inject(ILOVA_SOZLAMALARI) private readonly s: IlovaSozlamalari,
) {}
@Get("faollik")
async faollik(
@Egasi() fId: number, @Query() q: unknown,
): Promise<{ tz: string; kunlar: Kun[] }> {
const { kunlar } = tekshir(FaollikSorovi, q, "query");
// vaqt sharti — bo'lim kaliti bo'yicha (pruning): faqat oxirgi
// N kun bo'limlari o'qiladi. Kun chegarasi — APP_TZ mintaqasida
const qatorlar = await egasiSifatida(
this.b.pool, fId, async (c) => (await c.query<{
kun: string; tur: string; n: number;
}>(
`SELECT to_char((vaqt AT TIME ZONE $2)::date, 'YYYY-MM-DD')
AS kun, tur, count(*)::int AS n
FROM vazifa_hodisalari
WHERE egasi_id = $1
AND vaqt >= ((now() AT TIME ZONE $2)::date
- ($3::int - 1)) AT TIME ZONE $2
GROUP BY 1, 2 ORDER BY 1, 2`,
[fId, this.s.tz, kunlar],
)).rows,
);
const jadval = new Map<string, Kun>();
for (const q2 of qatorlar) {
const k = jadval.get(q2.kun) ?? { kun: q2.kun };
k[q2.tur as Tur] = q2.n;
jadval.set(q2.kun, k);
}
return { tz: this.s.tz, kunlar: [...jadval.values()] };
}
}
/** Bo'limlarni oldindan yaratish (idempotent). Xato ilovani
* to'xtatmaydi — DEFAULT bo'lim yozuvni qabul qiladi, log
* ogohlantiradi */
@Injectable()
export class BolimlarXizmati implements OnApplicationBootstrap {
constructor(
@Inject(STATISTIKA)
private readonly b: StatistikaBogliqliklari | null,
@Inject(LOG) private readonly log: Log,
) {}
async onApplicationBootstrap(): Promise<void> {
await this.tayyorla();
}
@Cron("0 3 * * *", { name: "hodisa-bolimlari", timeZone: "UTC" })
async tayyorla(): Promise<string[]> {
if (this.b === null) return [];
try {
return (await this.b.pool.query<{ nom: string }>(
"SELECT hodisa_bolimlari_tayyorla(2) AS nom",
)).rows.map((r) => r.nom);
} catch (err) {
this.log.warn({ err },
"hodisa bo'limlari yaratilmadi (DEFAULT bo'limga yoziladi)");
return [];
}
}
}
@Global()
@Module({})
export class StatistikaModule {
static forRoot(
o: StatistikaBogliqliklari | undefined, configdan: boolean,
): DynamicModule {
const controllers: Type[] = [];
const providers: Provider[] = [BolimlarXizmati];
if (o !== undefined) {
controllers.push(StatistikaController);
providers.push({ provide: STATISTIKA, useValue: o });
} else if (configdan) {
controllers.push(StatistikaController);
providers.push({
provide: STATISTIKA, inject: [SAQLASH],
useFactory: (s: Saqlash): StatistikaBogliqliklari => {
if (s.pool === undefined) {
throw new Error("statistika: ma'lumot pool'i yo'q " +
"(SAQLASH=postgres|prisma kerak)");
}
return { pool: s.pool };
},
});
} else {
providers.push({ provide: STATISTIKA, useValue: null });
}
return {
module: StatistikaModule, controllers, providers,
exports: [STATISTIKA],
};
}
}Kodni bo'lak-bo'lak o'qiymiz.
SQL so'rovi. Unda darsdagi uch g'oya bor. (1) Kun — vaqt AT TIME ZONE $2, mintaqa sozlamadan. (2) WHERE vaqt >= ... — bo'lim kaliti sharti, faqat oxirgi kunlar kunning bo'limlari o'qiladi. (3) Chegara mintaqa yarim tunida: (now() AT TIME ZONE $2)::date — mintaqadagi bugungi sana, undan kunlar - 1 kun ayiriladi va qaytadan AT TIME ZONE $2 bilan timestamptz ga aylantiriladi. kunlar = 7 — bugun va oldingi 6 kun.
egasiSifatida — 28-qismdagi RLS yordamchisi: so'rov tranzaksiyasida ilova.foydalanuvchi_id ni o'rnatadi. 301 dagi siyosat foydalanuvchiga faqat o'z hodisalarini ko'rsatadi. WHERE egasi_id = $1 sharti esa RLS bo'lsa ham kerak — u (egasi_id, vaqt) indeksini ishlatadi.
Natija shakli. SQL (kun, tur, n) qatorlarini beradi, kod ularni kunma-kun obyektga yig'adi: { kun: "2026-11-03", yaratildi: 1, bajarildi: 1 }. Bo'sh kunlar yo'q — mijoz to'ldiradi (2-mashq).
BolimlarXizmati. Ikki marta ishlaydi: ilova ishga tushganda (onApplicationBootstrap) va har kuni 03:00 UTC da (@Cron). Xato bo'lsa — faqat ogohlantirish: ilova to'xtamasligi kerak, yozuvlar DEFAULT bo'limga tushadi. Partitioning darsidagi «DEFAULT bo'lim tuzog'i» shu yerdan keladi — bu ogohlantirish monitoringda ko'rinishi shart.
forRoot — Dynamic modullar naqshi, uch rejim: testlar pool beradi; config rejimida pool SAQLASH dan olinadi; xotira/fayl rejimida controller umuman ro'yxatdan o'tmaydi — /v1/statistika/faollik 404. BolimlarXizmati esa doim bor, lekin null bilan hech narsa qilmaydi.
8.3 Ulash
src/tokenlar.ts oxiriga:
// ─── 31-qism (prodakshn va AI) ───
/** vazifa hodisalari statistikasi — { pool } yoki null */
export const STATISTIKA = Symbol("STATISTIKA");src/integratsiya.module.ts — IntegratsiyaSozlamasi turiga bitta maydon (va tur importi):
import type {
StatistikaBogliqliklari,
} from "./statistika/statistika.module.ts";
// ...
/** hodisalar statistikasi (testlar pool beradi; config
* rejimida — postgres/prisma bo'lsa) */
statistika?: StatistikaBogliqliklari | undefined;src/app.module.ts — import va modullar ro'yxatiga, IntegratsiyaModule dan keyin:
import { yoqilganlar } from "./integratsiya.module.ts";
import { StatistikaModule } from "./statistika/statistika.module.ts";
// ...
// hodisalar statistikasi va bo'limlar (config rejimida —
// faqat postgres/prisma + auth)
StatistikaModule.forRoot(
integratsiya.statistika,
ombor === undefined &&
yoqilganlar({ config, authYoqilgan }).bildirishnoma,
),Kichik kuzatuv: "postgres/prisma + auth bo'lsa" sharti yoqilganlar(...).bildirishnoma orqali olinadi. 29-qism integratsiyalarida bu bayroq aynan shu shartga teng (pg), shuning uchun natija to'g'ri, lekin nom chalg'itadi. O'z kodingizda alohida statistika maydonini qo'shish o'qishni osonlashtiradi.
8.4 Test
// statistika.test.ts — vazifa hodisalari (sql/301): trigger hodisa
// yozadi, oyma-oy bo'limlar, kunlik statistika (begona hodisalar
// ko'rinmaydi), eski bo'limni DETACH + DROP. Auth test bazasi kerak;
// bo'lmasa SKIP.
import { afterAll, beforeEach, test } from "vitest";
import assert from "node:assert/strict";
import request from "supertest";
import { expressIlova } from "./yordamchi.ts";
import { PgOmbori } from "../src/vazifalar/pg-ombor.ts";
import {
BolimlarXizmati,
} from "../src/statistika/statistika.module.ts";
import {
authBaza, authBogliqliklari, hisobOch, tozala,
} from "./auth-yordam.ts";
const baza = await authBaza("statistika");
if (typeof baza === "string") {
test.skip(`vazifa hodisalari (Postgres) — ${baza}`, () => {});
} else {
const pool = baza;
afterAll(() => pool.end());
beforeEach(() => tozala(pool));
const ilova = async () => {
const d = authBogliqliklari(pool);
return {
d,
app: await expressIlova({
ombor: (e) => new PgOmbori(pool, e), auth: d,
statistika: { pool },
}),
};
};
test("trigger: yaratildi → bajarildi → qaytarildi → ochirildi; " +
"statistika faqat o'ziniki; kun — Asia/Tashkent", async () => {
const { d, app } = await ilova();
const aziz = await hisobOch(app, d, "aziz@example.com");
const dil = await hisobOch(app, d, "dilnoza@example.com");
const v = (await request(app).post("/v1/vazifalar")
.set("Cookie", aziz).send({ matn: "Hisobot yozish" })
.expect(201)).body as { id: number };
const patch = (b: object) => request(app)
.patch(`/v1/vazifalar/${String(v.id)}`).set("Cookie", aziz)
.set("Content-Type", "application/merge-patch+json")
.send(b).expect(200);
await patch({ bajarildi: true });
await patch({ bajarildi: false });
// bajarildi/ochirilgan o'zgarmadi — hodisa yo'q
await patch({ matn: "Hisobot yozish #ish" });
await request(app).delete(`/v1/vazifalar/${String(v.id)}`)
.set("Cookie", aziz).expect(204);
await request(app).post("/v1/vazifalar").set("Cookie", dil)
.send({ matn: "Dilnozaniki" }).expect(201);
const { rows } = await pool.query<{ tur: string; bolim: string }>(
"SELECT tur, tableoid::regclass::text AS bolim " +
"FROM vazifa_hodisalari WHERE vazifa_id = $1 ORDER BY vaqt",
[v.id],
);
assert.deepEqual(rows.map((r) => r.tur),
["yaratildi", "bajarildi", "qaytarildi", "ochirildi"]);
// DEFAULT emas — joriy oy bo'limi
assert.match(rows[0]?.bolim ?? "",
/^bolimlar\.vazifa_hodisalari_\d{4}_\d{2}$/);
const s = (await request(app)
.get("/v1/statistika/faollik?kunlar=7").set("Cookie", aziz)
.expect(200)).body as {
tz: string; kunlar: Record<string, unknown>[];
};
const bugun = new Intl.DateTimeFormat("en-CA",
{ timeZone: "Asia/Tashkent" }).format(new Date());
assert.equal(s.tz, "Asia/Tashkent");
assert.deepEqual(s.kunlar, [{
kun: bugun, yaratildi: 1, bajarildi: 1, qaytarildi: 1,
ochirildi: 1,
}]);
await request(app).get("/v1/statistika/faollik?kunlar=0")
.set("Cookie", aziz).expect(400);
await request(app).get("/v1/statistika/faollik").expect(401);
});
test("bo'limlar: tayyorla() idempotent; tozala() faqat muddati " +
"o'tganini DETACH + DROP qiladi; bo'limsiz vaqt — DEFAULT " +
"bo'limga", async () => {
const xizmat = new BolimlarXizmati(
{ pool }, { warn: () => undefined } as never);
const a = await xizmat.tayyorla();
const b = await xizmat.tayyorla();
assert.equal(a.length, 3);
assert.deepEqual(a, b);
await pool.query("SELECT hodisa_bolimi_yarat(date '2024-01-01')");
await pool.query(
"INSERT INTO vazifa_hodisalari (vaqt, egasi_id, tur) VALUES " +
"('2024-01-15', 1, 'yaratildi'), " +
"('2030-06-01', 1, 'yaratildi')");
const { rows: [def] } = await pool.query<{ n: number }>(
"SELECT count(*)::int AS n " +
"FROM bolimlar.vazifa_hodisalari_boshqa");
// 2030 — oldindan yaratilmagan oy → DEFAULT (yo'qolmadi)
assert.equal(def?.n, 1);
const { rows } = await pool.query<{ nom: string }>(
"SELECT hodisa_bolimlari_tozala('13 months') AS nom");
assert.deepEqual(rows.map((r) => r.nom),
["vazifa_hodisalari_2024_01"]);
const r = await pool.query<{ r: string | null }>(
"SELECT to_regclass('bolimlar.vazifa_hodisalari_2024_01')" +
"::text AS r");
assert.equal(r.rows[0]?.r, null);
await pool.query(
"DELETE FROM vazifa_hodisalari WHERE vaqt >= '2030-01-01'");
});
}Birinchi test butun yo'lni HTTP orqali tekshiradi: API kodi hodisa yozmaydi, lekin trigger yozadi; matnni tahrirlash hodisa bermaydi; Dilnozaning hodisasi Azizning statistikasiga kirmaydi; kunlar=0 — 400, sessiyasiz — 401. Ikkinchisi — bo'lim funksiyalari. Oxirgi qatorga e'tibor bering: test 2030-yilgi qatorni o'chirib, DEFAULT bo'limni bo'sh qoldiradi — aks holda keyingi testlarda o'sha tuzoqqa tushardi.
Biz bo'lingan nusxani o'z PostgreSQL 17.6 bazamizda (loyihaning sql/ fayllari, namunasiz) ishga tushirdik:
U=postgres://postgres@127.0.0.1:5432/vazifalar_test_auth
TEST_AUTH_DATABASE_URL=$U npx vitest run test/statistika.test.ts ✓ |asosiy| test/statistika.test.ts (2 tests) 1397ms
✓ trigger: yaratildi → bajarildi → qaytarildi → ochirildi; statistika faqat o'ziniki; kun — Asia/Tashkent 1079ms
✓ bo'limlar: tayyorla() idempotent; tozala() faqat muddati o'tganini DETACH + DROP qiladi; bo'limsiz vaqt — DEFAULT bo'limga 314ms
Test Files 1 passed (1)
Tests 2 passed (2)Port — o'zingiznikini qo'ying (bizda boshqa port edi); vaqt qatorlari olib tashlandi.
8.5 Shu bosqichdagi test tuzatishlari
Kanon bu bosqichda yana to'rtta testni tuzatdi. Ular statistikaga aloqador emas — yuklangan mashinada ba'zan yiqilgan (flaky) testlar edi:
| Test | Sabab | Tuzatish |
|---|---|---|
telegram-bot |
dialog holati javobdan keyin yoziladi | holatni kutib() bilan kutish |
prisma-ombor |
yolg'iz 10.7 s, chegara 20 s | o'z chegarasi 60 s |
migratsiya (afterAll) |
DROP DATABASE checkpoint kutadi |
60 s |
auth (timing) |
mutlaq 20 ms chegara yuk ostida | max(20 ms, 15 %) |
Flaky testlarning sabablari va topish usullari — Test arxitekturasi va CI darsida. Sizning mashinangizda ular yiqilmasa, tuzatishlarni shu darsgacha qoldirishingiz mumkin.
8.6 Tekshiruv va commit
Kanon bosqich tekshiruvi (tip, lint, test, build, migratsiyalar), kanon chiqishidan:
--- npm run tip
exit 0 (27952 ms)
--- npm run lint
exit 0 (61890 ms)
--- npm run build
exit 0 (26657 ms)
Test Files 56 passed | 1 skipped (57)
Tests 300 passed | 1 skipped (301)
--- prisma migrate deploy (yangi baza): 21 migrations found in prisma/migrations The following migration(s) have been applied: All migrations have been successfully applied.
--- schema.prisma ↔ migratsiyalar (migrate diff): []
--- migratsiyalar va sql/ sxemasi farqi (pg_dump, _prisma_migrations'siz): 0 qatorHammasi yashil. Kanon chiqishidagi lint qismida ikkita yordamchi skript xabari (cp: cannot stat …) bor — tekshiruv skriptining o'z vaqtinchalik fayli, natijaga ta'sir qilmagan; ularni chiqishdan olib tashladik. Vaqtlar yuklangan mashinada — 28 soniyalik tip odatda ancha tez. O'tkazilgan bitta test — Testcontainers (Docker talab qiladi).
Commit:
git add -A
M="feat(db): vazifa hodisalari — oyma-oy bo'limli vaqt qatori"
M+=" (BRIN, trigger), /v1/statistika/faollik,"
M+=" bo'limlarni cron bilan tayyorlash"
git commit -m "$M"9. Real ishda
- Qayerda uchraydi: foydalanuvchi faolligi, sotuv grafiklari, server metrikalari, IoT, moliyaviy kotirovkalar. Deyarli har dashboard — vaqt qatori ustida agregat.
- Intervyu savollari: "OLTP va OLAP farqi?", "Ustunli baza nega hisobotda tez?", "Kunlik agregatda vaqt zonasi qanday hisobga olinadi?", "Rollup jadvalni qanday yangilab turasiz?", "Postgres'dan ClickHouse'ga qachon o'tasiz?"
- Monitoring: keyingi darslarda servisning o'z metrikalarini (so'rovlar, kechikish) Prometheus formatida chiqaramiz — bu ham vaqt qatori, faqat alohida tizimda saqlanadi (Observability).
- Kod review'da: "kun qaysi zonada?", "grafik so'rovida vaqt sharti bormi?", "jamlanma kech kelgan hodisalarni ushlaydimi?"
Xulosa
- Time-series — vaqt belgili, faqat qo'shiladigan ma'lumot; so'rovlar — oraliq va guruhlash.
- OLTP — qatorlar va tranzaksiyalar (Postgres), OLAP — ustunli saqlash va katta agregatlar (ClickHouse, DuckDB).
- Kunlik agregat —
AT TIME ZONE 'Asia/Tashkent',count(*) FILTER, bo'sh kunlar —generate_seriesyoki mijozda. - Kanon: tizim grafigi jamlanmadan 12 baravar tez, foydalanuvchi grafigi esa xom hodisalardan tezroq — loyihada jamlanma yo'q.
- TimescaleDB — Postgres ichida avtomatik bo'limlar va jamlanmalar; alohida baza — faqat haqiqiy sabab bilan.
- Loyihada:
GET /v1/statistika/faollikva bo'limlarni har kuni tayyorlovchi cron — commit qilindi.
Keyingi dars: CDC va ma'lumot sinxronlash — Postgres o'zgarishlarini WAL'dan o'qib, boshqa tizimlarga real vaqtda uzatamiz.
Manbalar
- PostgreSQL 18 hujjati — Aggregate Functions (
FILTER): https://www.postgresql.org/docs/18/sql-expressions.html#SYNTAX-AGGREGATES - PostgreSQL 18 hujjati — Window Functions: https://www.postgresql.org/docs/18/tutorial-window.html
- PostgreSQL 18 hujjati —
AT TIME ZONE: https://www.postgresql.org/docs/18/functions-datetime.html#FUNCTIONS-DATETIME-ZONECONVERT - TimescaleDB hujjati (kompaniya endi Tiger Data deb ataladi) — Hypertables: https://www.tigerdata.com/docs/use-timescale/latest/hypertables (2026-11-03 holati; eski docs.timescale.com shu yerga yo'naltiradi)
- ClickHouse hujjati — "column-oriented DBMS for OLAP": https://clickhouse.com/docs/intro (2026-11-03 holati)
- DuckDB hujjati — Node.js mijozi (
@duckdb/node-api): https://duckdb.org/docs/stable/clients/node_neo/overview (2026-11-03 holati) - NestJS hujjati — Task Scheduling: https://docs.nestjs.com/techniques/task-scheduling
- Kanon o'lchovi (IlmHamroh, 2026-11-02/03): PostgreSQL 18.6 (Docker), 3 000 000 sintetik hodisa; holat tekshiruvi — Node 24.21.0, NestJS 12.2.0, Vitest 5.0.5. SQL bloklari, statistika testi va mashq yechimlari — biz ishga tushirdik (2026-11-03, PostgreSQL 17.6, Node 24.21.0).
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!