Ombor modellashtirish: fakt va o'lchov
Direktorning oddiy savoli
Direktor kirib keladi va so'raydi:
— Sentabrda Yunusobodda o'rtacha yetkazish qancha vaqt oldi?
Siz jadvalni ochasiz. Ustunlar: buyurtma_id, vaqt, tuman, restoran_id, kuryer_id,
mahsulotlar, summa_ming, tayyorlash_daq, yol_daq, holat.
Bir so'rov yozasiz, javob chiqadi. Tamom.
Ertasi kuni direktor yana keladi:
— Yaxshi. Endi restoran turlari bo'yicha ko'rsat. Fast-food va milliy taomlar alohida.
Siz jadvalga qaraysiz. restoran_id bor — R065. Lekin restoran turi yo'q. U qayerda?
Boshqa tizimda, boshqa jadvalda.
Uchinchi kun:
— Kuryerlar tajribasi bo'yicha-chi? Yangi kelganlar sekinroqmi?
kuryer_id bor — K0789. Ishga kirgan sanasi yo'q.
Har savol yangi ustun talab qiladi. Har safar siz jadvalni kengaytirasiz. Olti oydan keyin jadvalda 60 ta ustun bo'ladi va hech kim qaysi biri nimani anglatishini bilmaydi.
Bu bob — shu muammoning yechimi haqida. Yechim ellik yildan beri ma'lum va u juda oddiy.
Ikki xil ma'lumot
Jadvalga yana bir marta qarang, lekin boshqa ko'z bilan. Ustunlar ikki turga bo'linadi:
Sodir bo'lgan voqea. buyurtma_id, vaqt, summa_ming, yol_daq — bular bitta
buyurtma haqidagi o'lchovlar. Ular sodir bo'ldi va hech qachon o'zgarmaydi. Kechagi
buyurtma 12.3 daqiqada yetkazilgan bo'lsa, u abadiy shunday qoladi.
Kim/nima haqidagi ma'lumot. tuman, restoran_id, kuryer_id — bular buyurtmaning
o'zi emas, balki unga tegishli narsalar. Va ular o'zgaradi: restoran nomini
o'zgartirishi mumkin, kuryer boshqa tumanga o'tishi mumkin.
Birinchisi — fakt (fact). Ikkinchisi — o'lchov (dimension).
diagramma( { "O'LCHOV\nsana": (0, 3.0, 2.2, 1.0, "saqlash"), "O'LCHOV\nrestoran": (0, 1.0, 2.2, 1.0, "saqlash"), "FAKT\nbuyurtma": (3.6, 2.0, 2.6, 1.4, "qayta"), "O'LCHOV\nkuryer": (7.4, 3.0, 2.2, 1.0, "saqlash"), "O'LCHOV\ntuman": (7.4, 1.0, 2.2, 1.0, "saqlash"), }, [ ("O'LCHOV\nsana", "FAKT\nbuyurtma"), ("O'LCHOV\nrestoran", "FAKT\nbuyurtma"), ("FAKT\nbuyurtma", "O'LCHOV\nkuryer"), ("FAKT\nbuyurtma", "O'LCHOV\ntuman"), ], nom="yulduz_sxema.png", kenglik=12, izoh="Yulduz sxemasi: o'rtada fakt, atrofida o'lchovlar.", )

Yulduz sxemasi: o'rtada fakt, atrofida o'lchovlar.
Rasmga qarang — u yulduzga o'xshaydi. Shuning uchun bu tuzilish yulduz sxemasi (star schema) deyiladi.
Farqni ajratishning oson yo'li
Ustunga qarab ikki savol bering:
- Bu son bilan hisob qilasizmi? (yig'indi, o'rtacha) → fakt
- Bu bo'yicha guruhlaysizmi? ("... bo'yicha ko'rsat") → o'lchov
summa_ming — yig'indisini olasiz, demak fakt. tuman — "tuman bo'yicha" deysiz, demak
o'lchov.
Direktorning savollari doim bir xil shaklda keladi: "[fakt] ni [o'lchov] bo'yicha ko'rsat".
Nega ajratish kerak
Hamma narsani bitta jadvalda saqlasak bo'lmaydimi? Bo'ladi, lekin uch narxi bor.
Birinchi narx — takrorlanish. 60 ming buyurtmada restoran nomi 60 ming marta yoziladi. Restoran esa 200 ta.
Ikkinchi narx — o'zgartirish. Restoran nomini o'zgartirdi. Bitta jadvalda 300 ta qatorni tuzatasiz. Ularning bittasi o'tkazib yuborilsa, hisobotda ikkita restoran paydo bo'ladi.
Uchinchi narx — tarix buziladi. Mana eng nozigi. Kuryer sentabrda Chilonzonda ishlagan, oktabrda Yunusobodga o'tdi. Siz uning tumanini yangilaysiz. Endi sentabr hisoboti ham o'zgarib ketadi — go'yo u sentabrda ham Yunusobodda ishlagandek.
Shu uchinchi muammoning nomi bor: sekin o'zgaruvchi o'lchov (slowly changing dimension). Uni pastda o'lchaymiz.
Ma'lumotni tayyorlaymiz
python3 -c " import duckdb, pandas as pd d = pd.read_csv('../datasets/buyurtma.csv', parse_dates=['vaqt']) con = duckdb.connect('ombor.db') con.execute('DROP TABLE IF EXISTS xom') con.execute('CREATE TABLE xom AS SELECT * FROM d') print(f'xom jadval: {len(d):,} qator, {len(d.columns)} ustun') print() for u in ('tuman', 'restoran_id', 'kuryer_id'): n = con.execute(f'SELECT count(DISTINCT {u}) FROM xom').fetchone()[0] print(f' {u:14s}: {n:>6,} xil qiymat, {len(d):,} marta takrorlangan') con.close() "
xom jadval: 60,000 qator, 10 ustun tuman : 10 xil qiymat, 60,000 marta takrorlangan restoran_id : 259 xil qiymat, 60,000 marta takrorlangan kuryer_id : 899 xil qiymat, 60,000 marta takrorlangan
Raqamlarga qarang: 200 ga yaqin restoran nomi 60 ming marta saqlanmoqda. Mana takrorlanish.
Yulduz sxemasini quramiz
shu faylni yaratingsrc/quvur/ombor.py
"""Yulduz sxemasi: fakt va o'lchov jadvallari.""" import duckdb OLCHOVLAR = { "olchov_tuman": ("tuman", ["tuman"]), "olchov_restoran": ("restoran_id", ["restoran_id"]), "olchov_kuryer": ("kuryer_id", ["kuryer_id"]), } def olchov_yarat(con: duckdb.DuckDBPyConnection, nom: str, kalit: str) -> int: """Noyob qiymatlardan o'lchov jadvali yasaydi va surrogat kalit beradi.""" con.execute(f"DROP TABLE IF EXISTS {nom}") con.execute(f""" CREATE TABLE {nom} AS SELECT row_number() OVER (ORDER BY {kalit}) AS id, {kalit} FROM (SELECT DISTINCT {kalit} FROM xom WHERE {kalit} IS NOT NULL) """) return con.execute(f"SELECT count(*) FROM {nom}").fetchone()[0] def fakt_yarat(con: duckdb.DuckDBPyConnection) -> int: """Fakt jadvali: o'lchovlarga id bilan bog'lanadi, matn saqlamaydi.""" con.execute("DROP TABLE IF EXISTS fakt_buyurtma") con.execute(""" CREATE TABLE fakt_buyurtma AS SELECT x.buyurtma_id, CAST(x.vaqt AS DATE) AS sana, t.id AS tuman_id, r.id AS restoran_id, k.id AS kuryer_id, x.summa_ming, x.tayyorlash_daq, x.yol_daq FROM xom x LEFT JOIN olchov_tuman t ON t.tuman = x.tuman LEFT JOIN olchov_restoran r ON r.restoran_id = x.restoran_id LEFT JOIN olchov_kuryer k ON k.kuryer_id = x.kuryer_id """) return con.execute("SELECT count(*) FROM fakt_buyurtma").fetchone()[0]
python3 -c " import sys, pathlib; sys.path.insert(0, 'src') import duckdb from quvur.ombor import OLCHOVLAR, olchov_yarat, fakt_yarat con = duckdb.connect('ombor.db') for nom, (kalit, _) in OLCHOVLAR.items(): n = olchov_yarat(con, nom, kalit) print(f'{nom:18s}: {n:>6,} qator') n = fakt_yarat(con) print(f'{\"fakt_buyurtma\":18s}: {n:>6,} qator') def hajm(jadval): return con.execute(f'SELECT count(*) FROM {jadval}').fetchone()[0] print() con.execute('DROP TABLE IF EXISTS yassi') con.execute('CREATE TABLE yassi AS SELECT * FROM xom') con.close() "
olchov_tuman : 10 qator olchov_restoran : 259 qator olchov_kuryer : 899 qator fakt_buyurtma : 60,000 qator
Endi hajmni solishtiramiz. Bitta yassi jadval va yulduz sxemasi — bir xil ma'lumot:
python3 -c " import duckdb, pathlib, os con = duckdb.connect('ombor.db') def parquet_hajm(jadval): f = f'/tmp/{jadval}.parquet' con.execute(f\"COPY {jadval} TO '{f}' (FORMAT PARQUET, COMPRESSION ZSTD)\") return pathlib.Path(f).stat().st_size yassi = parquet_hajm('yassi') yulduz = parquet_hajm('fakt_buyurtma') for t in ('olchov_tuman','olchov_restoran','olchov_kuryer'): yulduz += parquet_hajm(t) print(f'yassi jadval : {yassi/1024:>8.1f} KB') print(f'yulduz sxemasi : {yulduz/1024:>8.1f} KB') print(f'tejaldi : {(1-yulduz/yassi)*100:>8.1f}%') con.close() "
yassi jadval : 794.2 KB yulduz sxemasi : 517.1 KB tejaldi : 34.9%
Yulduz sxemasi doim kichik bo'lmaydi
Bu yerda tejash sezilarli chiqdi, lekin bu kafolat emas — natija ustunlarning turiga
bog'liq. Bizda tuman qisqa matn, restoran_id esa R065 ko'rinishidagi 4 belgili satr.
Agar o'lchovda uzun matn bo'lsa (restoran to'liq nomi, manzili, telefoni), tejash ancha katta bo'ladi. Aksincha — agar barcha ustunlar allaqachon raqam bo'lsa, tejash deyarli nolga tushadi, chunki Parquet takroriy qiymatlarni o'zi ham siqadi.
Yulduz sxemasining asosiy foydasi — joy tejash emas, balki tartib va o'zgartirish qulayligi. Joy tejalsa — bonus.
So'rov yozish: oldin va keyin
Direktorning birinchi savoliga javob beramiz.
python3 -c " import duckdb, time con = duckdb.connect('ombor.db') def olch(sql, n=5): con.execute(sql).fetchall() v = [] for _ in range(n): t0 = time.perf_counter() con.execute(sql).fetchall() v.append((time.perf_counter()-t0)*1000) return min(v) yassi_sql = ''' SELECT tuman, avg(yol_daq) FROM yassi WHERE tuman = 'Yunusobod' GROUP BY tuman ''' yulduz_sql = ''' SELECT t.tuman, avg(f.yol_daq) FROM fakt_buyurtma f JOIN olchov_tuman t ON t.id = f.tuman_id WHERE t.tuman = 'Yunusobod' GROUP BY t.tuman ''' a, b = olch(yassi_sql), olch(yulduz_sql) print(f'yassi jadval : {a:6.2f} ms') print(f'yulduz (join) : {b:6.2f} ms') print() print(f'natija bir xilmi: {con.execute(yassi_sql).fetchall()[0][1] == con.execute(yulduz_sql).fetchall()[0][1]}') con.close() "
yassi jadval : 0.80 ms yulduz (join) : 0.85 ms natija bir xilmi: True
Yulduz sxemasida JOIN qo'shildi, ya'ni ish ko'paydi. Bu — to'lov. Nimaning evaziga?
Endi direktorning ikkinchi savolini eslang: "restoran turlari bo'yicha". Yassi jadvalda buning uchun 60 ming qatorning hammasini tuzatish kerak edi. Yulduz sxemasida esa:
python3 -c " import duckdb, random con = duckdb.connect('ombor.db') # restoran turini QO'SHAMIZ - faqat o'lchov jadvaliga random.seed(42) con.execute('ALTER TABLE olchov_restoran ADD COLUMN IF NOT EXISTS turi TEXT') con.execute(\"\"\"UPDATE olchov_restoran SET turi = CASE WHEN id % 3 = 0 THEN 'fast-food' WHEN id % 3 = 1 THEN 'milliy' ELSE 'kafe' END\"\"\") n = con.execute('SELECT count(*) FROM olchov_restoran').fetchone()[0] print(f'o\\'zgartirilgan qatorlar: {n:,} (o\\'lchov jadvalida)') print(f'fakt jadvali tegilmadi : 60,000 qator o\\'zgarmadi') print() r = con.execute(''' SELECT r.turi, count(*) AS buyurtma, round(avg(f.yol_daq),2) AS ort_yol FROM fakt_buyurtma f JOIN olchov_restoran r ON r.id = f.restoran_id GROUP BY r.turi ORDER BY buyurtma DESC ''').fetchall() print(f'{\"turi\":12s} {\"buyurtma\":>10s} {\"ort_yol\":>9s}') for t, b, y in r: print(f'{t:12s} {b:>10,d} {y:>9.2f}') con.close() "
o'zgartirilgan qatorlar: 259 (o'lchov jadvalida) fakt jadvali tegilmadi : 60,000 qator o'zgarmadi turi buyurtma ort_yol milliy 20,278 17.18 fast-food 20,051 17.08 kafe 19,671 17.10
Mana to'lovning javobi. Yangi xususiyat 200 qatorlik o'lchov jadvaliga qo'shildi, 60 ming qatorlik fakt jadvali umuman tegilmadi. Va endi direktorning savoliga javob bor.
Sekin o'zgaruvchi o'lchov
Eng nozik mavzuga keldik, va u bir qarashda zerikarli ko'rinadi. Ishoning — bu bobning eng qimmat qismi.
Kuryer K0789 sentabrda Chilonzorda ishlagan. Oktabrda Yunusobodga o'tdi.
Siz uning yozuvini yangilaysiz: tuman = 'Yunusobod'. Mantiqiy, to'g'rimi? Endi ma'lumot
haqiqatga mos.
Endi direktor keladi va so'raydi: — Sentabrda qaysi tumanda yetkazish sekin edi?
Siz hisobot chiqarasiz. Va hisobotda K0789 ning sentabr buyurtmalari Yunusobod ostida chiqadi — chunki jadvalda endi shunday yozilgan.
Sentabr hisoboti o'zgarib ketdi. Siz o'tmishni tahrirlab qo'ydingiz.
Diqqat qiling, hech kim hisobotga tegmadi. Siz shunchaki bitta ustunni yangiladingiz. Lekin o'sha ustun o'n minglab eski buyurtmaga bog'langan edi.
Bu xatoni topish deyarli imkonsiz: hisobot ishlaydi, xato bermaydi, raqamlar oqilona ko'rinadi. Faqat ular noto'g'ri.
Muammoning rasmiy nomi bor: sekin o'zgaruvchi o'lchov (slowly changing dimension, SCD). "Sekin" — chunki kuryer har kuni tuman almashtirmaydi, yiliga bir marta. Aynan shuning uchun u sezilmaydi.
Uch xil javob bor:
import pandas as pd show(pd.DataFrame([ {"Turi": "1-tur (Type 1)", "Nima qiladi": "Eski qiymat ustiga yozadi", "Tarix": "Yo'qoladi", "Qachon": "Xato tuzatish"}, {"Turi": "2-tur (Type 2)", "Nima qiladi": "Yangi qator qo'shadi, eskisini yopadi", "Tarix": "Saqlanadi", "Qachon": "Haqiqiy o'zgarish"}, {"Turi": "3-tur (Type 3)", "Nima qiladi": "'oldingi_qiymat' ustuni qo'shadi", "Tarix": "Faqat bitta qadam", "Qachon": "Kamdan-kam"}, ]))
| Turi | Nima qiladi | Tarix | Qachon |
|---|---|---|---|
| 1-tur (Type 1) | Eski qiymat ustiga yozadi | Yo'qoladi | Xato tuzatish |
| 2-tur (Type 2) | Yangi qator qo'shadi, eskisini yopadi | Saqlanadi | Haqiqiy o'zgarish |
| 3-tur (Type 3) | 'oldingi_qiymat' ustuni qo'shadi | Faqat bitta qadam | Kamdan-kam |
Farqni o'lchaymiz. Kuryer K0789 sentabrda Chilonzorda ishlagan, keyin Yunusobodga o'tdi.
python3 -c " import duckdb con = duckdb.connect('ombor.db') con.execute('DROP TABLE IF EXISTS kuryer_t1') con.execute(\"\"\"CREATE TABLE kuryer_t1 AS SELECT id, kuryer_id, 'Chilonzor' AS tuman FROM olchov_kuryer\"\"\") con.execute('DROP TABLE IF EXISTS kuryer_t2') con.execute(\"\"\"CREATE TABLE kuryer_t2 AS SELECT id, kuryer_id, 'Chilonzor' AS tuman, DATE '2026-01-01' AS boshi, DATE '9999-12-31' AS oxiri, TRUE AS joriy FROM olchov_kuryer\"\"\") print('--- O\\'ZGARISHDAN OLDIN ---') for j in ('kuryer_t1', 'kuryer_t2'): n = con.execute(f\"SELECT count(*) FROM {j} WHERE kuryer_id='K0789'\").fetchone()[0] print(f'{j}: {n} qator') # K0789 Yunusobodga o'tdi con.execute(\"UPDATE kuryer_t1 SET tuman='Yunusobod' WHERE kuryer_id='K0789'\") con.execute(\"\"\"UPDATE kuryer_t2 SET oxiri=DATE '2026-09-15', joriy=FALSE WHERE kuryer_id='K0789' AND joriy\"\"\") con.execute(\"\"\"INSERT INTO kuryer_t2 SELECT id, kuryer_id, 'Yunusobod', DATE '2026-09-16', DATE '9999-12-31', TRUE FROM olchov_kuryer WHERE kuryer_id='K0789'\"\"\") print() print('--- O\\'ZGARISHDAN KEYIN ---') r1 = con.execute(\"SELECT tuman FROM kuryer_t1 WHERE kuryer_id='K0789'\").fetchall() print(f'1-tur: {len(r1)} qator -> {[x[0] for x in r1]}') print(' savol: u SENTABRDA qayerda ishlagan? javob yo\\'q, tarix o\\'chdi') r2 = con.execute(\"\"\"SELECT tuman, boshi, oxiri FROM kuryer_t2 WHERE kuryer_id='K0789' ORDER BY boshi\"\"\").fetchall() print(f'2-tur: {len(r2)} qator') for t, b, o in r2: print(f' {t:12s} {b} .. {o}') print(' savol: u SENTABRDA qayerda ishlagan? -> 1-qator, Chilonzor') con.close() "
--- O'ZGARISHDAN OLDIN --- kuryer_t1: 1 qator kuryer_t2: 1 qator --- O'ZGARISHDAN KEYIN --- 1-tur: 1 qator -> ['Yunusobod'] savol: u SENTABRDA qayerda ishlagan? javob yo'q, tarix o'chdi 2-tur: 2 qator Chilonzor 2026-01-01 .. 2026-09-15 Yunusobod 2026-09-16 .. 9999-12-31 savol: u SENTABRDA qayerda ishlagan? -> 1-qator, Chilonzor
Natija aniq. 1-turda savolga javob yo'q — tarix ustiga yozilgan. 2-turda esa har qiymatning amal qilish muddati saqlangan, shuning uchun "o'sha paytda qanday edi" degan savol javobsiz qolmaydi.
2-tur bepul emas
Har o'zgarish yangi qator qo'shadi. Tez o'zgaradigan o'lchov (masalan mijozning oxirgi faolligi) 2-tur bilan saqlansa, o'lchov jadvali fakt jadvalidan kattaroq bo'lib ketishi mumkin.
Qoida: 2-tur ni faqat tarixi haqiqatan kerak bo'lgan ustunlarga qo'llang. Qolganiga 1-tur.
Va eng muhimi: so'rovda joriy = TRUE shartini unutmang. Aks holda har kuryer bir necha
marta hisoblanadi va raqamlaringiz jim ravishda shishadi.
Tozalash
python3 -c " import duckdb, pathlib con = duckdb.connect('ombor.db') con.execute('DROP TABLE IF EXISTS yassi') con.close() for f in pathlib.Path('/tmp').glob('*.parquet'): f.unlink(missing_ok=True) print('vaqtinchalik fayllar tozalandi') print('ombor.db saqlanadi (18-bobda kerak)') "
vaqtinchalik fayllar tozalandi ombor.db saqlanadi (18-bobda kerak)
Xulosa
| Tushuncha | Asosiy fikr |
|---|---|
| Fakt | Sodir bo'lgan voqea; o'lchanadi, o'zgarmaydi |
| O'lchov | Kim/nima haqida; guruhlash uchun, o'zgaradi |
| Ajratish mezoni | Hisob qilasizmi → fakt; guruhlaysizmi → o'lchov |
| Yulduz sxemasi | O'rtada fakt, atrofida o'lchovlar |
| Surrogat kalit | Matn o'rniga raqam; joy va tartib |
| Asosiy foyda | Joy emas — o'zgartirish qulayligi |
| Yangi xususiyat | 200 qatorlik o'lchovga qo'shiladi, faktga emas |
| 1-tur | Ustiga yozadi, tarix yo'qoladi |
| 2-tur | Yangi qator, muddat bilan; tarix saqlanadi |
| 2-tur tuzog'i | joriy = TRUE ni unutsangiz raqamlar shishadi |
Talaba ishi
olchov_restoranga "manzil" va "telefon" ustunlarini qo'shing (uzun matn). Yulduz sxemasining hajm afzalligi qanday o'zgardi?- Sana o'lchovi (
olchov_sana) yarating: yil, oy, hafta kuni, dam olish kunimi. "Dam olish kunlari yetkazish sekinroqmi?" degan savolga javob bering. kuryer_t2dan foydalanib sentabr va oktabr hisobotini alohida chiqaring. Farq bormi?- 2-turdagi so'rovda
joriy = TRUEni ataylab tushirib qoldiring. Buyurtmalar soni qancha oshib ketdi?
Bu bobni bepul o'qidingiz.
Kitob o'rgatadi, lekin sizning kodingiz nega ishlamaganini ayta olmaydi. Kursda uy vazifangiz tekshiriladi.