Asosiy qismga o'tish

Data Engineering va MLOps: chegara qayerda

9-bob 6 kod bloki 1802 so'z

Ikki kasb, bitta poydevor

I qismda model bilan ishladik: takrorlanish, versiya, sifat, Docker, quvur. Har safar bir narsa tayyor deb faraz qildik — jadval. ijara.csv bor edi, uni o'qidik.

Haqiqiy kompaniyada o'sha jadval osmondan tushmaydi. Kimdir uni yig'adi, tozalaydi, birlashtiradi va har kuni yangilab turadi. Bu ish — Data Engineering.

python21 qator
diagramma(
    {
        "Ilova bazasi\n(Postgres)": (0, 3.2, 2.3, 1.0, "manba"),
        "Hodisalar\n(klik, buyurtma)": (0, 1.6, 2.3, 1.0, "manba"),
        "Tashqi API\n(karta, ob-havo)": (0, 0.0, 2.3, 1.0, "manba"),
        "Yig'ish\n(Kafka, CDC)": (3.1, 1.6, 2.1, 1.0, "qayta"),
        "Saqlash\n(Iceberg)": (5.9, 1.6, 2.1, 1.0, "saqlash"),
        "Jadval\n(ijara.csv)": (8.7, 1.6, 2.1, 1.0, "saqlash"),
        "Model\n(I va III qism)": (11.5, 1.6, 2.1, 1.0, "xizmat"),
    },
    [
        ("Ilova bazasi\n(Postgres)", "Yig'ish\n(Kafka, CDC)"),
        ("Hodisalar\n(klik, buyurtma)", "Yig'ish\n(Kafka, CDC)"),
        ("Tashqi API\n(karta, ob-havo)", "Yig'ish\n(Kafka, CDC)"),
        ("Yig'ish\n(Kafka, CDC)", "Saqlash\n(Iceberg)"),
        ("Saqlash\n(Iceberg)", "Jadval\n(ijara.csv)"),
        ("Jadval\n(ijara.csv)", "Model\n(I va III qism)"),
    ],
    nom="de_chegara.png", kenglik=15,
    izoh="II qism chapdagi to'rt quti haqida. I va III qism eng o'ngdagisi haqida edi.",
)

II qism chapdagi to'rt quti haqida. I va III qism eng o'ngdagisi haqida edi.

II qism chapdagi to'rt quti haqida. I va III qism eng o'ngdagisi haqida edi.

Chegara aniq: Data Engineer jadvalni yetkazadi, ML muhandis modelni yetkazadi. Lekin ikkalasi ham bir xil savollarga javob beradi — qaysi versiya, qachon yangilandi, sifat tekshirildimi. Shuning uchun II qism MLOps kitobida turibdi: bu alohida fan emas, bir xil poydevor.

OLTP va OLAP: nega ikki xil baza kerak

Eng ko'p chalkashtiriladigan joy shu, va u har doim bitta savol bilan boshlanadi:

— Bizda allaqachon Postgres bor. Nega yana boshqa baza kerak?

Javobni ikki xil kutubxonachi bilan tushuntiraman.

Birinchi kutubxonachi — javondagi kitobni topib beradi. Siz kelasiz, "Alisher Navoiy, Xamsa" deysiz. U katalogga qaraydi, javon raqamini topadi, borib oladi. O'ttiz soniya.

Ikkinchi kutubxonachi — statistika yuritadi. Siz so'raysiz: "Bu yil qaysi janrdagi kitoblar eng ko'p olingan?" U birinchi kutubxonachiga borsa, u bitta-bitta hamma kartochkani ko'rishi kerak bo'ladi — bir hafta ketadi.

Ikkalasi ham kerak. Ikkalasi ham "kutubxonachi". Lekin ular butunlay boshqa ish qiladi va boshqa tartibda saqlaydi.

Ma'lumotlar bazasida ham shunday, va ikki ishning rasmiy nomi bor:

  • OLTP (Online Transaction Processing) — "M0076 mijozning oxirgi buyurtmasi qaysi?" Bitta qator, juda tez. Ilova shu bilan ishlaydi.
  • OLAP (Online Analytical Processing) — "Sentabrda tumanlar bo'yicha o'rtacha yetkazish vaqti qancha?" Barcha qatorlar. Hisobot shu bilan ishlaydi.

Nomlarni yodlash shart emas. Farqni eslang: bitta qator va butun ustun.

Endi buni Newapp buyurtmalarida o'lchaymiz. Avval bir xil ma'lumotni ikki formatda saqlaymiz:

python19 qator
python3 -c "
import sqlite3, time, pathlib
import pandas as pd, pyarrow as pa, pyarrow.parquet as pq

d = pd.read_csv('../datasets/buyurtma.csv')
print(f'buyurtmalar: {len(d):,} qator, {len(d.columns)} ustun')

# OLTP uslubi: qatorli saqlash + indeks
con = sqlite3.connect('buyurtma.db')
d.to_sql('buyurtma', con, index=False, if_exists='replace')
con.execute('CREATE INDEX i_kuryer ON buyurtma(kuryer_id)')
con.commit(); con.close()

# OLAP uslubi: ustunli saqlash
pq.write_table(pa.Table.from_pandas(d), 'buyurtma.parquet', compression='zstd')

for f in ['buyurtma.db', 'buyurtma.parquet']:
    print(f'  {f:20s} {pathlib.Path(f).stat().st_size/1024:8.1f} KB')
"
Chiqish (qaytish kodi 0, 3033 ms)
buyurtmalar: 60,000 qator, 10 ustun
  buyurtma.db            6060.0 KB
  buyurtma.parquet        900.3 KB

Endi ikki savolni ikkala saqlashda ham o'lchaymiz:

python38 qator
python3 -c "
import sqlite3, time
import numpy as np, pyarrow.parquet as pq

def olch(f, n=20):
    f()                                   # isitish
    v = [ (lambda t0=time.perf_counter(): (f(), (time.perf_counter()-t0)*1000)[1])() for _ in range(n) ]
    return np.median(v)

con = sqlite3.connect('buyurtma.db')

def oltp_sqlite():
    return con.execute(\"SELECT * FROM buyurtma WHERE kuryer_id='K0789'\").fetchall()

def oltp_parquet():
    t = pq.read_table('buyurtma.parquet')
    return t.filter(t['kuryer_id'].to_pandas().values == 'K0789')

def olap_sqlite():
    return con.execute('SELECT tuman, AVG(yol_daq) FROM buyurtma GROUP BY tuman').fetchall()

def olap_parquet():
    t = pq.read_table('buyurtma.parquet', columns=['tuman', 'yol_daq']).to_pandas()
    return t.groupby('tuman')['yol_daq'].mean()

nom1 = 'OLTP: bitta kuryer qatorlari'
nom2 = 'OLAP: tuman kesimida o\'rtacha'
print(f'{\"savol\":34s} {\"SQLite\":>10s} {\"Parquet\":>10s}')
print('-' * 56)
a, b = olch(oltp_sqlite), olch(oltp_parquet)
print(f'{nom1:34s} {a:9.2f}ms {b:9.2f}ms')
c, e = olch(olap_sqlite), olch(olap_parquet)
print(f'{nom2:34s} {c:9.2f}ms {e:9.2f}ms')
print()
print(f'OLTP da SQLite {b/a:.1f}x tez')
print(f'OLAP da Parquet {c/e:.1f}x tez')
con.close()
"
Chiqish (qaytish kodi 0, 10437 ms)
savol                                  SQLite    Parquet
--------------------------------------------------------
OLTP: bitta kuryer qatorlari            0.60ms     11.38ms
OLAP: tuman kesimida o'rtacha         386.80ms     42.44ms

OLTP da SQLite 19.1x tez
OLAP da Parquet 9.1x tez

Natijani diqqat bilan o'qing — bu jadval bobning butun mazmuni.

Bitta g'olib yo'q. Har savolda boshqa format yutdi, va farq kichik emas — o'nlab barobar, ikki yo'nalishda ham.

Nega shunday? Ikki sabab, va ikkalasi ham 4-bobdagi kundalik-daftar rasmiga qaytadi.

OLTP da SQLite yutdi, chunki unda indeks bor. Indeks — bu kitob oxiridagi mundarija: "K0789 ni qidiryapsizmi? 4712-qatorga qarang". U butun jadvalni ko'rmasdan to'g'ri joyga boradi.

Parquet esa bu savolda butun faylni ochib, har qatorni tekshirdi. Mundarija yo'q.

OLAP da Parquet yutdi, chunki u tuman va yol_daq ni o'qiyotganda qolgan 8 ta ustunni diskdan umuman ko'tarmaydi. SQLite esa qatorli saqlaydi — bitta ustunni olish uchun har qatorning butunini o'qishi kerak, keyin keraksiz qismini tashlaydi.

Ya'ni ikkalasi ham "sekin" emas. Har biri o'zi mo'ljallanmagan savolda sekin.

Kutubxonachilarni eslang: birinchisidan statistika so'rasangiz, u yomon kutubxonachi emas — siz noto'g'ri odamga murojaat qilgansiz.

"Bazamiz sekin, indeks qo'shaylik"

Hisobot sekin ishlayotganda birinchi instinkt — indeks qo'shish. Lekin indeks OLTP muammosini hal qiladi: "bitta qatorni top". Hisobot esa barcha qatorlarni o'qiydi.

Barcha qatorlar kerak bo'lganda indeks yordam bermaydi, aksincha yozishni sekinlashtiradi. Bu yerda kerak bo'lgan narsa — indeks emas, boshqa saqlash formati.

Batch va oqim

Ikkinchi chegara — qachon.

python7 qator
import pandas as pd
show(pd.DataFrame([
    {"": "Batch", "qachon ishlaydi": "Jadval bo'yicha (masalan har tunda)",
     "kechikish": "Soatlar", "misol": "Kunlik broker hisoboti"},
    {"": "Oqim (streaming)", "qachon ishlaydi": "Hodisa kelishi bilan",
     "kechikish": "Soniyalar", "misol": "Ballon skanerlanganda ogohlantirish"},
]))
qachon ishlaydikechikishmisol
BatchJadval bo'yicha (masalan har tunda)SoatlarKunlik broker hisoboti
Oqim (streaming)Hodisa kelishi bilanSoniyalarBallon skanerlanganda ogohlantirish

Farqni his qilish uchun butun 40 mingta hodisani ikki usulda hisoblaymiz:

python32 qator
python3 -c "
import time
import pandas as pd

d = pd.read_csv('../datasets/ballon.csv', parse_dates=['vaqt'])
print(f'jami hodisalar: {len(d):,}')

t0 = time.perf_counter()
natija = d.groupby('ombor').size()
batch_ms = (time.perf_counter() - t0) * 1000

t0 = time.perf_counter()
holat = {}
for omb in d['ombor'].values:
    holat[omb] = holat.get(omb, 0) + 1
oqim_ms = (time.perf_counter() - t0) * 1000

nisbat = oqim_ms / batch_ms
bitta = oqim_ms / len(d) * 1000
oxirgi = d['vaqt'].max()

print()
print(f'BATCH: bitta guruhlash        -> {batch_ms:7.1f} ms')
print(f'OQIM : har hodisada yangilash -> {oqim_ms:7.1f} ms')
print(f'  bitta hodisaga {bitta:.2f} mikrosekund')
print()
print(f'natijalar bir xilmi: {dict(natija) == holat}')
print(f'oqim batchdan {nisbat:.1f}x ko\'p vaqt oldi')
print()
print(f'BATCH javobi {oxirgi} dan keyin tayyor')
print('OQIM  javobi har hodisadan keyin tayyor')
"
Chiqish (qaytish kodi 0, 373 ms)
jami hodisalar: 40,000

BATCH: bitta guruhlash        ->     1.2 ms
OQIM : har hodisada yangilash ->     2.2 ms
  bitta hodisaga 0.05 mikrosekund

natijalar bir xilmi: True
oqim batchdan 1.8x ko'p vaqt oldi

BATCH javobi 2026-09-30 23:55:50 dan keyin tayyor
OQIM  javobi har hodisadan keyin tayyor

Mana asosiy savdo (trade-off): oqim umumiy ishni ko'paytiradi, lekin kechikishni yo'qotadi. Batch arzonroq, lekin javob kun oxirida keladi.

Kichik namunada o'lchov teskari chiqadi

Men bu tajribani avval bir kunlik ma'lumotda (1291 hodisa) o'tkazgandim. Natija: oqim batchdan tez chiqdi — 0.2x. Ya'ni yuqorida yozgan gapimning teskarisi.

Sabab: 1291 qatorda pandas.groupby ning o'z tayyorgarligi hisobning o'zidan qimmat. Batchning afzalligi hajm kattalashganda ochiladi, chunki u vektorlashtirilgan.

Shuning uchun bu yerda butun 40 ming hodisa o'lchanadi. Saboq umumiy: "qaysi usul tez" degan savolga javob berishdan oldin qaysi hajmda deb so'rang. Kichik namunadagi natija ishlab chiqarishda teskari bo'lishi mumkin.

Qaysi biri kerakligini qanday bilasiz

Savol texnik emas, biznesniki: javob 6 soat kechiksa nima yo'qoladi?

  • Kunlik broker hisoboti kechiksa — hech narsa. Batch.
  • Ballon g'oyib bo'lganini ertasiga bilsangiz — ballon ketdi. Oqim.

Oqimni "zamonaviyroq" degani uchun tanlash — eng qimmat xato.

Kim nimani yetkazadi

python11 qator
import pandas as pd
show(pd.DataFrame([
    {"Vazifa": "Postgres dan o'zgarishlarni olish", "Egasi": "Data Engineer", "Bob": "11"},
    {"Vazifa": "Hodisalar oqimini yig'ish", "Egasi": "Data Engineer", "Bob": "10"},
    {"Vazifa": "Jadval sxemasi va evolyutsiyasi", "Egasi": "Data Engineer", "Bob": "15"},
    {"Vazifa": "Ma'lumot sifati tekshiruvi", "Egasi": "Ikkalasi", "Bob": "6"},
    {"Vazifa": "Xususiyat hisoblash", "Egasi": "Ikkalasi", "Bob": "3"},
    {"Vazifa": "Model o'qitish va baholash", "Egasi": "ML muhandis", "Bob": "24"},
    {"Vazifa": "Model versiyasi va registr", "Egasi": "ML muhandis", "Bob": "23"},
    {"Vazifa": "Inference servis", "Egasi": "ML muhandis", "Bob": "27"},
]))
VazifaEgasiBob
Postgres dan o'zgarishlarni olishData Engineer11
Hodisalar oqimini yig'ishData Engineer10
Jadval sxemasi va evolyutsiyasiData Engineer15
Ma'lumot sifati tekshiruviIkkalasi6
Xususiyat hisoblashIkkalasi3
Model o'qitish va baholashML muhandis24
Model versiyasi va registrML muhandis23
Inference servisML muhandis27

"Ikkalasi" qatorlariga e'tibor bering — kelishmovchilik aynan o'sha yerda tug'iladi. 3-bobdagi xususiyat siljishi (skew) shuning natijasi: o'qitish xususiyatni bir joyda, xizmat boshqa joyda hisoblaydi. II qism oxirida bu muammoni jadval darajasida hal qilamiz.

II qismda nima quriladi

Bob Mavzu Nimani ishga tushiramiz
10 Apache Kafka Haqiqiy broker, topic, partition, consumer group
11 CDC Postgres → Debezium → Kafka
12 Spark asoslari DataFrame, dangasa hisoblash, shuffle
13 Spark on Kubernetes Driver va executor pod
14 Oqimli qayta ishlash Oyna, watermark, kech kelgan ma'lumot
15 Apache Iceberg Time travel, sxema evolyutsiyasi
16 Lakehouse Ombor va ko'l birlashgani
17 Ombor modellashtirish Fakt va o'lchov jadvallari
18 So'rov optimizatsiyasi Partitsiya, statistika, reja o'qish
19 Tez serving qatlami ClickHouse uslubidagi o'qish
20 O'zini tuzatuvchi quvur Qayta urinish, karantin, ogohlantirish
21 DE loyihasi Uchtasini bitta quvurga yig'ish

Xulosa

Tushuncha Asosiy fikr
Chegara DE jadvalni, MLOps modelni yetkazadi
OLTP Bitta qator kerak — indeksli qatorli baza
OLAP Butun ustun kerak — ustunli format
Bitta g'olib yo'q O'lchagan ikki savol, ikki xil g'olib
Batch Arzon, lekin javob kechikadi
Oqim Ko'proq ish, lekin javob doim tayyor
Tanlov mezoni "6 soat kechiksa nima yo'qoladi?"
Umumiy joy Sifat va xususiyat — ikkalasining ishi

Talaba ishi

  1. ijara.csv uchun ham ikkala saqlashni yarating va o'sha ikki savolni o'lchang. Jadval kichraygani uchun farq qanday o'zgardi?
  2. SQLite da tuman ustuniga indeks qo'shing va OLAP so'rovini qayta o'lchang. Tezlashdimi? Nega?
  3. Parquet dan barcha ustunlarni o'qib ko'ring (columns= ni olib tashlang). Ustunli formatning afzalligi qanchasi yo'qoladi?
  4. Ballon hodisalarida oqim hisobini 1 daqiqalik oynaga bo'ling. Endi "javob doim tayyor" degani qanchalik to'g'ri?

Bu bobni bepul o'qidingiz.

Kitob o'rgatadi, lekin sizning kodingiz nega ishlamaganini ayta olmaydi. Kursda uy vazifangiz tekshiriladi.

Telegramda yozish Narxlar