สิ่งที่ได้เรียนรู้จากการรันร้านค้าออนไลน์จริงด้วย SQLite
(ultrathink.art)ultrathink.art คือร้านค้าอีคอมเมิร์ซที่ดำเนินงานโดย AI agent อย่างอัตโนมัติ ตั้งแต่การออกแบบสินค้า การจัดการคำสั่งซื้อ ไปจนถึงการเขียนบล็อก ล้วนเป็นหน้าที่ของ AI ทั้งหมด บทความนี้รวบรวมประสบการณ์จากการนำร้านค้านั้นไปรันด้วย SQLite ในสภาพแวดล้อม production ที่ประมวลผลการชำระเงินผ่าน Stripe จริงด้วย
โครงสร้าง: 4 ไฟล์, 1 โวลุม
ในสภาพแวดล้อม production มีการใช้งานฐานข้อมูล SQLite ทั้งหมด 4 ตัว ได้แก่ primary (คำสั่งซื้อ·สินค้า·ผู้ใช้), cache (Rails cache), queue (แบ็กกราวนด์จ็อบ) และ cable (Action Cable) โดยทั้งหมดถูกเก็บไว้ใน Docker volume เดียว
Rails 8 ทำให้ SQLite กลายเป็นตัวเลือกอันดับหนึ่ง และในทางปฏิบัติก็ได้ประโยชน์อย่างการ deploy ที่เรียบง่ายขึ้น ไม่ต้องจัดการ connection pool และไม่ต้องมี DB server แยกต่างหาก
หลักการที่โหมด WAL ทำให้เกิด concurrency ได้
โหมด journal เริ่มต้นของ SQLite จะล็อกฐานข้อมูลทั้งก้อนเมื่อมีการเขียน จึงไม่เหมาะกับเว็บแอปที่มีคำขอพร้อมกันจำนวนมาก ในโหมด WAL (Write-Ahead Logging) การเขียนจะถูกต่อเพิ่มลงในไฟล์ -wal แยกต่างหาก ขณะที่การอ่านยังคงใช้ไฟล์หลักต่อไป ทำให้การอ่านหลายรายการและการเขียนหนึ่งรายการเกิดขึ้นพร้อมกันได้ Rails 8 เปิดใช้โหมด WAL ให้กับ SQLite เป็นค่าเริ่มต้น
อุบัติเหตุ: คำสั่งซื้อ 2 รายการหายไป
วันที่ 4 กุมภาพันธ์ มีการ push commit ไปที่ main 11 ครั้งภายใน 2 ชั่วโมง ทุกครั้งที่ push จะมีการทำ blue-green deploy ของ Kamal ส่งผลให้เกิดช่วงทับซ้อนที่ container เดิมและ container ใหม่เปิดไฟล์ WAL เดียวกันพร้อมกัน เมื่อการ deploy ทั้ง 11 ครั้งซ้อนทับกัน จึงเกิดสถานการณ์ที่ container A กำลัง draining ขณะที่ B เริ่มทำงาน และก่อนที่ B จะพร้อมเต็มที่ การ deploy ของ C ก็เริ่มขึ้นแล้ว
คำสั่งซื้อหมายเลข 16 และ 17 ชำระเงินผ่าน Stripe สำเร็จ และเงินก็ถูกหักจากบัญชีลูกค้าแล้ว แต่ไม่มี record ถูกบันทึกไว้ใน DB เมื่อตรวจสอบด้วย sqlite_sequence พบว่าตัวนับ auto-increment ชี้ไปที่ 17 แต่มีแถวจริงอยู่เพียง 15 แถวเท่านั้น
วิธีแก้: ทำให้การ deploy ช้าลง
วิธีแก้ไม่ใช่เรื่องเทคนิค แต่เป็นเรื่องกระบวนการ โดยกำหนดให้รวมการเปลี่ยนแปลงที่เกี่ยวข้องเข้าด้วยกันก่อน deploy และหลีกเลี่ยงการ push ถี่ ๆ ติดต่อกัน แล้วระบุเป็นกฎไว้ในไฟล์ governance (CLAUDE.md) ที่ AI agent ต้องปฏิบัติตาม
นี่ไม่ใช่ปัญหาของ SQLite แต่เป็นปัญหาของ deployment pipeline PostgreSQL เชื่อมต่อผ่าน TCP socket ดังนั้นแม้จะเป็น container ใหม่ก็ยังเชื่อมต่อไปยัง DB server ตัวเดิม และให้ DB engine จัดการลำดับการเขียนได้ ส่วน SQLite พึ่งพา file-system lock บน Docker volume ที่ใช้ร่วมกัน ซึ่งจะพังได้เมื่อมี container ซ้อนทับกัน
sqlite_sequence: ใช้เป็นเครื่องมือ forensic
ตาราง sqlite_sequence คือเครื่องมือดีบักที่ถูกประเมินค่าต่ำเกินไปมากที่สุดอย่างหนึ่งใน SQLite มันจะจดจำค่าสูงสุดที่เคยถูกจัดสรรให้กับ auto-increment เอาไว้ แม้ว่าแถวนั้นจะถูกลบไปในภายหลังก็ตาม หากจำนวนแถวปัจจุบันกับค่า sequence ห่างกันผิดคาด นั่นคือสัญญาณว่ามีบางอย่างลบแถวผิดพลาดไป
กับดักที่ไม่มีใครบอกคุณ
ILIKE ที่นักพัฒนา PostgreSQL ใช้กันจนเคยชินจะทำให้เกิด syntax error ใน SQLite ต้องใช้ LOWER(name) LIKE แทน json_extract หากค่าถูกเก็บเป็นตัวเลข มันจะคืนค่าเป็นจำนวนเต็มและล้มเหลวแบบเงียบ ๆ เมื่อเปรียบเทียบกับสตริง ส่วน kamal app exec จะสร้าง container ใหม่ทุกครั้ง และถ้ารันพร้อมกันสองครั้งบนเซิร์ฟเวอร์ RAM 2GB ตัว OOM killer จะฆ่า web process
ถ้าเลือกใหม่อีกครั้ง จะยังใช้ SQLite หรือไม่?
ใช่ ถ้าเป็น single server ที่มีภาระการเขียนในระดับพอเหมาะ SQLite จะช่วยตัดความซับซ้อนของโครงสร้างพื้นฐานออกไปได้ทั้งหมด แม้แต่การสำรองข้อมูลก็ใช้เพียงคำสั่ง sqlite3 .backup คำสั่งเดียวก็พอแล้ว (และจัดการโหมด WAL กับการเขียนพร้อมกันได้อย่างปลอดภัย) เมื่อถึงวันที่ต้องการ horizontal scaling หรือ concurrency แบบ multi-writer ที่แท้จริง ค่อย migrate ไป PostgreSQL ตอนนั้นก็ยังไม่สาย Rails ช่วยให้การเปลี่ยนผ่านนั้นง่ายขึ้นมาก
ต้นฉบับ: ultrathink.art Blog, 2026.04.03
4 ความคิดเห็น
ต่อให้จะมีปัญหาเรื่องความซับซ้อนของอินฟราก็ตาม แต่ในงานอย่างร้านค้าออนไลน์ที่ต้องการการเขียนพร้อมกันจำนวนมาก การไม่ใช้ sqlite ตั้งแต่แรกน่าจะเป็นตัวเลือกที่ดีกว่าไม่ใช่หรือครับ?
ยิ่งถ้าจัดระบบด้วย Docker อยู่แล้ว ความซับซ้อนของอินฟราที่ต้องใช้ PostgreSQL ก็คงไม่ได้สูงมากนักด้วย
ผมรู้สึกว่านี่เป็นความเห็นในทำนองว่าพอทำด้วย Rails แล้ว ecosystem มันปูทางให้ไปใช้ sqlite เลยถูกชักนำให้คิดว่ามันเป็นทางเลือกที่ดี
เกิดข้อผิดพลาดร้ายแรงอย่างการตกหล่นของคำสั่งซื้อขึ้นแล้ว และถึงจะมีปัญหาเรื่องการเขียนพร้อมกันก็ตาม สำหรับงานอย่างร้านค้าออนไลน์ที่ต้องการการเขียนพร้อมกันจำนวนมาก การไม่ใช้ sqlite ตั้งแต่แรกก็น่าจะเป็นตัวเลือกที่ดีกว่าไม่ใช่หรือครับ?
เพราะทำด้วย Rails แล้ว ecosystem มันปูทางให้ไปใช้ sqlite เลยทำให้ผมรู้สึกว่าถูกชักนำให้คิดว่ามันเป็นทางเลือกที่ดี
มีข้อผิดพลาดร้ายแรงอย่างการตกหล่นของคำสั่งซื้อเกิดขึ้นแล้ว และการแก้ปัญหานี้จากรากจริง ๆ ก็คือการใช้ฐานข้อมูลที่รองรับการเขียนพร้อมกันอย่าง pg
แต่เพราะชอบ sqlite ในเชิงเทคนิค เลยยืนกรานว่าจะใช้มันต่อไป แบบนี้สำหรับผมฟังดูเป็นคำพูดที่ทำให้ความน่าเชื่อถือในฐานะวิศวกรลดลง
มันให้ความรู้สึกเหมือนเวอร์ชันตรงข้ามของการพัฒนาแบบ resume-driven ที่ทั้ง ๆ ที่ไม่จำเป็นก็ยก k8s ขึ้นมา ทำ HPA ที่มี replica แค่ 1 แล้วเปลี่ยน monolith ที่ใช้งานได้ดีอยู่แล้วให้เป็น MSA
ปัญหาเกิดจากการตั้งค่า volume ผิดตอนรันด้วยคอนเทนเนอร์ ไม่ใช่เพราะเขียนพร้อมกันไม่ได้จนกลายเป็นปัญหา ถ้าลองอ่านบทความใหม่จะเห็นว่าเขาบอกไว้อยู่ว่าเรื่องเขียนพร้อมกันนั้นครอบคลุมได้เพียงพอด้วย busy timeout ส่วนการตั้งค่า volume ก็น่าจะแก้ได้ด้วย
ipc=host
ท้ายที่สุดแล้ว
postgresต้องมีการดูแลระบบในการใช้งานจริง แต่sqliteแค่แจกจ่ายแอปไบนารีไปที่ไหนก็ได้ก็พอพอมีประสบการณ์การใช้งานจริงและความล้มเหลวมากมายสั่งสมมากขึ้น
sqliteก็เลยเริ่มได้รับความนิยมและเรื่องการเขียนพร้อมกันก็มีระบุไว้อย่างชัดเจนในบทความแล้วว่าไม่ได้เป็นปัญหาเลย
จริง ๆ แล้วแค่ตั้งค่าหลาย ๆ อย่างก็พอ แต่สุดท้ายก็ต้องอาศัยประสบการณ์อยู่ดี ฮ่า ๆ ยังไงก็ตาม ผมชอบบทความแบบนี้นะ
ใช่เลย เพราะอย่างนั้นแหละ ถ้ามีบทความแบบนี้ขึ้นมาบ้างเป็นครั้งคราวก็คงดีนะ