IlmHamroh
JavaScript Full-stack/31-qism. Prodakshn backend va AI integratsiya9/34-dars28 daqiqa
Mundarija (32)

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/faollik yo'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-api ga 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:

  1. Qayerdan sanaymiz? vazifalar jadvali joriy holatni saqlaydi: vazifa hozir bajarilganmi. Lekin "o'tgan seshanba nechta vazifa bajarilgan edi" — bu tarix. Uni faqat hodisalardan bilish mumkin.
  2. 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:

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

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

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

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

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

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

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

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

text
=== 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
Jamlanmadan o'qish vaqti: xom hodisalar = 100 %
  • 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:

text
   ->  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=181

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

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

text
2026-10-30 2 1
2026-10-31 0 0
2026-11-01 0 3
2026-11-02 0 0

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

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

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:

ts
// ─── 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):

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

ts
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

ts
// 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:

bash
U=postgres://postgres@127.0.0.1:5432/vazifalar_test_auth
TEST_AUTH_DATABASE_URL=$U npx vitest run test/statistika.test.ts
text
 ✓ |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:

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

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

bash
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_series yoki 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/faollik va 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

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Time-series va analitik DB'lar: hodisalardan faollik grafigi — IlmHamroh