SSD Nodes Learn RAM 8GB — $66/ปี
คู่มือ Matt Connorโดย Matt Connor · อัปเดตเมื่อ 2026-08-01

DuckDB เทียบกับ SQLite บนเซิร์ฟเวอร์ ใช้ตัวไหนดี

SQLite เหมาะกับสถานะแอปแบบธุรกรรม ส่วน DuckDB เหมาะกับการวิเคราะห์ Parquet และ CSV ดูเหตุผลที่ VPS เดียวใช้ทั้งคู่ พร้อมตัวอย่างการใช้งานจริง

DuckDB เทียบกับ SQLite บนเซิร์ฟเวอร์: คำตอบในหนึ่งประโยค

SQLite เป็น engine สำหรับ OLTP (การประมวลผลธุรกรรมออนไลน์): จัดเก็บข้อมูลเป็นแถว และออกแบบมาเพื่ออ่านและเขียนข้อมูลครั้งละไม่กี่แถวอย่างปลอดภัยและรวดเร็ว ส่วน DuckDB เป็น engine สำหรับ OLAP (การประมวลผลเชิงวิเคราะห์ออนไลน์): จัดเก็บข้อมูลเป็นคอลัมน์ และออกแบบมาเพื่อสแกนข้อมูลหลายล้านแถวแล้วส่งคืนผลรวมหนึ่งรายการ ทั้งสองเป็นไลบรารีแบบฝังตัว ทั้งสองเปิดไฟล์ธรรมดา และไม่มีตัวใดต้องเรียกใช้กระบวนการเซิร์ฟเวอร์ที่คุณต้องคอยดูแล

ดังนั้น คำตอบตามจริงสำหรับคำถามว่า “ควรเลือกตัวใด” มักเป็น “ใช้ทั้งสองตัวบน VPS เดียวกัน” แอปพลิเคชันของคุณเก็บสถานะที่ใช้งานอยู่ใน SQLite ส่วนระบบรายงานอ่านไฟล์ Parquet และ CSV ด้วย DuckDB ทั้งสองไม่ได้แข่งขันกัน เพราะไม่ได้ทำงานประเภทเดียวกัน

เหตุใดการจัดเก็บแบบแถวและแบบคอลัมน์จึงทำให้คำตอบแตกต่างกัน

SQLite เขียนแถวเป็นข้อมูลต่อเนื่องชิ้นเดียวภายใน page การดึง order รายการหนึ่งด้วย primary key จะเข้าถึง index page หนึ่งหน้าและ data page หนึ่งหน้า รวมเป็นการอ่าน 2 ครั้ง นี่ตรงกับสิ่งที่แอปพลิเคชันทำหลายพันครั้งต่อวินาที ได้แก่ อ่าน user นี้ อัปเดต session นี้ และแทรก order นี้

DuckDB เขียนแต่ละ column แยกกันและบีบอัดข้อมูล การหาผลรวมของ amount_cents จาก 5 ล้านแถวจะอ่านเฉพาะ column amount_cents ข้ามทุก byte อื่นในไฟล์ และคำนวณผลรวมด้วยโค้ดแบบ vectorised กับชุดค่าข้อมูล การอ่าน column อื่นจาก disk ไม่จำเป็น นี่คือสาเหตุที่ทำให้ทำงานได้เร็ว

ตอนนี้ให้ engine แต่ละตัวทำงานของอีกตัวหนึ่ง การหาผลรวมของ column ใน SQLite ต้องไล่อ่านทุกแถวและดึงทั้งแถวออกจาก page เพื่อเข้าถึง field เดียว จึงอ่านข้อมูลจาก disk มากกว่าที่จำเป็นมาก ส่วนการแทรก order หนึ่งรายการใน DuckDB ต้องเข้าถึง storage ของทุก column เพื่อเก็บค่าเดียว และต้องใช้ write lock กับไฟล์ database ทั้งหมดเพื่อดำเนินการดังกล่าว ไม่มี engine ใดทำงานผิดพลาด แต่ละตัวกำลังตอบคำถามที่ไม่ได้รับการออกแบบมาเพื่อรองรับ

จุดที่ SQLite เหมาะสม: สถานะแอปพลิเคชันแบบมีธุรกรรม

เลือกใช้ SQLite เมื่อการเขียนข้อมูลมีขนาดเล็ก เกิดขึ้นบ่อย และต้องไม่สูญหาย เช่น เซสชัน รายการสั่งซื้อ แถวข้อมูลในคิว การตั้งค่า และข้อมูลอื่นใดที่คำขอเว็บสร้างขึ้น

sudo apt update && sudo apt install -y sqlite3
sudo install -d -o "$USER" -g "$USER" /srv/app

สร้างตารางและเปิดใช้การบันทึกแบบ write-ahead logging ในขั้นตอนเดียวกัน

sqlite3 /srv/app/app.db <<'SQL'
PRAGMA journal_mode = WAL;
CREATE TABLE IF NOT EXISTS orders (
  id           INTEGER PRIMARY KEY,
  customer     TEXT    NOT NULL,
  placed_at    TEXT    NOT NULL,
  amount_cents INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS orders_customer ON orders (customer);
INSERT INTO orders (customer, placed_at, amount_cents)
  VALUES ('ana', '2026-07-30T09:14:00Z', 4200);
SQL

บรรทัดแรกของผลลัพธ์คือ wal นั่นคือ PRAGMA ที่รายงานโหมดซึ่งระบบเปลี่ยนไปใช้ และเป็นการตั้งค่าที่มีประโยชน์มากที่สุดบนเซิร์ฟเวอร์ ในโหมด rollback journal ที่เป็นค่าเริ่มต้น การเขียนข้อมูลจะบล็อกการอ่านทุกครั้ง ในโหมด WAL ตัวอ่านยังคงอ่านสถานะที่ commit ล่าสุดได้ ขณะที่ตัวเขียนเพิ่มข้อมูลต่อท้าย ดังนั้นรายงานที่ทำงานช้าจะไม่ทำให้คำขอเว็บที่รออยู่หยุดชะงักอีกต่อไป

ตรวจสอบว่าแถวข้อมูลถูกส่งกลับมา:

sqlite3 /srv/app/app.db "SELECT * FROM orders WHERE customer = 'ana';"

คุณจะได้รับ 1|ana|2026-07-30T09:14:00Z|4200 ไฟล์เพิ่มเติมอีก 2 ไฟล์ปรากฏถัดจากฐานข้อมูล คือ app.db-wal และ app.db-shm และทั้งสองไฟล์เป็นส่วนหนึ่งของฐานข้อมูล การคัดลอกเฉพาะ app.db ขณะที่แอปพลิเคชันกำลังทำงาน จะทำให้ได้ข้อมูลสำรองที่ไม่สมบูรณ์ ซึ่งจะกล่าวถึงเพิ่มเติมด้านล่าง

SQLite ยังคงอนุญาตให้มีตัวเขียนได้ครั้งละ 1 ตัวเท่านั้น ข้อจำกัดนี้เป็นการล็อก ไม่ใช่คิว ดังนั้นตัวเขียนตัวที่ 2 ซึ่งรอนานเกินไปจะล้มเหลวด้วย database is locked แทนที่จะบล็อกตลอดไป เพิ่มเวลารอด้วย PRAGMA busy_timeout = 5000; ในทุกการเชื่อมต่อที่แอปพลิเคชันเปิดขึ้นมา การรอ 5 วินาทีช่วยขจัดข้อผิดพลาดส่วนใหญ่บนภาระงานเว็บทั่วไปได้

จุดเด่นของ DuckDB: การวิเคราะห์ไฟล์ที่มีอยู่แล้ว

เลือกใช้ DuckDB เมื่อคำถามเริ่มต้นด้วย “มีจำนวนเท่าใด”, “มีปริมาณเท่าใด” หรือ “10 อันดับแรกคือรายการใด” และอินพุตเป็นไฟล์ CSV หรือ Parquet จำนวนมาก ติดตั้งไคลเอ็นต์บรรทัดคำสั่ง โดยเวอร์ชัน 1.5.5 ณ เดือน July 2026:

curl https://install.duckdb.org | sh

สคริปต์จะติดตั้งไบนารีไว้ที่ ~/.duckdb/cli/latest/duckdb และแสดงบรรทัดคำสั่งสำหรับเพิ่มไบนารีดังกล่าวลงใน PATH ของคุณ ยืนยันว่าโปรแกรมทำงานได้:

~/.duckdb/cli/latest/duckdb :memory: "SELECT version();"

สร้างไฟล์ที่เหมาะสำหรับการสืบค้น ไฟล์นี้จะเขียนข้อมูลคำสั่งซื้อจำนวนห้าล้านแถวลงใน Parquet และบีบอัดด้วย zstd:

mkdir -p /srv/data
duckdb :memory: "
COPY (
  SELECT i                                                     AS id,
         'cust_' || (i % 5000)                                 AS customer,
         TIMESTAMP '2026-01-01 00:00:00' + INTERVAL (i) MINUTE AS placed_at,
         (i * 37) % 20000                                      AS amount_cents
  FROM range(5000000) t(i)
) TO '/srv/data/orders.parquet' (FORMAT parquet, COMPRESSION zstd);"

จากนั้นตั้งคำถามเชิงวิเคราะห์ เปิด shell เปิดใช้ตัวจับเวลา และสืบค้นไฟล์โดยตรงโดยไม่ต้องนำเข้าข้อมูล:

.timer on
SELECT customer,
       count(*)                AS orders,
       sum(amount_cents)/100.0 AS revenue
FROM '/srv/data/orders.parquet'
GROUP BY customer
ORDER BY revenue DESC
LIMIT 5;

อ่านค่าของคุณเองจาก .timer แทนการเชื่อถือค่าที่เผยแพร่ไว้ เนื่องจากผลลัพธ์ขึ้นอยู่กับดิสก์และจำนวน core ของคุณ สิ่งสำคัญคือรูปแบบการทำงาน ไม่มี CREATE TABLE ไม่มี INSERT และไม่มีขั้นตอน load: DuckDB อ่าน footer ของ Parquet คำนวณว่า query ต้องใช้ column chunk ใด และอ่านเฉพาะข้อมูลเหล่านั้น ไดเรกทอรีทั้งชุดทำงานในลักษณะเดียวกันได้ด้วย glob คือ FROM '/srv/data/orders-*.parquet' ซึ่งทำให้ไฟล์ส่งออกประจำวันตลอดหนึ่งเดือนกลายเป็น query เดียว

ความเร็วของดิสก์เป็นข้อจำกัดพื้นฐานของกระบวนการทั้งหมดนี้ และการสแกนคอลัมน์เป็นการอ่านข้อมูลตามลำดับเป็นเวลานาน ดังนั้นความแตกต่างระหว่าง NVMe และพื้นที่จัดเก็บ SATA รุ่นเก่าบน VPS จึงเห็นได้ชัดเจนกว่าการใช้งานภายใต้การอ่านแบบสุ่มขนาดเล็กของ SQLite

การอ่านฐานข้อมูล SQLite จาก DuckDB

เอนจินทั้งสองทำงานร่วมกันผ่านส่วนขยาย sqlite ของ DuckDB แนบฐานข้อมูลของแอปพลิเคชันแบบอ่านอย่างเดียว เพื่อให้คำสั่งวิเคราะห์ไม่สามารถเขียนลงในสถานะที่ใช้งานอยู่ได้:

INSTALL sqlite;
LOAD sqlite;
ATTACH '/srv/app/app.db' AS app (TYPE sqlite, READ_ONLY);
SELECT customer, count(*) AS orders
FROM app.orders
GROUP BY customer
ORDER BY orders DESC;

คำสั่งนี้อ่านแถวจากไฟล์ SQLite ณ เวลาที่เรียกใช้คำสั่ง โดยไม่ทำสำเนา วิธีนี้สะดวก แต่ไม่รวดเร็ว เนื่องจากข้อมูลบนดิสก์ยังจัดเก็บเป็นแถว และ DuckDB ต้องไล่อ่านข้อมูล ใช้วิธีนี้สำหรับการส่งออกข้อมูล ไม่ใช่สำหรับแดชบอร์ดที่โหลดข้อมูลใหม่ทุกสามสิบวินาที:

COPY (SELECT * FROM app.orders)
  TO '/srv/data/orders-2026-07.parquet' (FORMAT parquet, COMPRESSION zstd);

คำสั่งเดียวนี้คือรูปแบบทั้งหมด SQLite เป็นเจ้าของแถวข้อมูลล่าสุดที่กำลังใช้งาน การส่งออกตามกำหนดเวลาจะแปลงช่วงเวลาที่ปิดแล้วเป็น Parquet DuckDB ตอบคำถามที่ครอบคลุมข้อมูลหลายเดือน และฐานข้อมูลของแอปพลิเคชันยังคงมีขนาดเล็ก ทำให้การเขียนข้อมูลทำได้รวดเร็ว

เรียกใช้การส่งออกตามกำหนดเวลาแทนการเรียกใช้ด้วยตนเอง คู่ service และ timer ของ systemd เหมาะกับงานนี้ โดยมี unit หนึ่งสำหรับเรียกใช้ COPY และ timer หนึ่งสำหรับเรียกใช้ unit ดังกล่าวทุกคืน

การทำงานทั้งสองระบบบน VPS เดียวกัน

ส่วนนี้ไม่ต้องใช้ container และไม่ต้องใช้ port ใด ๆ ทั้งสอง engine เป็น library ดังนั้นการติดตั้งจึงประกอบด้วย package และ file path หาก stack ส่วนที่เหลือของคุณทำงานอยู่ภายใต้ Docker Compose บน VPS เดียวกัน อยู่แล้ว ให้ mount data directory เข้าไปใน container ที่ต้องใช้งาน แทนการเพิ่ม database service เนื่องจากไม่มี service ให้เพิ่ม

มีกฎ 2 ข้อที่ช่วยให้การจัดวางนี้ทำงานได้โดยไม่มีปัญหา

กำหนด directory แยกให้แต่ละ engine: /srv/app สำหรับ SQLite file ที่ application เขียนข้อมูล และ /srv/data สำหรับ Parquet files ที่ analytics อ่านข้อมูล หากใช้ directory ร่วมกัน backup job ที่ทำ snapshot ของระบบหนึ่งอาจทำงานชนกับอีกระบบ

อย่าให้ 2 processes ชี้ไปที่ DuckDB database file เดียวกันใน read-write mode มีเพียง 1 process เท่านั้นที่สามารถเปิด DuckDB file เพื่อเขียนข้อมูลได้ และ process ที่สองจะเปิด file ไม่สำเร็จเลย สามารถมี readers หลายตัวได้ หากทุกตัวกำหนด access_mode = 'READ_ONLY' กรณีนี้อาจทำให้ผู้ที่คุ้นเคยกับ SQLite แปลกใจ เพราะ SQLite อนุญาตให้หลาย processes ใช้ file เดียวกันเป็นปกติ หาก analytics ของคุณอ่านเฉพาะ Parquet files ปัญหานี้จะไม่เกิดขึ้น ซึ่งเป็นอีกเหตุผลหนึ่งที่ควรเก็บ durable state ไว้ใน SQLite

ข้อมูลสำรองแตกต่างกัน และความแตกต่างนี้ก่อให้เกิดปัญหา

ฐานข้อมูล SQLite ที่กำลังทำงานประกอบด้วยไฟล์ 3 ไฟล์ การคัดลอกไฟล์เหล่านี้ด้วย cp ระหว่างที่มีการเขียนข้อมูล จะได้ไฟล์ที่เปิดได้แต่มีข้อมูลไม่ถูกต้อง ใช้คำสั่งสำรองข้อมูลของ engine เอง คำสั่งนี้จะสร้าง snapshot ที่สอดคล้องกัน ขณะที่แอปพลิเคชันยังคงเขียนข้อมูลต่อไป:

sqlite3 /srv/app/app.db ".backup '/srv/backup/app-$(date -u +%Y%m%dT%H%M%SZ).db'"
sqlite3 /srv/backup/app-20260730T091400Z.db "PRAGMA integrity_check;"

integrity_check จะแสดง ok เมื่อสำเนาถูกต้อง หากแสดงค่าอื่น ให้ลบ snapshot นั้นแล้วสร้างใหม่

ไฟล์ Parquet จะไม่เปลี่ยนแปลงหลังจากเขียนเสร็จ จึงไม่ต้องจัดการเป็นพิเศษ ให้สำรองข้อมูลทั้งไดเรกทอรี ส่งทั้ง 2 path ออกจาก server ด้วย การสำรองข้อมูลด้วย restic จาก VPS จากนั้น data layer ทั้งหมดจะอยู่ใน 2 ไดเรกทอรีภายในงานสำรองข้อมูลเดียวกัน

โหมดความล้มเหลวและข้อความที่จะแสดงแบบตรงตัว

Error: database is locked จาก SQLite หมายความว่าการเชื่อมต่ออื่นถือ write lock นานกว่าระยะหมดเวลาที่กำหนด ไม่ได้หมายความว่าข้อมูลเสียหาย ให้ตั้งค่า PRAGMA busy_timeout ในการเชื่อมต่อทุกรายการ จากนั้นค้นหา transaction ที่ใช้เวลานาน ซึ่งควรแบ่งเป็นหลาย transaction ที่สั้นกว่า

Error: unable to open database file หลังเปลี่ยนสิทธิ์ โดยทั่วไปหมายความว่า process สามารถเขียนไฟล์ได้ แต่ไม่สามารถเขียนไดเรกทอรีได้ SQLite จะสร้าง app.db-wal และ app.db-shm ไว้ถัดจากฐานข้อมูล ดังนั้นไดเรกทอรีต้องมีสิทธิ์เขียน ไม่ใช่เฉพาะไฟล์ .db

IO Error: Could not set lock on file จาก DuckDB หมายความว่า process ที่สองเปิดฐานข้อมูลนั้นไว้เพื่อเขียนอยู่แล้ว ให้ปิด shell อื่น หรือเปิดของคุณในโหมดอ่านอย่างเดียว

Out of Memory Error จาก DuckDB บน VPS ขนาดเล็ก หมายความว่า query ต้องใช้หน่วยความจำสำหรับการประมวลผลมากกว่าที่มีอยู่ DuckDB จะเขียนข้อมูลชั่วคราวลงดิสก์เมื่อทำได้ ดังนั้นให้จัดพื้นที่สำหรับการเขียนข้อมูลชั่วคราวโดยเปิดไฟล์ฐานข้อมูลบนดิสก์แทน :memory: และจำกัดการใช้หน่วยความจำด้วย SET memory_limit = '2GB'; บนเครื่องที่เรียกใช้ service อื่น ขีดจำกัดนี้จะป้องกันไม่ให้ query เฉพาะกิจดึง RAM ของ application ไปจนหมด

Binder Error: Referenced column "amount" not found เมื่อ query Parquet โดยเกือบทุกครั้งหมายความว่า schema ของไฟล์ไม่ตรงกับที่คุณจำได้ ให้เรียกใช้ DESCRIBE SELECT * FROM '/srv/data/orders.parquet'; แล้วอ่านชื่อคอลัมน์จริงที่แสดงกลับมา

วิธีเลือกใช้งานจริง

ให้พิจารณาว่ารูปแบบการเขียนข้อมูลเป็นอย่างไร การเขียนข้อมูลขนาดเล็กหลายครั้งที่ต้องคงอยู่แม้ไฟฟ้าดับ หมายถึงควรใช้ SQLite ให้พิจารณาด้วยว่ารูปแบบการอ่านข้อมูลเป็นอย่างไร การสแกนข้อมูลทั้งหมดพร้อมการคำนวณแบบรวมตลอดประวัติข้อมูลที่ยาวนาน หมายถึงควรใช้ DuckDB ระบบจริงส่วนใหญ่มีทั้งสองลักษณะ คำตอบที่เหมาะสมคือให้แต่ละ engine ทำงานในส่วนที่เหมาะกับตนเอง แทนการบังคับให้ engine ใด engine หนึ่งทำหน้าที่แทนอีก engine

สิ่งที่ควรหลีกเลี่ยงคือการย้ายสถานะการทำงานของแอปพลิเคชันไปไว้ใน DuckDB เพียงเพราะรายงานทำงานช้า รายงานทำงานช้าเนื่องจากการจัดวางข้อมูล ดังนั้นวิธีแก้คือการ export ข้อมูล ไม่ใช่การเขียนเส้นทางการเขียนข้อมูลใหม่ทั้งหมด

FAQ

DuckDB สามารถแทนที่ SQLite สำหรับฐานข้อมูลของแอปพลิเคชันได้หรือไม่

ไม่เหมาะกับฐานข้อมูลที่มีการเขียนข้อมูลบ่อย DuckDB จะล็อกไฟล์ฐานข้อมูลทั้งไฟล์เพื่อเขียนข้อมูล อนุญาตให้มี process แบบ read-write ได้ครั้งละ 1 รายการ และได้รับการปรับให้เหมาะกับการเปลี่ยนแปลงข้อมูลจำนวนมาก มากกว่าการแทรกข้อมูลทีละแถว ให้เก็บสถานะธุรกรรมไว้ใน SQLite และให้ DuckDB อ่านข้อมูลด้วย ATTACH '/srv/app/app.db' AS app (TYPE sqlite, READ_ONLY); เมื่อต้องสร้างรายงาน

DuckDB เร็วกว่า SQLite สำหรับการวิเคราะห์ข้อมูลจริงหรือไม่

เร็วกว่าเมื่อสแกนและคำนวณ aggregate บนตารางขนาดใหญ่ เหตุผลอยู่ที่รูปแบบการจัดเก็บข้อมูล ไม่ใช่เทคนิคการปรับแต่ง DuckDB จะอ่านเฉพาะคอลัมน์ที่ query ระบุและประมวลผลค่าเป็นชุด ขณะที่ SQLite ต้องอ่านทั้งแถวเพื่อเข้าถึงฟิลด์เดียว สำหรับการดึงข้อมูล 1 แถวด้วย primary key ผลจะกลับกัน เพราะ SQLite เข้าถึง 2 pages ส่วน DuckDB เข้าถึงพื้นที่จัดเก็บของทุกคอลัมน์

ต้องใช้ RAM จำนวนมากเพื่อเรียกใช้ DuckDB บน VPS หรือไม่

ไม่จำเป็น แต่ควรกำหนดขีดจำกัดและจัดเตรียม disk ให้เพียงพอ เปิดไฟล์ฐานข้อมูลแทน :memory: เพื่อให้ DuckDB สามารถเขียนผลลัพธ์ระหว่างการประมวลผลลง disk ได้ จากนั้นตั้งค่า SET memory_limit = '2GB'; เป็นค่าที่ VPS สามารถจัดสรรได้ หากไม่กำหนดขีดจำกัด GROUP BY ขนาดใหญ่อาจทำให้เกิด Out of Memory Error หรือทำให้ service อื่นไม่มี RAM ใช้งาน

จะนำข้อมูล SQLite เข้า Parquet ได้อย่างไร

ให้ attach ไฟล์ SQLite จาก DuckDB แล้วใช้ COPY (SELECT * FROM app.orders) TO '/srv/data/orders.parquet' (FORMAT parquet, COMPRESSION zstd); เพื่อคัดลอกผลลัพธ์ของ query ออกมาโดยตรง เรียกใช้ตาม schedule สำหรับช่วงเวลาที่ปิดแล้ว เช่น rows ของเดือนที่แล้ว และเก็บ rows ล่าสุดไว้ใน SQLite ซึ่งแอปพลิเคชันยังคงเขียนข้อมูลลงไป

ควรสำรองข้อมูลรายการใด และสำรองอย่างไร

ควรสำรองข้อมูลทั้ง 2 รายการด้วยวิธีที่แตกต่างกัน สร้าง snapshots ของ SQLite ด้วย sqlite3 app.db ".backup '/srv/backup/app.db'" แทน cp เพราะฐานข้อมูลที่กำลังทำงานอยู่เป็น -wal และเป็นไฟล์ -shm ด้วย การคัดลอกไฟล์โดยตรงอาจได้ไฟล์ที่ไม่สมบูรณ์ ไฟล์ Parquet จะไม่เปลี่ยนแปลงหลังจากเขียนเสร็จ ดังนั้นการคัดลอก directory ก็เพียงพอ

#duckdb#sqlite#database#analytics#parquet#self-hosting