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

SQLite: bitta fayldagi baza, WAL rejimi va bitta yozuvchi cheklovi

Qisqacha: SQLite — serversiz ma'lumotlar bazasi: butun baza bitta faylda, uni dasturning o'zi o'qiydi va yozadi. Node 24 da u node:sqlite moduli sifatida tayyor turibdi. Ikki narsani bilish shart. Birinchisi — WAL rejimi: kanon o'lchovida alohida commit'lar 136 baravar tezlashdi va o'quvchi yozuvchini kutmay qo'ydi. Ikkinchisi — bir vaqtda faqat bitta yozuvchi: ikkinchisi database is locked oladi yoki navbat kutadi. CLI, test, desktop va kichik saytlar uchun SQLite yetadi; ko'p foydalanuvchi bir vaqtda yozadigan API uchun esa — Postgres.

Bu darsda

  • SQLite serverli bazadan nimasi bilan farq qilishini chizib tushuntirasiz.
  • DELETE va WAL jurnal rejimlarini o'lchov va jonli misol bilan solishtirasiz.
  • database is locked xatosini o'zingiz chiqarasiz va timeout bilan yumshatasiz.
  • WAL'dagi bazani noto'g'ri nusxalash tuzog'ini ko'rasiz va to'g'ri zaxira usulini ishlatasiz.
  • Loyiha uchun "SQLite yetadimi?" degan savolga asosli javob berasiz.

Oldin bilishingiz kerak: Node'ning yangi o'rnatilgan imkoniyatlari, postgres.js va boshqa driverlar, Tranzaksiya asoslari, Postgres arxitekturasi va asosiy sozlamalar.

31-qism boshlandi. vazifalar-api 30-qismda NestJS'ga ko'chdi va endi uni prodakshnga tayyor holatga keltiramiz. Qism uch yo'nalishdan iborat. Avval ma'lumotlar bazasini chuqurroq ko'ramiz: boshqa bazalar, replikalar, bo'limlash, xavfsizlik. Keyin loyihaga AI xususiyati qo'shiladi — vazifani qadamlarga bo'lish. Oxirida prodakshn naqshlari va testlar keladi. Har darsda tartib bir xil: muammo → kanon o'lchovi → yechim → narxi.

Birinchi to'rt dars — tajriba darslari. Loyiha kodi o'zgarmaydi. Biz Postgres'dan tashqariga qaraymiz: SQLite, MySQL, replika va falokatdan tiklash. Bu bilimlar loyihaga kirmasa ham, ishda albatta kerak bo'ladi.

1. Nega bu kerak?

26-qismda Postgres o'rnatdik. Unga ulanish uchun server jarayoni, port, parol va ko'pincha Docker kerak bo'ldi. Endi bunday vazifalarni tasavvur qiling:

  • valyuta CLI dasturi kurslarni saqlab, yillik o'rtachani ko'rsatishi kerak.
  • Desktop ilova (masalan, kassa dasturi) internet bo'lmasa ham savdolarni yozib borishi kerak.
  • Testlar har yugurishda toza bazani bir zumda olishi kerak.

Har biri uchun alohida Postgres server ko'tarish — og'ir. Foydalanuvchining kompyuterida server bo'lmaydi. Bu yerda SQLite yordam beradi.

O'xshatish: Postgres — kutubxona. Kitob olish uchun kutubxonachiga borasiz, u javondan olib beradi. SQLite — cho'ntakdagi daftar. Kutubxonachi yo'q, daftarni o'zingiz ochib yozasiz. Daftar tez va qulay. Lekin unga bir vaqtda faqat bitta odam yoza oladi.

2. SQLite qanday ishlaydi

2.1 Serversiz baza

SQLite — dasturga ulanadigan kutubxona ko'rinishidagi ma'lumotlar bazasi. Alohida server jarayoni yo'q. SQL so'rovni dasturning o'z jarayoni bajaradi va natijani to'g'ridan-to'g'ri diskdagi fayldan o'qiydi. Bunday bazani o'rnatilgan baza (embedded database) deyishadi.

flowchart LR
  subgraph PG["Postgres"]
    A1["Ilova"] -->|"TCP, parol"| S["Server jarayoni"]
    S --> F1[("Fayllar")]
  end
  subgraph SL["SQLite"]
    A2["Ilova + SQLite"] --> F2[("vazifalar.db")]
  end

Chap qutiga qarang: Postgres'da ilova va baza orasida tarmoq va alohida jarayon bor. O'ng qutida bunday oraliq yo'q — ilovaning ichidagi SQLite kodi faylni o'zi ochadi. Shuning uchun SQLite'da tarmoq kechikishi yo'q va hamma metodlar sinxron ishlaydi (postgres.js va boshqa driverlar darsida ko'rgandik).

2.2 Qayerda uchraydi

SQLite hujjatiga ko'ra, u dunyoda eng ko'p o'rnatilgan baza dvigateli. Har Android va iPhone telefonida, Chrome va Firefox brauzerlarida, ko'p desktop dasturlarda SQLite bor. Telefoningizdagi kontaktlar ham, brauzer tarixi ham ehtimol SQLite faylida yotibdi.

Node 24.21 ichida SQLite 3.53.4 bor. Biz tekshirdik:

js
import { DatabaseSync } from "node:sqlite";

const db = new DatabaseSync(":memory:");
const q = db.prepare("SELECT sqlite_version() AS v").get();
console.log(q.v); // 3.53.4

:memory: — fayl o'rniga xotiradagi baza. U jarayon tugashi bilan yo'qoladi. Testlar uchun qulay.

2.3 Postgres'chi uchun tuzoq: erkin tiplar

SQLite Postgres'dan bir muhim joyda farq qiladi. Ustun turi u yerda qat'iy emas. INTEGER ustunga matn yozsangiz, SQLite uni rad etmaydi. Buni tur yaqinligi (type affinity) deyishadi: SQLite qiymatni ustun turiga o'girishga urinadi, o'girib bo'lmasa — o'z holicha saqlaydi.

js
import { DatabaseSync } from "node:sqlite";

const db = new DatabaseSync(":memory:");
db.exec("CREATE TABLE oddiy (narx INTEGER)");
db.exec("CREATE TABLE qatiy (narx INTEGER) STRICT");

for (const qiymat of ["35000", "osh"]) {
  db.prepare("INSERT INTO oddiy VALUES (?)").run(qiymat);
}
const qatorlar = db
  .prepare("SELECT narx, typeof(narx) AS tur FROM oddiy")
  .all();
console.log(qatorlar.map((q) => `${q.narx} (${q.tur})`));

try {
  db.prepare("INSERT INTO qatiy VALUES (?)").run("osh");
} catch (e) {
  console.log(e.message);
}

Konsolda:

text
[ '35000 (integer)', 'osh (text)' ]
cannot store TEXT value in INTEGER column qatiy.narx

"35000" satri songa o'girildi — bu yaxshi. Lekin "osh" so'zi narx ustuniga matn bo'lib yozildi va xato bermadi. Postgres bu yerda invalid input syntax for type integer derdi. Yechim — jadval oxiridagi STRICT so'zi. Ikkinchi jadval xuddi Postgres kabi qat'iy: "INTEGER ustunga TEXT qiymat sig'maydi" deb rad etdi.

Maslahat: Yangi SQLite loyihasida hamma jadvallarni STRICT bilan yarating. Aks holda xato ma'lumot jim kirib, oylar o'tib hisobotda paydo bo'ladi.

Tekshirib ko'ring: Nega SQLite'da await yozilmaydi, pg da esa har so'rovda await kerak edi?

Javob

pg so'rovni tarmoq orqali boshqa jarayonga yuboradi va javobni kutadi — bu vaqtda event loop boshqa ishlarni qilsin deb, metod Promise qaytaradi. SQLite esa faylni shu jarayonning o'zida o'qiydi. Ish qisqa va tarmoq yo'q, shuning uchun API sinxron. Lekin og'ir so'rov baribir event loop'ni to'xtatadi — buni Event loop'ni bloklash darsida ko'rgansiz.

3. Node'dan ulanish: node:sqlite va better-sqlite3

3.1 Ikki yo'l

Node'da SQLite bilan ishlashning ikki asosiy yo'li bor:

node:sqlite better-sqlite3
Qayerdan Node ichida npm paketi
O'rnatish kerak emas C++ qism, tayyor ikkilik fayl yoki kompilyatsiya
Holati (Node 24) Release candidate yillar davomida barqaror
API sinxron, prepare / run / get / all sinxron, deyarli bir xil

"Release candidate" — Node hujjatidagi barqarorlik darajasi (Stability 1.2): modul deyarli tayyor, API kamdan-kam o'zgaradi, lekin hali "to'liq barqaror" belgisi qo'yilmagan. Node 22.13 dan boshlab u bayroqsiz ishlaydi.

node:sqlite ni Node'ning yangi imkoniyatlari darsida o'rgangansiz. better-sqlite3 esa Node'ga SQLite qo'shilishidan ancha oldin paydo bo'lgan va hozir ham juda ko'p loyihada ishlatiladi. U native addon — C++ da yozilgan qism. O'rnatishda tayyor ikkilik fayl yuklanadi yoki kompyuterda kompilyatsiya qilinadi. Windows'da bu ko'pincha birinchi muammo bo'ladi.

3.2 Kanon o'lchovi

Kanon ikkalasini bitta skriptda, bitta jadval va bitta so'rovlar bilan solishtirdi (Node 24.21.0, node:sqlite — SQLite 3.53.4, better-sqlite3 12.11.1; Windows NTFS, bitta mashina, bir vaqtda):

O'lchov (WAL rejimi) node:sqlite better-sqlite3
2 000 INSERT, har biri o'z commit'i 78 ms 71 ms
100 000 INSERT bitta tranzaksiyada 160 ms 209 ms
10 000 ta birlamchi kalit bo'yicha o'qish 97.1 ms 41.1 ms

Xulosa: o'qishda better-sqlite3 taxminan 2 baravar tez, yozishda ikkalasi teng yoki node:sqlite biroz oldinda. Farq katta emas. Shuning uchun yangi loyihada node:sqlite yetadi — paket yo'q, kompilyatsiya yo'q. Eski Node (22.12 va undan oldingi) yoki o'qish juda ko'p bo'lsa — better-sqlite3.

Diqqat: Bu raqamlar — bitta mashinadagi bitta yugurish. Mutlaq millisekundlarni o'z kompyuteringizda boshqacha olasiz. Ishonsa bo'ladigani — nisbat.

4. Jurnal rejimlari: DELETE va WAL

4.1 Har commit — diskka yozish

Tranzaksiya commit bo'lganda baza "bu ma'lumot endi yo'qolmaydi" deb va'da beradi. Va'dani bajarish uchun ma'lumot diskka haqiqatan yetib borishi kerak. SQLite buni jurnal (journal) orqali qiladi. Sukut bo'yicha rejim — DELETE: commit oldidan eski sahifalar alohida jurnal fayliga ko'chiriladi, keyin asosiy fayl o'zgaradi, oxirida jurnal o'chiriladi. Har commit diskka bir necha marta majburiy yozishni talab qiladi.

WAL (write-ahead log) rejimida tartib teskari. O'zgarishlar avval alohida -wal fayli oxiriga qo'shib boriladi. Asosiy fayl keyinroq, checkpoint paytida yangilanadi. Bu g'oya sizga tanish: Postgres ham har o'zgarishni avval WAL'ga yozadi (Postgres arxitekturasi darsi).

Rejim PRAGMA buyrug'i bilan tanlanadi: PRAGMA journal_mode = WAL. PRAGMA — SQLite'ning sozlama buyrug'i. U Postgres'dagi SET buyrug'iga o'xshaydi.

Ikki sozlama farqli yashaydi. Jurnal rejimi baza faylining o'zida saqlanadi: bir marta WAL qilsangiz, keyingi ulanishlar ham WAL'da ochiladi. synchronous esa har ulanishda alohida qo'yiladi. U commit paytida diskni qanchalik qattiq majburlashni aytadi: FULL, NORMAL yoki OFF.

Kanon ikkala rejimda 2 000 ta INSERT'ni har birini alohida commit qilib yozdi (synchronous = NORMAL). Grafikda — soniyasiga nechta commit:

2 000 alohida commit: soniyasiga nechta
  • node:sqlite, DELETE189 commit/s
  • better-sqlite3, DELETE185 commit/s
  • node:sqlite, WAL25 644 commit/s
  • better-sqlite3, WAL28 168 commit/s

Manba: IlmHamroh kanon o'lchovi, 2026-11-02: Node 24.21.0, SQLite 3.53.4 / better-sqlite3 12.11.1, Windows 11 NTFS, i5-12500H; synchronous=NORMAL; bitta mashina, bitta yugurish

DELETE rejimida 2 000 ta commit 10.6 soniya oldi. WAL'da — 78 millisekund. Farq 136 baravar. Kutubxonalar orasidagi farq esa bu yerda ko'rinmaydi ham: tor joy — disk, kutubxona emas.

4.2 Bu tezlik qayerdan keladi va narxi nima

synchronous = NORMAL bilan WAL rejimi har commit'da diskni majburlamaydi. Majburiy yozish faqat checkpoint'da bo'ladi. SQLite hujjati buni ochiq aytadi: shu rejimda commit qilingan tranzaksiya elektr o'chganda yoki tizim yiqilganda orqaga qaytishi mumkin. Baza buzilmaydi — faqat oxirgi bir necha commit yo'qolishi mumkin.

Bu Postgres'dagi synchronous_commit = off ga juda o'xshaydi (Postgres arxitekturasi darsida ko'rgansiz). Tezlik evaziga eng oxirgi soniyalardagi yozuvlar xavf ostida. CLI keshi yoki log uchun bu odatda mos. Pul yoki buyurtma uchun — synchronous = FULL qo'ying.

4.3 Eng katta yutuq — tranzaksiya

Ikkinchi o'lchov: 100 000 ta INSERT bitta tranzaksiya ichida. Kanon natijasi rejimga deyarli bog'liq emas: 143–209 ms. Ya'ni bitta tranzaksiyadagi 100 000 qator alohida commit qilingan 2 000 qatordan (DELETE rejimida) 50–74 baravar tezroq yozildi.

Qoida oddiy: ko'p qator yozsangiz — bitta tranzaksiyaga o'rang. Bu Postgres'dagi ommaviy yozish qoidasining o'zi.

4.4 O'quvchi va yozuvchi

WAL'ning ikkinchi yutug'i — o'quvchilar yozuvchini to'xtatmaydi. Buni o'z ko'zingiz bilan ko'ring. Quyidagi blok bitta faylga ikkita ulanish ochadi. O'quvchi o'qish tranzaksiyasini boshlaydi, shu payt yozuvchi yangi qator qo'shmoqchi bo'ladi:

js
import { DatabaseSync } from "node:sqlite";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";

const papka = mkdtempSync(join(tmpdir(), "sqlite-"));

for (const rejim of ["DELETE", "WAL"]) {
  const fayl = join(papka, `${rejim}.db`);
  const yozuvchi = new DatabaseSync(fayl);
  const oquvchi = new DatabaseSync(fayl);
  yozuvchi.exec(`PRAGMA journal_mode = ${rejim}`);
  yozuvchi.exec("CREATE TABLE v (id INTEGER PRIMARY KEY, matn TEXT)");
  yozuvchi.exec("INSERT INTO v (matn) VALUES ('Non olish')");

  oquvchi.exec("BEGIN");
  const sana = () =>
    oquvchi.prepare("SELECT count(*) AS n FROM v").get().n;
  console.log(rejim, "o'quvchi boshida:", sana());
  try {
    yozuvchi.exec("INSERT INTO v (matn) VALUES ('Choy damlash')");
    console.log(rejim, "yozuvchi: yozdi");
  } catch (e) {
    console.log(rejim, "yozuvchi:", e.message);
  }
  console.log(rejim, "o'quvchi tranzaksiya ichida:", sana());
  oquvchi.exec("COMMIT");
  console.log(rejim, "o'quvchi tranzaksiyadan keyin:", sana());
  yozuvchi.close();
  oquvchi.close();
}
rmSync(papka, { recursive: true, force: true });

Konsolda:

text
DELETE o'quvchi boshida: 1
DELETE yozuvchi: database is locked
DELETE o'quvchi tranzaksiya ichida: 1
DELETE o'quvchi tranzaksiyadan keyin: 1
WAL o'quvchi boshida: 1
WAL yozuvchi: yozdi
WAL o'quvchi tranzaksiya ichida: 1
WAL o'quvchi tranzaksiyadan keyin: 2

Ikki rejimni qator-qator solishtiring:

  • DELETE. O'quvchi tranzaksiyasi ochiq ekan, yozuvchi commit qila olmadi: database is locked. Asosiy faylni o'zgartirish uchun hech kim uni o'qimayotgan bo'lishi kerak.
  • WAL. Yozuvchi bemalol yozdi. O'quvchi esa tranzaksiya ichida hamon 1 ni ko'radi — u tranzaksiya boshidagi suratni (snapshot) o'qiyapti. Tranzaksiya yopilgach — 2.

Bu Postgres'dagi MVCC g'oyasining kichik ukasi (MVCC darsi): har o'quvchi o'z vaqtidagi holatni ko'radi. Kanon katta bazada ham shuni o'lchadi: yozuvchi tranzaksiyasi ochiq turganda o'quvchi 16.8 ms da eski suratni o'qib qaytdi.

Tekshirib ko'ring: WAL rejimida -wal fayli cheksiz o'saveradimi?

Javob

Yo'q. Checkpoint WAL'dagi sahifalarni asosiy faylga ko'chiradi va -wal faylini boshidan qayta ishlata boshlaydi. SQLite sukut bo'yicha WAL 1 000 sahifaga yetganda avtomatik checkpoint qiladi. Lekin uzoq ochiq qolgan o'qish tranzaksiyasi checkpoint'ni to'liq tugatishga yo'l qo'ymaydi — shunda -wal haqiqatan o'sib ketadi. Postgres'dagi "uzoq tranzaksiya VACUUM'ni to'xtatadi" muammosiga o'xshaydi (VACUUM darsi).

5. Bitta yozuvchi cheklovi

5.1 database is locked

WAL o'quvchilarni ozod qildi, lekin yozuvchilar uchun qoida o'zgarmadi: bir vaqtda faqat bitta yozuvchi. Ikkinchi yozuvchi birinchisi tugashini kutishi shart. Kutish vaqti — timeout (SQLite tilida busy_timeout). node:sqlite da u sukut bo'yicha 0, ya'ni ikkinchi yozuvchi darhol xato oladi:

js
import { DatabaseSync } from "node:sqlite";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";

const papka = mkdtempSync(join(tmpdir(), "sqlite-"));
const fayl = join(papka, "vazifalar.db");
const birinchi = new DatabaseSync(fayl);
const ikkinchi = new DatabaseSync(fayl);
birinchi.exec("PRAGMA journal_mode = WAL");
birinchi.exec("CREATE TABLE v (id INTEGER PRIMARY KEY, matn TEXT)");

birinchi.exec("BEGIN IMMEDIATE");
birinchi.exec("INSERT INTO v (matn) VALUES ('Choy damlash')");
try {
  ikkinchi.exec("INSERT INTO v (matn) VALUES ('Dars qilish')");
} catch (e) {
  console.log(e.code, "—", e.message);
}
birinchi.exec("COMMIT");
ikkinchi.exec("INSERT INTO v (matn) VALUES ('Dars qilish')");
const n = ikkinchi.prepare("SELECT count(*) AS n FROM v").get().n;
console.log("qatorlar:", n);
birinchi.close();
ikkinchi.close();
rmSync(papka, { recursive: true, force: true });

Konsolda:

text
ERR_SQLITE_ERROR — database is locked
qatorlar: 2

BEGIN IMMEDIATE — tranzaksiyani yozish huquqi bilan darhol boshlash. Oddiy BEGIN esa huquqni birinchi yozish paytida oladi. Ikkinchi ulanish shu payt yozmoqchi bo'ldi va "baza qulflangan" javobini oldi. Birinchisi commit qilgach, xuddi o'sha INSERT muammosiz o'tdi.

5.2 Kutish: timeout

Haqiqiy dasturda ikkinchi yozuvchi xato bermasdan biroz kutishi kerak. Kanon buni ikki alohida jarayon bilan sinadi. Birinchi jarayon yozish tranzaksiyasini 1.5 soniya ushlab turdi, ikkinchisi shu payt yozmoqchi bo'ldi:

text
ikkinchi yozuvchi, busy_timeout=   0: ERR_SQLITE_ERROR database is locked (15 ms)
ikkinchi yozuvchi, busy_timeout=3000: yozdi (1484 ms)

Birinchi qator: kutish 0 — 15 ms da xato. Ikkinchi qator: kutish 3 soniyagacha ruxsat — ikkinchi yozuvchi navbatda turdi va birinchisi tugagach yozdi (1 484 ms). node:sqlite da bu sozlama ulanish ochilganda beriladi:

js
const db = new DatabaseSync("vazifalar.db", { timeout: 3000 });

Diqqat: timeout muammoni yashiradi, yo'q qilmaydi. Agar yozuvchilar ko'p bo'lsa, ular navbatda turib, hamma so'rov sekinlashadi. Bu — SQLite'ning tabiiy chegarasi. Uni Postgres'dagi qator darajasidagi qulflar bilan solishtiring (Qulflar darsi): u yerda ikki foydalanuvchi turli qatorlarni bir vaqtda o'zgartira oladi.

5.3 Bitta jarayon — eng yaxshi holat

SQLite'ni server ilovasida ishlatsangiz, eng xavfsiz naqsh — bazaga bitta jarayon yozsin. Masalan, bitta Node jarayoni hamma so'rovlarni qabul qiladi va o'zining yagona ulanishi orqali yozadi. Shunda database is locked deyarli uchramaydi: sinxron API tufayli yozuvlar baribir ketma-ket bajariladi. Muammo cluster yoki bir nechta server nusxasi paydo bo'lganda boshlanadi.

Tekshirib ko'ring: Kichik do'kon sayti bitta Node jarayonida ishlaydi va SQLite ishlatadi. Egasi trafik oshdi deb cluster bilan 8 ta jarayon ishga tushirdi. Nima o'zgaradi?

Javob

O'qish tezlashadi — WAL'da 8 ta jarayon bir vaqtda o'qiy oladi. Yozish esa tezlashmaydi: 8 jarayon bitta yozuvchi qulfi uchun navbatga turadi. timeout 0 bo'lsa, foydalanuvchilar database is locked xatosini ko'radi; bo'lmasa — buyurtma berish sekinlashadi. Yozuv ko'p bo'lsa, bu Postgres'ga o'tish vaqti kelganining belgisi.

6. Zaxira: faylni shunchaki nusxalamang

6.1 Tuzoq

"SQLite — bitta fayl, demak zaxira — faylni nusxalash" degan fikr xavfli. WAL rejimida bazaning bir qismi hali -wal faylida turadi. Quyidagi blok shu holatni ko'rsatadi:

js
import { DatabaseSync } from "node:sqlite";
import {
  mkdtempSync, rmSync, readdirSync, copyFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";

const papka = mkdtempSync(join(tmpdir(), "sqlite-"));
const asl = join(papka, "asl.db");
const db = new DatabaseSync(asl);
db.exec("PRAGMA journal_mode = WAL");
db.exec("CREATE TABLE v (id INTEGER PRIMARY KEY, matn TEXT)");
db.exec("INSERT INTO v (matn) VALUES ('Non olish'), ('Choy')");
console.log(readdirSync(papka).sort());

copyFileSync(asl, join(papka, "nusxa.db"));
const n = new DatabaseSync(join(papka, "nusxa.db"));
try {
  console.log(n.prepare("SELECT count(*) AS n FROM v").get().n);
} catch (e) {
  console.log("nusxada:", e.message);
}
n.close();
db.close();
console.log(readdirSync(papka).sort());
rmSync(papka, { recursive: true, force: true });

Konsolda:

text
[ 'asl.db', 'asl.db-shm', 'asl.db-wal' ]
nusxada: no such table: v
[ 'asl.db', 'nusxa.db' ]

Birinchi qatorda uchta fayl: asosiy .db, -wal (yangi o'zgarishlar) va -shm (o'quvchilar uchun umumiy indeks). Biz faqat asl.db ni nusxaladik. Nusxada jadvalning o'zi ham yo'q — CREATE TABLE hali faqat WAL'da edi. Oxirgi qator: ulanish yopilgach SQLite checkpoint qildi va -wal, -shm fayllarini o'chirdi.

6.2 To'g'ri usul

Ikki xavfsiz yo'l bor, ikkalasi ham ishlab turgan bazada:

js
import { DatabaseSync, backup } from "node:sqlite";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";

const papka = mkdtempSync(join(tmpdir(), "sqlite-"));
const db = new DatabaseSync(join(papka, "asl.db"));
db.exec("PRAGMA journal_mode = WAL");
db.exec("CREATE TABLE v (id INTEGER PRIMARY KEY, matn TEXT)");
db.exec("INSERT INTO v (matn) VALUES ('Non olish'), ('Choy')");

await backup(db, join(papka, "zaxira.db"));
db.exec(`VACUUM INTO '${join(papka, "vacuum.db")}'`);
for (const nom of ["zaxira.db", "vacuum.db"]) {
  const z = new DatabaseSync(join(papka, nom));
  console.log(nom, z.prepare("SELECT count(*) AS n FROM v").get().n);
  z.close();
}
db.close();
rmSync(papka, { recursive: true, force: true });

Konsolda:

text
zaxira.db 2
vacuum.db 2
  • backup(db, yol) — node:sqlite dagi funksiya, SQLite'ning rasmiy backup API'sini chaqiradi. Sahifalarni qismma-qism ko'chiradi, Promise qaytaradi (await shuning uchun).
  • VACUUM INTO 'fayl' — SQL buyrug'i: bazaning siqilgan, izchil nusxasini yangi faylga yozadi.

Ikkalasi ham WAL'dagi ma'lumotni hisobga oladi. Zaxira qoidalari esa Postgres'dagi bilan bir xil: 3-2-1 va muntazam tiklab ko'rish (Backup va restore darsi).

7. Tarmoqdagi SQLite: Turso va Cloudflare D1

SQLite'ning "bitta kompyuterda bitta fayl" cheklovini yengish uchun bulut xizmatlari paydo bo'ldi. Ikkalasi ham kursda sinalmagan — hisob ochish kerak. Quyidagilar rasmiy hujjatlar bo'yicha (2026-11):

  • libSQL va Turso. libSQL — SQLite'ning ochiq manbali tarmog'i (fork): unga tarmoq orqali ulanish va replikatsiya qo'shilgan. Turso — shu asosdagi bulut xizmati. Node'dan @libsql/client paketi bilan ulaniladi. Qiziq imkoniyati — ichki replika (embedded replica): ilova yonida lokal SQLite nusxasi, o'qish undan juda tez, yozish markaziy bazaga ketadi.
  • Cloudflare D1. Cloudflare Workers (serverless va edge darsi) uchun SQLite asosidagi boshqariladigan baza. Kod Worker ichida env.DB.prepare(...).bind(...) kabi API bilan so'rov yuboradi.

Ikkalasining ham g'oyasi bitta: SQL dialekti — SQLite, lekin fayl endi sizning diskingizda emas. Bitta yozuvchi tamoyili bu yerda ham saqlanadi — yozuvlar bitta asosiy nusxaga boradi. Bu g'oyani keyingi darslarda Postgres'da ko'ramiz: Replikatsiya va read replica'lar.

8. Qachon SQLite yetadi

SQLite hujjatida "qachon ishlatish kerak" degan alohida sahifa bor. Undagi g'oyani kanon o'lchovlari bilan qisqa jadvalga yig'amiz:

Vaziyat SQLite yetadimi Sabab
CLI kesh, sozlama, lokal jurnal ha bitta foydalanuvchi, fayl qulay
Desktop yoki mobil ilova ha server yo'q, offline ishlaydi
Testlar uchun tez baza ko'pincha :memory:, lekin dialekt farqi bor
Kichik sayt, bitta server ha, WAL bilan o'qish ko'p, yozish kam
Ko'p foydalanuvchi bir vaqtda yozadigan API yo'q bitta yozuvchi
Bir nechta server nusxasi yo'q fayl bitta kompyuterda
RLS, rollar, murakkab tiplar kerak yo'q SQLite'da yo'q

Test qatoriga e'tibor bering. Prodakshnda Postgres, testda SQLite ishlatish — vasvasali, lekin xavfli. STRICTsiz tiplar, boshqa sana funksiyalari, RETURNING va upsert farqlari — testlar o'tadi, prodakshn yiqiladi. Kursda testlar shuning uchun haqiqiy Postgres'da yurdi (Testcontainers darsi).

9. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
database is locked ikkinchi yozuvchi, timeout 0 timeout, bitta yozuvchi jarayon, qisqa tranzaksiya
2 000 qator 10 soniya yozildi har INSERT — alohida commit bitta tranzaksiya, WAL
Nusxada jadval yo'q WAL'dagi bazadan faqat .db nusxalangan backup() yoki VACUUM INTO
Narx ustunida "osh" erkin tiplar STRICT jadval
Tarmoq papkasida baza buziladi qulf tarmoq fayl tizimida ishonchsiz bazani lokal diskda saqlang

Oxirgi qator SQLite hujjatidan: fayl qulflari NFS kabi tarmoq fayl tizimlarida ishonchsiz ishlaydi va baza buzilishi mumkin. Biz buni sinamadik — hujjat ogohlantiradi.

10. Mashqlar

Mashqlar kurs/mashqlar/31/01-sqlite/ da. 2- va 3-mashq yechimlari Node 24.21.0 da tekshirilgan (biz ishga tushirdik).

1-mashq (oson): bo'shliqlarni to'ldiring

  • O'quvchi yozuvchini kutmaydigan jurnal rejimi —
  • Matnni INTEGER ustunga kiritmaydigan jadval kalit so'zi —
  • Yozish huquqini tranzaksiya boshidayoq oladigan buyruq — BEGIN
  • Kanon o'lchovida DELETE → WAL o'tishi alohida commit'larni necha baravar tezlashtirdi? —
  • Ishlab turgan bazaning izchil nusxasini yozadigan SQL buyrug'i —
Yechim
  • WAL — o'quvchi tranzaksiya boshidagi suratni o'qiydi, yozuvchi -wal fayliga yozadi.
  • STRICT — jadval oxiriga yoziladi; tiplar Postgres kabi qat'iy bo'ladi.
  • IMMEDIATE — boshqa yozuvchi bo'lsa, xato tranzaksiya boshida chiqadi, o'rtasida emas.
  • 136 — 10 596 ms dan 78 ms ga (node:sqlite).
  • VACUUM INTO — yoki node:sqlite dagi backup() funksiyasi.

2-mashq (o'rta): paketlab yozish

paketlabYoz(db, matnlar) funksiyasini yozing. U hamma matnni bitta tranzaksiyada v jadvaliga qo'shsin va qo'shilgan qatorlar sonini qaytarsin. Agar biror qiymat xato bersa (masalan null — ustun NOT NULL), tranzaksiya to'liq bekor bo'lsin va xato tashqariga uzatilsin. Ishora: BEGIN → sikl → COMMIT, catch ichida ROLLBACK va throw (try/catch/finally).

Yechim
js
import { DatabaseSync } from "node:sqlite";

function paketlabYoz(db, matnlar) {
  const qosh = db.prepare("INSERT INTO v (matn) VALUES (?)");
  db.exec("BEGIN");
  try {
    for (const matn of matnlar) qosh.run(matn);
    db.exec("COMMIT");
    return matnlar.length;
  } catch (e) {
    db.exec("ROLLBACK");
    throw e;
  }
}

const db = new DatabaseSync(":memory:");
db.exec(
  "CREATE TABLE v (id INTEGER PRIMARY KEY, matn TEXT NOT NULL)",
);
const sana = () => db.prepare("SELECT count(*) AS n FROM v").get().n;

console.log(paketlabYoz(db, ["Non olish", "Choy", "Dars"]));
try {
  paketlabYoz(db, ["Bozor", null, "Sport"]);
} catch (e) {
  console.log("xato:", e.message);
}
console.log("jadvalda:", sana());

Konsolda:

text
3
xato: NOT NULL constraint failed: v.matn
jadvalda: 3

Ikkinchi chaqiruvda "Bozor" yozilgan edi, lekin null xato berdi va ROLLBACK uni ham bekor qildi. Jadvalda faqat birinchi paketning 3 qatori qoldi — "hammasi yoki hech biri". Bitta tranzaksiya tezlik ham beradi: barcha qatorlar bitta commit bilan diskka tushadi.

3-mashq (qiyin): band bo'lsa qayta urinish

qaytaUrin(amal, urinishlar, kutish) funksiyasini yozing. U amal() ni chaqirsin. Xato xabari database is locked bo'lsa — kutish(n) ni chaqirib (n — urinish raqami), qaytadan urinsin. Boshqa xato yoki urinishlar tugasa — xatoni tashqariga uzatsin. Natija: { natija, urinish }. Tekshirish uchun kutish ichida ikkinchi urinishdan keyin birinchi yozuvchining COMMIT ini chaqiring — shunda natija deterministik bo'ladi. Ishora: for sikl va try/catch; e.message.includes(...) (matn ichida qidirish).

Yechim
js
import { DatabaseSync } from "node:sqlite";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";

function qaytaUrin(amal, urinishlar, kutish) {
  for (let n = 1; ; n++) {
    try {
      return { natija: amal(), urinish: n };
    } catch (e) {
      const band = e.message.includes("database is locked");
      if (!band || n >= urinishlar) throw e;
      console.log(`urinish ${n}: band`);
      kutish(n);
    }
  }
}

const papka = mkdtempSync(join(tmpdir(), "sqlite-"));
const fayl = join(papka, "v.db");
const birinchi = new DatabaseSync(fayl);
const ikkinchi = new DatabaseSync(fayl);
birinchi.exec("PRAGMA journal_mode = WAL");
birinchi.exec("CREATE TABLE v (id INTEGER PRIMARY KEY, matn TEXT)");
birinchi.exec("BEGIN IMMEDIATE");

const yoz = () =>
  ikkinchi.prepare("INSERT INTO v (matn) VALUES (?)")
    .run("Dars").changes;
const kutish = (n) => {
  if (n === 2) birinchi.exec("COMMIT");
};
console.log(qaytaUrin(yoz, 5, kutish));

birinchi.exec("BEGIN IMMEDIATE");
try {
  qaytaUrin(yoz, 2, () => {});
} catch (e) {
  console.log("taslim:", e.message);
}
birinchi.exec("COMMIT");
birinchi.close();
ikkinchi.close();
rmSync(papka, { recursive: true, force: true });

Konsolda:

text
urinish 1: band
urinish 2: band
{ natija: 1, urinish: 3 }
urinish 1: band
taslim: database is locked

Birinchi chaqiruvda ikki urinish band bo'ldi, ikkinchisidan keyin birinchi yozuvchi bo'shadi va uchinchi urinish o'tdi. Ikkinchi chaqiruvda yozuvchi bo'shamadi: 2 urinish tugadi va xato tashqariga chiqdi. Haqiqiy kodda kutish — kichik pauza (masalan timers/promises bilan) va har urinishda uzayib boradi. Lekin ko'p hollarda bu ishni timeout sozlamasi o'zi bajaradi — o'z qayta urinish siklingiz faqat tranzaksiyani butunlay qaytadan boshlash kerak bo'lganda kerak.

11. Loyiha qadami: kanon tajribasi

Bu darsda vazifalar-api o'zgarmaydi va commit yo'q. Loyiha Postgres'da qoladi — sabablari aniq:

  • API'ga ko'p foydalanuvchi bir vaqtda yozadi — SQLite'da bitta yozuvchi.
  • 26–28-qismlarda qurgan narsalarimiz — RLS, rollar, triggerlar, o'zbekcha FTS, timestamptz — SQLite'da yo'q yoki boshqacha.
  • Kelajakda API bir nechta server nusxasida ishlaydi (33-qism) — fayl esa bitta kompyuterda.

Kanon tajribasi alohida skriptda bo'ldi: node:sqlite va better-sqlite3, DELETE va WAL, ikkinchi jarayon bilan qulf. Darsdagi raqamlar o'sha yugurishdan. Uning qisqa variantini o'zingiz takrorlang — paket ham, Docker ham kerak emas. Istalgan papkada sqlite-olchov.mjs faylini yarating:

js
import { DatabaseSync } from "node:sqlite";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";

const papka = mkdtempSync(join(tmpdir(), "sqlite-olchov-"));

function olchov(rejim) {
  const db = new DatabaseSync(join(papka, `${rejim}.db`));
  db.exec(`PRAGMA journal_mode = ${rejim}`);
  db.exec("PRAGMA synchronous = NORMAL");
  db.exec("CREATE TABLE v (id INTEGER PRIMARY KEY, matn TEXT)");
  const qosh = db.prepare("INSERT INTO v (matn) VALUES (?)");

  let t0 = performance.now();
  for (let i = 0; i < 2000; i++) qosh.run(`vazifa ${i}`);
  const alohida = performance.now() - t0;

  t0 = performance.now();
  db.exec("BEGIN");
  for (let i = 0; i < 100_000; i++) qosh.run(`vazifa ${i}`);
  db.exec("COMMIT");
  const paket = performance.now() - t0;
  db.close();
  console.log(
    `${rejim.padEnd(6)} 2 000 alohida commit: ` +
      `${alohida.toFixed(0)} ms | 100 000 bitta ` +
      `tranzaksiyada: ${paket.toFixed(0)} ms`,
  );
}

olchov("DELETE");
olchov("WAL");
rmSync(papka, { recursive: true, force: true });
bash
node sqlite-olchov.mjs

Biz ishga tushirdik (Node 24.21.0, Windows 11, mashina band edi):

text
DELETE 2 000 alohida commit: 21323 ms | 100 000 bitta tranzaksiyada: 157 ms
WAL    2 000 alohida commit: 94 ms | 100 000 bitta tranzaksiyada: 146 ms

Bizda DELETE rejimi kanondagidan ham sekin chiqdi (21 s), nisbat esa ~227 baravar. Sizning raqamlaringiz boshqacha bo'ladi — diskka, antivirusga va mashina yukiga bog'liq. Lekin manzara bir xil bo'lishi kerak: DELETE'da alohida commit'lar soniyalar oladi, WAL'da — millisekundlar, bitta tranzaksiya esa ikkala rejimda ham tez.

12. Real ishda

  • Qayerda uchraydi: mobil ilovalar (Android Room, iOS Core Data ichida SQLite), Electron desktop dasturlari, CLI vositalar, brauzer. Backend'da — kichik saytlar, prototiplar, edge funksiyalar (Turso, D1).
  • Intervyu savollari: "SQLite qachon yetarli?", "WAL nima beradi?", "Nega SQLite'da bir vaqtda faqat bitta yozuvchi?", "SQLite bazasini qanday zaxiralaysiz?"
  • Kod review'da: sikl ichida tranzaksiyasiz yozish, STRICTsiz jadval, timeoutsiz ulanish — tez-tez uchraydigan izohlar.
  • Tanlovda: "SQLite'dan boshlab, keyin Postgres'ga o'tamiz" — mumkin, lekin dialekt farqlari tufayli ko'chish bepul emas. Ko'p foydalanuvchili API'ni boshidan Postgres'da qurish odatda arzonroq.

Xulosa

  • SQLite — serversiz, bitta fayldagi baza; Node 24 da node:sqlite tayyor, better-sqlite3 — muqobil paket.
  • WAL rejimi alohida commit'larni kanon o'lchovida 136 baravar tezlashtirdi va o'quvchilarni yozuvchidan ozod qildi.
  • synchronous = NORMAL + WAL — elektr o'chsa oxirgi commit'lar yo'qolishi mumkin; muhim ma'lumot uchun FULL.
  • Bir vaqtda faqat bitta yozuvchi: database is locked ni timeout va bitta yozuvchi jarayon bilan boshqaring.
  • Zaxira — backup() yoki VACUUM INTO; WAL'dagi bazadan .db faylini nusxalash yetmaydi.
  • vazifalar-api Postgres'da qoladi: ko'p yozuvchi, RLS, rollar va bir nechta server nusxasi.

Keyingi dars: MySQL va MariaDB — Postgres'ni bilgan holda MySQL loyihasiga tushsangiz, qaysi tuzoqlar kutadi: utf8, vaqt zonasi, upsert va Prisma bilan ko'chish.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
SQLite: bitta fayldagi baza, WAL rejimi va bitta yozuvchi cheklovi — IlmHamroh