IlmHamroh
JavaScript Full-stack/26-qism. SQL va PostgreSQL46/64-dars18 daqiqa
Mundarija (29)

Postgres'ga ommaviy yozish: COPY, batch INSERT, unnest va cursor

Qisqacha: Ko'p qatorni bittalab INSERT bilan yozish — eng sekin yo'l: kanonda 145 qator/s, 100 000 qator uchun 11 daqiqa. Tezlashtirishning uch pog'onasi: bir nechta qatorni bitta so'rovda (VALUES partiya yoki unnest($1::int[], …) — 45–135 ming qator/s) va COPY FROM STDIN (pg-copy-streams) — 1000 qatorlik bo'laklar bilan 409 ming qator/s. Tuzoqlar ham o'lchangan: COPY'ga har qatorni alohida bo'lak qilib bersangiz — 8.7 ming qator/s (eng sekinlardan); bitta so'rovda 65 535 dan ortiq parametr — chalg'ituvchi xato. Katta natijani o'qishda esa pg-cursor / pg-query-stream xotirani 229 MB dan 52 MB ga tushiradi.

Bu darsda

  • Bittalab INSERT nega sekin ekanini ikki sabab bilan tushuntirasiz.
  • Ko'p qatorli VALUES va unnest bilan partiyalab yozasiz, parametr chegarasini hisobga olasiz.
  • COPY FROM STDIN ni pipeline bilan ishlatasiz va bo'lak hajmi nega muhimligini bilasiz.
  • COPY xatosi butun importni bekor qilishini va PG 17+ ON_ERROR ignore ni bilasiz.
  • 1 mln qatorni xotirani to'ldirmay pg-cursor va pg-query-stream bilan o'qiysiz.

Oldin bilishingiz kerak: Parametrli so'rovlar va SQL injection, pipeline, finished va stream xatolari, Backpressure, INSERT va RETURNING.

1. Nega bu kerak?

Tasavvur qiling: vazifalar ilovasiga "eski ro'yxatni import qilish" tugmasi qo'shildi. Foydalanuvchi CSV fayl yuklaydi — boshqa ilovadan eksport, 5 000 qator. Yoki 22-qismdagi brauzer versiyasidan serverga ko'chish: minglab foydalanuvchining localStorage dagi vazifalari. Yoki tunda tahliliy jadvalga million qator.

Birinchi xayolga keladigan kod — sikl ichida INSERT:

js
for (const q of qatorlar) {
  await client.query(
    "INSERT INTO t46 (egasi_id, matn, bajarildi) VALUES ($1, $2, $3)",
    q,
  );
}

Kanon o'lchovi (natijalar/tajriba/t46-copy.txt, PostgreSQL 18.6 Docker'da, Node 24.21.0, 3 marta mediana):

text
bittalab INSERT, autocommit                    5000 qator:  34581 ms  (145 qator/s)

5 000 qatorga 35 soniya. 100 000 qatorga — 11 daqiqadan ortiq (kanon buni 100 000 da o'lchamadi ham, juda uzoq). Foydalanuvchi bu vaqtda brauzerni yopib yuboradi.

O'xshatish: 1000 ta xatni pochtaga yuborish. Har xat uchun pochtaga borib-kelish — bittalab INSERT. Hammasini bitta qopga solib bir marta borish — partiya. Pochta bilan "konveyer" kelishuvi — xatlarni lenta bo'ylab tinmay jo'natish — COPY.

2. Nega bittalab sekin: ikki sabab

2.1 Tarmoq aylanishi

Har await client.query(...) — so'rov ketadi, javob keladi, keyingisi ketadi. Kanonda bu aylanish ~1.3 ms edi (postgres.js va boshqa driverlar dagi ketma-ket o'lchov). 5 000 so'rov × aylanish — faqat kutishga bir necha soniya.

2.2 Har COMMIT — diskka yozish

Ikkinchi, kattaroq sabab. BEGIN siz har INSERT — o'z tranzaksiyasi (autocommit). ACID dagi D — durability: COMMIT qaytgan bo'lsa, ma'lumot elektr o'chsa ham saqlanadi. Buning uchun Postgres har COMMIT'da yozuvlar jurnalini (WAL — Write-Ahead Log) diskka majburan tushiradi (fsync). 5 000 COMMIT — 5 000 marta diskni kutish.

Hammasini bitta tranzaksiyaga solsak — COMMIT bitta:

text
bittalab INSERT, bitta tranzaksiyada           5000 qator:   6592 ms  (759 qator/s)

5 baravar tez — kodda faqat BEGIN va COMMIT qo'shildi (Tranzaksiya asoslari). Lekin tarmoq aylanishi qoldi: 5 000 so'rov hamon 5 000 marta borib-keladi. Keyingi qadam — so'rovlar sonini kamaytirish.

Tekshirib ko'ring: Bitta tranzaksiyadagi 5 000 INSERT ning 4 999-chisi xato bersa, nechta qator saqlanadi?

Javob

Hech qaysi (agar xatoni ushlab, SAVEPOINT bilan davom etmasangiz). Tranzaksiya "yo hammasi, yo hech biri". Import uchun bu ko'pincha to'g'ri xulq: yarim import qilingan fayl — eng chalkash holat. Qatorma-qator rad etish kerak bo'lsa — Tranzaksiya asoslari dagi SAVEPOINT naqshi yoki pastdagi ON_ERROR.

3. Partiya: bitta so'rovda ko'p qator

3.1 Ko'p qatorli VALUES

INSERT va RETURNING darsida ko'rgan edik: INSERT … VALUES (…), (…), (…) — bir so'rovda bir nechta qator. Node'da parametrlarni yig'amiz: har qator uchun uchta $N:

js
function valuesQur(bolak) {
  const joylar = bolak.map((_, j) =>
    `($${j * 3 + 1}, $${j * 3 + 2}, $${j * 3 + 3})`,
  );
  return {
    text: "INSERT INTO t46 (egasi_id, matn, bajarildi) VALUES " +
      joylar.join(", "),
    values: bolak.flat(),
  };
}

const s = valuesQur([
  [1, "Bozordan meva olish #bozor", false],
  [2, "IELTS writing task 2 mashqi #ingliz", true],
]);
console.log(s.text);
console.log(s.values.length); // 6

Konsolda:

text
INSERT INTO t46 (egasi_id, matn, bajarildi) VALUES ($1, $2, $3), ($4, $5, $6)
6

Qiymatlar baribir parametrda — injection xavfi yo'q (Parametrli so'rovlar). Kanon 100 000 qatorni turli partiya hajmlarida yozdi:

text
VALUES, partiya 100                          100000 qator:   6992 ms  (14302 qator/s)
VALUES, partiya 1000                         100000 qator:   2205 ms  (45360 qator/s)
VALUES, partiya 5000                         100000 qator:   1464 ms  (68286 qator/s)

Partiya 100 ham bittalabdan 19 baravar tez (14 302 va 759). 1000 dan 5000 ga o'tish esa atigi 1.5 baravar — so'rovlar soni 100 tadan 20 taga tushdi, aylanish endi asosiy xarajat emas.

3.2 Parametr chegarasi: 65 535

Kanon kattaroq partiyani sinadi — 22 000 qator, ya'ni 66 000 parametr:

text
VALUES, 22 000 qator (66 000 parametr): XATO bind message has 464 parameter formats but 0 parameters

Xabar chalg'itadi: "bind xabarida 464 ta parametr formati bor, lekin 0 ta parametr". Biz 66 000 berdik — 464 qayerdan? Postgres protokolida parametrlar soni 16 bitli son bilan yoziladi: eng ko'pi 65 535. 66 000 sig'maydi va "o'ralib" qoladi:

js
console.log(66000 % 65536); // 464
console.log(Math.floor(65535 / 3)); // 21845

Ikkinchi qator — 3 ustunli jadvalga bitta so'rovda eng ko'pi 21 845 qator. Qoida: partiya hajmi × ustunlar soni ≤ 65 535. Amalda 1 000–5 000 qator — yetarli tez va chegaradan uzoq.

3.3 unnest: ustunlar massiv bo'lib

Parametrlar sonidan butunlay qutulish yo'li bor: har ustun uchun bitta massiv parametr. Postgres'ning unnest funksiyasi bir nechta massivni qatorlarga "yoyadi":

js
await client.query(
  `INSERT INTO t46 (egasi_id, matn, bajarildi)
   SELECT * FROM unnest($1::int[], $2::text[], $3::bool[])`,
  [
    b.map((x) => x[0]),   // hamma egasi_id lar
    b.map((x) => x[1]),   // hamma matnlar
    b.map((x) => x[2]),   // hamma bajarildi lar
  ],
);

Kanon chiqishi:

text
unnest($1::int[], $2::text[], $3::bool[]), partiya 10000 100000 qator:    896 ms  (111605 qator/s)
unnest($1::int[], $2::text[], $3::bool[]), partiya 100000 100000 qator:    741 ms  (135041 qator/s)

Har so'rovda 3 ta parametr, qator soni qancha bo'lsa ham. Butun 100 000 qator bitta so'rovda — 741 ms. VALUES ning eng yaxshisidan 2 baravar tez: SQL matni qisqa, Postgres 300 000 ta $N ni tahlil qilmaydi. Bu usulning yana bir afzalligi — oddiy INSERT … SELECT: ON CONFLICT (UPSERT va MERGE) va RETURNING bilan birga ishlaydi.

Tekshirib ko'ring: 12 ustunli jadvalga VALUES bilan bitta so'rovda eng ko'pi nechta qator sig'adi? unnest bilan-chi?

Javob

VALUES: Math.floor(65535 / 12) = 5 461 qator. unnest: parametrlar soni 12 (har ustunga bitta massiv) — qator soni parametr chegarasiga bog'liq emas. Chegarani endi xotira va so'rov hajmi belgilaydi.

4. COPY FROM STDIN

4.1 Nima va qanday

COPY — Postgres'ning ommaviy yuklash buyrug'i. U SQL tahlili, rejalashtirish va har qator uchun alohida ishlov berishni chetlab o'tadi: ma'lumot CSV yoki matn oqimi sifatida kirib, to'g'ri jadvalga yoziladi. psql'dagi \copy (psql chuqur) ham aynan shuni ishlatadi.

FROM STDIN — "fayldan emas, ulanish orqali yuboraman". Node'da buni pg-copy-streams paketi (kanon: 7.0.0) Writable oqim qilib beradi. Unga oqimni pipeline bilan ulaymiz:

js
import { pipeline } from "node:stream/promises";
import { Readable } from "node:stream";
import copyStreams from "pg-copy-streams";

const csv = (q) =>
  `${q[0]},"${q[1].replaceAll('"', '""')}",${q[2]}\n`;
const kirish = client.query(copyStreams.from(
  "COPY t46 (egasi_id, matn, bajarildi) FROM STDIN WITH (FORMAT csv)",
));
await pipeline(
  Readable.from(function* () {
    for (let i = 0; i < qatorlar.length; i += 1000) {
      yield qatorlar.slice(i, i + 1000).map(csv).join("");
    }
  }()),
  kirish,
);

Generator har safar 1000 qatorni bitta satrga yig'ib beradi. pipeline backpressure'ni o'zi boshqaradi (Backpressure): Postgres ulgurmasa, generator to'xtaydi — xotira to'lmaydi.

4.2 Bo'lak hajmi: kanonning eng kutilmagan raqami

Kanon COPY'ni ikki xil sinadi — har qator alohida bo'lak va 1000 qatorlik bo'laklar:

text
COPY FROM STDIN (CSV oqim)                   100000 qator:  11477 ms  (8713 qator/s)
COPY FROM STDIN (CSV, 1000 qatorlik bo'lak)  100000 qator:    244 ms  (409061 qator/s)

Bir xil buyruq, bir xil ma'lumot — 47 baravar farq. Har qatorni alohida yield qilganda oqimga 100 000 ta mayda bo'lak tushdi. Kanon o'lchadi, lekin sababini alohida tekshirmadi; bizning izohimiz: har bo'lak drayverda alohida yozish va tarmoq paketi bo'lishi mumkin, ularning har biri xarajat. 1000 qatorlik bo'lakda — 100 ta yozish.

Saboq: "COPY — eng tez" faqat bo'laklar katta bo'lsa rost. Noto'g'ri ishlatilgan COPY partiyali VALUES dan 8 baravar sekin chiqdi.

Postgres'ga yozish tezligi (kanon, mediana)
  • bittalab, autocommit145 qator/s
  • bittalab, 1 tranzaksiya759 qator/s
  • COPY, har qator bo'lak8 713 qator/s
  • VALUES, partiya 100045 360 qator/s
  • VALUES, partiya 500068 286 qator/s
  • unnest, 100 000135 041 qator/s
  • COPY, 1000 qatorlik bo'lak409 061 qator/s

Manba: IlmHamroh kanon o'lchovi t46, 2026-10-24: Node 24.21.0, pg 8.23.1, pg-copy-streams 7.0.0, PostgreSQL 18.6 (Docker, WSL2), Windows 11, i5-12500H; 100 000 qator (bittalab — 5 000), 3 marta mediana

Grafikda chapdan o'ngga: har pog'ona tezlikni bir necha baravar oshiradi. Eng chap va eng o'ng orasida — 2 800 baravar. Bitta ustunga e'tibor bering: noto'g'ri bo'laklangan COPY pastda, partiyalardan ham sekin.

4.3 CSV'ni to'g'ri yozish

COPY CSV'ni o'zi tahlil qiladi — biz to'g'ri CSV berishimiz kerak. Matnda vergul, qo'shtirnoq yoki yangi qator bo'lsa, maydon qo'shtirnoqqa olinadi va ichidagi " ikkilanadi:

js
const csvMaydon = (s) => `"${String(s).replaceAll('"', '""')}"`;
const csvQator = (q) => q.map(csvMaydon).join(",") + "\n";

console.log(csvQator([1, 'Kitob: "O\'tkan kunlar"', true]));

Konsolda:

text
"1","Kitob: ""O'tkan kunlar""","true"

Ichki " lar "" bo'ldi. Bu — injection'ning CSV'dagi o'xshashi: to'g'ri escape qilinmagan qo'shtirnoq maydonni "sindiradi" va keyingi ustunlar siljiydi. Faqat SQL emas, ma'lumot buziladi.

4.4 Bitta yomon qator — butun COPY bekor

text
COPY yomon qator: 22P02 invalid input syntax for type integer: "x"; jadvalda 0 qator
COPY ON_ERROR ignore (PG 17+): jadvalda 2 qator

Uch qatordan ikkinchisida egasi_id o'rnida x. Natija — 0 qator: COPY bitta buyruq, bitta tranzaksiya; bitta xato — hammasi bekor. PostgreSQL 17 dan ON_ERROR ignore opsiyasi bor: tur o'girishda xato bergan qatorlarni o'tkazib yuboradi (LOG_VERBOSITY verbose — qaysilari o'tkazilganini log'ga yozadi). Kanon: 2 qator yozildi.

Diqqat: ON_ERROR ignore faqat kirish formati xatolarini (matnni songa o'girib bo'lmadi) o'tkazadi. CHECK, NOT NULL, UNIQUE, FOREIGN KEY buzilishi baribir butun COPY'ni bekor qiladi. Shuning uchun tavsiya etiladigan naqsh: avval cheklovsiz vaqtinchalik jadvalga COPY, keyin SQL bilan tekshirib, INSERT … SELECT bilan asosiy jadvalga.

5. Katta natijani o'qish

5.1 Muammo: hammasi xotirada

client.query("SELECT * FROM katta") — pg hamma qatorlarni olib, massivga yig'adi va keyin qaytaradi. Kanon 1 mln qatorli jadvalni uch usulda o'qib, Node xotirasini (heap cho'qqisi) o'lchadi:

text
butun natija (client.query)          1000000 qator, 2011 ms, heap cho'qqisi +229 MB
pg-cursor, 1000 tadan                1000000 qator, 3863 ms, heap cho'qqisi +52 MB
pg-query-stream → Writable           1000000 qator, 4019 ms, heap cho'qqisi +54 MB
Usul Vaqt Xotira Qachon
client.query 2.0 s +229 MB natija kichik (sahifa, hisobot)
pg-cursor 3.9 s +52 MB partiyalab qayta ishlash
pg-query-stream 4.0 s +54 MB oqim zanjiri (fayl, HTTP)

Butun natija — eng tez, lekin xotira 4 baravar ko'p. 10 mln qatorda u 2 GB bo'ladi va jarayon yiqiladi (Node xotira limitlari). Cursor va stream — ikki baravar sekinroq, lekin xotira natija hajmiga bog'liq emas: har lahzada faqat 1000 qator.

5.2 pg-cursor

Kursor (cursor) — Postgres tomonida "qayergacha o'qidim" belgisi. Natija serverda turadi, biz uni bo'lak-bo'lak so'raymiz:

js
import Cursor from "pg-cursor";

const cur = client.query(new Cursor("SELECT * FROM t46_oqi"));
let n = 0;
for (;;) {
  const qatorlar = await cur.read(1000);   // keyingi 1000 ta
  if (qatorlar.length === 0) break;
  n += qatorlar.length;                     // shu yerda qayta ishlash
}
await cur.close();

Har read(1000) — bitta tarmoq aylanishi. await tufayli keyingi bo'lak oldingisi qayta ishlangach so'raladi — backpressure tabiiy ravishda bor. Kursor ochiq turganda ulanish band: hovuzdan olingan bo'lsa, oxirida close() va release().

5.3 pg-query-stream

Xuddi shu kursor, lekin Readable oqim sifatida — pipeline zanjiriga to'g'ridan ulanadi:

js
import QueryStream from "pg-query-stream";
import { Writable } from "node:stream";

let n = 0;
await pipeline(
  client.query(new QueryStream("SELECT * FROM t46_oqi", [], {
    batchSize: 1000,
  })),
  new Writable({
    objectMode: true,
    write(_qator, _enc, cb) { n++; cb(); },
  }),
);

Haqiqiy ilovada Writable o'rnida — CSV'ga o'giruvchi Transform (O'z Readable/Transform stream'ingizni yozish) va HTTP javob yoki fayl. "Hamma vazifalarimni CSV qilib yuklab olish" tugmasi xotirani to'ldirmay ishlaydi.

5.4 Eksport: COPY TO STDOUT

O'qishning eng tez yo'li ham COPY — teskari yo'nalishda. Biz ishga tushirdik (PostgreSQL 17.6, Node 24.21.0, pg-copy-streams 7.0.0):

js
import pg from "pg";
import { to as copyTo } from "pg-copy-streams";
import { pipeline } from "node:stream/promises";

const client = new pg.Client({
  connectionString: process.env.DATABASE_URL,
});
await client.connect();
const sql = `COPY (SELECT id, matn, bajarildi FROM vazifalar
  WHERE egasi_id = 1 ORDER BY id LIMIT 3)
  TO STDOUT WITH (FORMAT csv, HEADER)`;
await pipeline(client.query(copyTo(sql)), process.stdout, {
  end: false,
});
await client.end();
text
id,matn,bajarildi
1,JavaScript kursining 26-qismini boshlash #o'qish,f
2,Postgres'ni Docker'da ko'tarish #o'qish #docker,t
8,Bozordan kartoshka va piyoz olish #bozor,t

Postgres CSV'ni o'zi yasadi: sarlavha (HEADER), boolean — t/f. { end: false } — process.stdout ni yopmaslik (Backpressure darsidagi qoida). process.stdout o'rnida HTTP javob bo'lsa — eksport endpoint tayyor.

6. Qaysi usulni tanlash

flowchart TD
  A[Necha qator?] -->|o'nlab| B[oddiy INSERT]
  A -->|minglab| C[unnest yoki VALUES partiya]
  A -->|yuz minglab+| D[COPY, katta bo'laklar]
  C --> E{RETURNING yoki ON CONFLICT kerakmi?}
  E -->|ha| F[unnest]
  D --> G[vaqtinchalik jadval + INSERT SELECT]

Diagramma — boshlang'ich yo'l-yo'riq. Har qarorni o'z ma'lumotingizda o'lchang: kanon raqamlari bitta mashinada, Docker ichidagi Postgres bilan olingan.

7. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
Import daqiqalab davom etadi siklda autocommit INSERT partiya, unnest yoki COPY
bind message has 464 parameter formats but 0 parameters 65 535 dan ortiq parametr partiyani kichraytiring yoki unnest
COPY kutilganidan sekin har qator alohida bo'lak 1000+ qatorni bitta bo'lakka yig'ing
Bitta yomon qator — 0 qator yozildi COPY — bitta tranzaksiya ON_ERROR ignore (PG 17+) yoki vaqtinchalik jadval
Ustunlar siljib ketdi CSV'da " escape qilinmagan csvMaydon
Katta SELECT da jarayon yiqiladi butun natija xotirada pg-cursor / pg-query-stream

8. Mashqlar

Mashqlar kurs/mashqlar/26/46-copy/ da.

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

  • Bitta so'rovdagi eng ko'p parametr —
  • 4 ustunli jadvalga VALUES bilan eng ko'p qator —
  • Kanonda eng tez usul —
  • PG 17+ da format xatoli qatorni o'tkazib yuborish — ON_ERROR
  • Kursordan keyingi 500 qatorni olish — await cur.(500)
Yechim
  • 65535 — 16 bitli son; 66 000 → 464 (66000 % 65536).
  • 16383 — Math.floor(65535 / 4).
  • COPY — 1000 qatorlik bo'laklar bilan 409 ming qator/s.
  • ignore — faqat kirish formati xatolari; cheklov buzilishi baribir bekor qiladi.
  • read — bo'sh massiv qaytsa, natija tugadi.

2-mashq (o'rta): chegarani hurmat qiluvchi partiyalovchi

partiyalar(qatorlar, ustunlar, maksParam = 65535) generator funksiyasini yozing: qatorlar massivini shunday bo'laklarga bo'lsin-ki, har bo'lakdagi parametrlar soni (bo'lak uzunligi × ustunlar) maksParam dan oshmasin. Sinang: 10 qator, 3 ustun, maksParam = 9 — bo'lak uzunliklarini chiqaring. (function* — Generator funksiyalar.)

Yechim
js
function* partiyalar(qatorlar, ustunlar, maksParam = 65535) {
  const hajm = Math.floor(maksParam / ustunlar);
  if (hajm < 1) throw new RangeError("bitta qator ham sig'maydi");
  for (let i = 0; i < qatorlar.length; i += hajm) {
    yield qatorlar.slice(i, i + hajm);
  }
}

const qatorlar = Array.from(
  { length: 10 },
  (_, i) => [i, "matn", false],
);
const bolaklar = [...partiyalar(qatorlar, 3, 9)];
const uzunliklar = bolaklar.map((b) => b.length);
console.log(uzunliklar);
console.log(Math.floor(65535 / 3)); // 21845

Konsolda:

text
[ 3, 3, 3, 1 ]
21845

maksParam = 9, 3 ustun → 3 qatordan. Oxirgi bo'lak — qolgan 1 qator. Haqiqiy ishda sukut 65 535 ni emas, o'lchangan qulay hajmni (masalan 1 000) bering: chegara — himoya, maqsad emas.

3-mashq (qiyin): CSV import vaqtinchalik jadvalga

Uchta vazifani (biri ichida " va ', biri ichida vergul va yangi qator) COPY bilan vaqtinchalik jadvalga yozing: CREATE TEMP TABLE import_v (matn text NOT NULL, bajarildi boolean) (ulanish yopilganda o'zi o'chadi — asosiy bazaga tegmaydi). CSV'ni csvMaydon/csvQator bilan yasang, pipeline + pg-copy-streams ishlating, oxirida jadvalni o'qib chiqaring.

Yechim
js
import pg from "pg";
import { from as copyFrom } from "pg-copy-streams";
import { Readable } from "node:stream";
import { pipeline } from "node:stream/promises";

const csvMaydon = (s) => `"${String(s).replaceAll('"', '""')}"`;
const csvQator = (q) => q.map(csvMaydon).join(",") + "\n";

const vazifalar = [
  ["Bozordan meva olish #bozor", false],
  ['Kitob: "O\'tkan kunlar" ni o\'qish', true],
  ["Vergul, nuqta; va\nyangi qator", false],
];

const client = new pg.Client({
  connectionString: process.env.DATABASE_URL,
});
await client.connect();
await client.query(
  "CREATE TEMP TABLE import_v " +
    "(matn text NOT NULL, bajarildi boolean)",
);
await pipeline(
  Readable.from(vazifalar.map(csvQator)),
  client.query(
    copyFrom("COPY import_v FROM STDIN WITH (FORMAT csv)"),
  ),
);
const { rows } = await client.query("SELECT * FROM import_v");
console.log(rows);
await client.end();

Biz ishga tushirdik (PostgreSQL 17.6, Node 24.21.0, pg-copy-streams 7.0.0):

text
[
  { matn: 'Bozordan meva olish #bozor', bajarildi: false },
  { matn: `Kitob: "O'tkan kunlar" ni o'qish`, bajarildi: true },
  { matn: 'Vergul, nuqta; va\nyangi qator', bajarildi: false }
]

Uchala qiyin matn ham butun keldi: " ikkilanib, vergul va yangi qator qo'shtirnoq ichida qoldi. "false" qo'shtirnoq ichida bo'lsa ham Postgres uni boolean ga o'girdi. Node matnda ham ", ham ' bo'lganini ko'rib, uni backtick bilan ko'rsatdi. Bu yerda bo'laklar kichik (3 qator) — katta importda 1000 qatorlik bo'laklarni unutmang.

9. Node qadami

vazifalar-api da bu darsning bilimi kerak bo'lgan joy bor: PUT /v1/vazifalar — butun ro'yxatni yangisi bilan almashtirish (25-qismda frontend sinxroni uchun qo'shilgan). Postgres omborida (PgOmbori.almashtirHammasi) birinchi variant (holat 44) har vazifani alohida so'rov bilan yozardi — darsning boshidagi "sikl ichida INSERT". Kanon o'lchovi (olchov/put-olchov.ts: 1000 vazifa — 500 tasi yangilanadi, 500 tasi yangi; 5 marta, mediana):

text
## holat 44
almashtirHammasi 1000 vazifa (500 yangilash + 500 yangi): 1000 qaytdi, so'rovlar 1010, mediana 2175 ms (1603, 2067, 2175, 2442, 2872)

Bitta PUT — 1 010 so'rov va 2 soniyadan ortiq. Bu yerda tranzaksiya allaqachon bitta (COMMIT bitta), demak qolgan xarajat — tarmoq aylanishlari. Davosi — «unnest: ustunlar massiv bo'lib» bo'limidagi naqsh. Qatorma-qator sikl o'rniga ikkita to'plam so'rovi (holat 48, qisqartirildi):

ts
// mavjud vazifalar — bitta UPDATE
await c.query(
  `UPDATE vazifalar v SET matn = u.matn, bajarildi = u.bajarildi,
     tartib = u.tartib, ochirilgan_vaqt = NULL
   FROM unnest($2::bigint[], $3::text[], $4::boolean[], $5::int[])
     AS u(id, matn, bajarildi, tartib)
   WHERE v.id = u.id AND v.egasi_id = $1`,
  [this.#egasi, ...ustunlar(bor)]);
// yangi vazifalar — bitta INSERT … SELECT
await c.query(
  `INSERT INTO vazifalar (id, egasi_id, matn, bajarildi, tartib)
   OVERRIDING SYSTEM VALUE
   SELECT u.id, $1, u.matn, u.bajarildi, u.tartib
   FROM unnest($2::bigint[], $3::text[], $4::boolean[], $5::int[])
     AS u(id, matn, bajarildi, tartib)`,
  [this.#egasi, ...ustunlar((i) => !bor(i))]);

ustunlar(tanla) — tanlangan vazifalardan to'rtta massiv yasaydi (id'lar, matnlar, bajarildi lar, tartib raqamlari); bor(i) — shu id bazada egasiniki bo'lsa true. Parametrlar har doim 5 ta — vazifa 10 ta bo'lsa ham, 10 000 ta bo'lsa ham, 65 535 chegarasi bu yerda xavf emas. (Asl kodda bajarilgan_vaqt uchun CASE ham bor — qisqartirishda olib tashlandi. OVERRIDING SYSTEM VALUE — mijoz bergan id ni GENERATED ALWAYS ustunga yozishga ruxsat, Primary key tanlovi.)

Xuddi shu o'lchov holat 48 da:

text
## holat 48
almashtirHammasi 1000 vazifa (500 yangilash + 500 yangi): 1000 qaytdi, so'rovlar 12, mediana 147 ms (136, 139, 147, 153, 274)

Nima bo'ldi? So'rovlar 1 010 dan 12 ga, vaqt 2 175 ms dan 147 ms ga — 15 baravar tez. Qolgan 12 so'rov vazifalar soniga bog'liq emas. Ular: BEGIN va COMMIT, egasi qulfi, id'larni ikki tekshirish (begonami, o'zimnikimi), yangi id'lar uchun sequence. Yana: yo'qlarini Savatga tushirish, ikkita unnest, sequence'ni surish, ro'yxat versiyasi va javob uchun ro'yxatni o'qish. COPY bu yerda kerak emas: 1 000 qator uchun unnest yetarli, ustiga UPDATE … FROM va RETURNING bilan ishlaydi.

Atomiklik (butun PUT yo bo'ladi, yo yo'q) va commit — Tranzaksiya asoslari darsining «Node qadami» da: holat 48 o'sha darsda yakunlanadi. Katta natijani o'qish (pg-cursor, stream) API'ga kirmadi: 25-qismdagi CSV eksport ro'yxatni allaqachon sahifalab (ombor.royxat) o'qiydi, alohida kursor kerak bo'lmadi.

10. Real ishda

  • Import/eksport — CRM'lar, buxgalteriya, "ma'lumotlaringizni yuklab oling" tugmalari (masalan Yevropadagi GDPR'ning ma'lumotni ko'chirish huquqi) — COPY va stream.
  • ETL va tahlil — kechasi millionlab qatorni ko'chirish; COPY + vaqtinchalik jadval + INSERT … SELECT … ON CONFLICT standart naqsh.
  • Migratsiya — eski tizimdan ko'chish; partiya hajmi va tranzaksiya chegaralari migratsiya rejasining asosiy qismi.
  • Intervyuda: "Million qatorni qanday tez yozasiz?", "COPY nega INSERT dan tez?", "Katta SELECT natijasini xotirani to'ldirmay qanday qayta ishlaysiz?"

Xulosa

  • Bittalab INSERT sekin: har so'rov — tarmoq aylanishi, har autocommit — diskka fsync (kanon: 145 qator/s; bitta tranzaksiyada 759).
  • Partiya: VALUES (≤ 65 535 parametr — aks holda chalg'ituvchi "464" xatosi) yoki unnest (har ustunga bitta massiv, 135 ming qator/s).
  • COPY FROM STDIN + pipeline — eng tez (409 ming qator/s), lekin faqat katta bo'laklar bilan; har qator bo'lak — 8.7 ming.
  • COPY — bitta tranzaksiya: yomon qator hammasini bekor qiladi; PG 17+ ON_ERROR ignore faqat format xatolari uchun.
  • Katta natija: pg-cursor / pg-query-stream — xotira 229 MB o'rniga ~52 MB, vaqt 2 baravar ko'p; eksport — COPY TO STDOUT.

Keyingi dars: LISTEN/NOTIFY — Postgres'ning o'zi "vazifa o'zgardi" xabarini yuboradi: broker'siz oddiy realtime va uning 8 000 baytlik chegarasi.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Postgres'ga ommaviy yozish: COPY, batch INSERT, unnest va cursor — IlmHamroh