การเปรียบเทียบประสิทธิภาพแบบครอบคลุมของไดรเวอร์ SQLite เก้าตัวสำหรับภาษาโปรแกรม Go ได้เผยให้เห็นลักษณะประสิทธิภาพที่ไม่คาดคิด โดยเฉพาะการเน้นย้ำถึงแนวทางที่ไม่เป็นทางการซึ่งใช้การสื่อสารแบบ stdin/stdout กับ subprocess การประเมินนี้ได้ทดสอบไดรเวอร์ต่างๆ ในหลายสถานการณ์ ตั้งแต่การดำเนินงานแบบกลุ่มง่ายๆ ไปจนถึงภาระงานแบบ concurrent ที่ซับซ้อน
ไดรเวอร์ SQLite ที่ทำการทดสอบ:
- bvinc: ใช้ CGO เป็นฐาน ไม่รองรับ database/sql
- cznic: ใช้ CGO เป็นฐาน ไม่รองรับ database/sql
- eaton: ใช้ CGO เป็นฐาน ไม่รองรับ database/sql
- gwenn: Pure Go รองรับ database/sql
- mattn: ใช้ CGO เป็นฐาน รองรับ database/sql (มาตรฐานหลัก)
- modernc: Pure Go รองรับ database/sql (โค้ด C ที่แปลงเป็น Go)
- numine: Pure Go รองรับ database/sql (ใช้ WASM เป็นฐาน)
- sqinn: Pure Go ไม่รองรับ database/sql (subprocess แบบ stdin/stdout)
- zombie: Pure Go ไม่รองรับ database/sql (เขียนใหม่จาก modernc)
โซลูชัน Pure Go ท้าทายแนวทาง CGO แบบดั้งเดิม
ผลการเปรียบเทียบแสดงให้เห็นว่าการใช้งานแบบ pure Go เช่น modernc.org/sqlite ตอนนี้เป็นทางเลือกที่เป็นไปได้แทนโซลูชันแบบ CGO ดั้งเดิม การพัฒนานี้แก้ไขจุดเจ็บปวดที่มีมานานสำหรับนักพัฒนา Go ที่ต้องการ cross-compile แอปพลิเคชันสำหรับแพลตฟอร์มต่างๆ ความต้องการ CGO มักจะทำให้กระบวนการ build ซับซ้อน โดยเฉพาะเมื่อกำหนดเป้าหมายเป็นระบบปฏิบัติการหรือสถาปัตยกรรมที่แตกต่างกัน
นักพัฒนาหลายคนในชุมชนได้รายงานความสำเร็จกับไดรเวอร์ modernc ในสภาพแวดล้อมการใช้งานจริง โดยชื่นชมความน่าเชื่อถือและความง่ายในการปรับใช้ ไดรเวอร์ทำงานโดยการ transpile โค้ด C ของ SQLite ไปเป็น Go โดยตรง ทำให้ไม่จำเป็นต้องใช้ CGO ในขณะที่ยังคงความเข้ากันได้กับอินเทอร์เฟซ database/sql มาตรฐาน
CGO: ฟีเจอร์ใน Go ที่อนุญาตให้โปรแกรม Go เรียกใช้โค้ด C แต่ต้องการคอมไพเลอร์ C ในระหว่างกระบวนการ build
ผู้ชนะที่ไม่คาดคิด: แนวทาง stdin/stdout
บางทีผลลัพธ์ที่น่าประหลาดใจที่สุดมาจาก sqinn ซึ่งเป็นไลบรารีที่สื่อสารกับ SQLite ผ่าน subprocess แยกต่างหากโดยใช้ stdin และ stdout streams แนวทางนี้มีประสิทธิภาพเหนือกว่าการใช้งานแบบดั้งเดิมหลายตัวอย่างสม่ำเสมอในเบนช์มาร์กต่างๆ แม้จะมีค่าใช้จ่ายที่เห็นได้ชัดของการสื่อสารระหว่างกระบวนการ
ข้อได้เปรียบด้านประสิทธิภาพน่าจะเกิดจากวิธีที่ระบบปฏิบัติการจัดการ stream buffers เมื่อเปรียบเทียบกับค่าใช้จ่ายการจัดสรรหน่วยความจำในการเรียก API โดยตรง เมื่อใช้ stdin/stdout ระบบจะจัดการ send และ receive buffers โดยอัตโนมัติ ซึ่งอาจลดรอบการจัดสรรและยกเลิกการจัดสรรที่เกิดขึ้นกับ CGO bindings แบบดั้งเดิม
อย่างไรก็ตาม แนวทางนี้มาพร้อมกับการแลกเปลี่ยน แอปพลิเคชันที่ใช้ sqinn ต้องการการแจกจ่าย SQLite binary แยกต่างหากควบคู่ไปกับ Go executable ซึ่งทำลายข้อได้เปรียบของ Go ในการแจกจ่ายแบบ single-binary ในระดับหนึ่ง นอกจากนี้ การเข้าถึงฐานข้อมูลแบบ concurrent จะต้องจัดการ subprocesses หลายตัว
ประสิทธิภาพในโลกแห่งความจริงแตกต่างกันตามกรณีการใช้งาน
การเปรียบเทียบได้ทดสอบหกสถานการณ์ที่แตกต่างกัน ตั้งแต่การ bulk inserts ง่ายๆ ไปจนถึงการดำเนินงานแบบ concurrent ที่ซับซ้อน ผลลัพธ์แตกต่างกันอย่างมีนัยสำคัญขึ้นอยู่กับประเภทของภาระงาน โดยไม่มีไดรเวอร์ตัวเดียวที่ครองทุกหมวดหมู่ ความแตกต่างนี้เน้นย้ำถึงความสำคัญของการทดสอบไดรเวอร์กับความต้องการของแอปพลิเคชันเฉพาะแทนที่จะพึ่งพาเพียงเบนช์มาร์กทั่วไป
สำหรับนักพัฒนาที่ต้องรับมือกับความท้าทายของ cross-compilation โซลูชัน pure Go เสนอทางเลือกที่น่าสนใจ สมาชิกชุมชนคนหนึ่งได้กล่าวถึงความสำเร็จของพวกเขาในการเปลี่ยนจากไลบรารีแบบ CGO ไปสู่การใช้งานแบบ pure Go โดยเฉพาะเพื่อแก้ไขปัญหา FreeBSD cross-compilation บน macOS
หมวดหมู่ของ Benchmark:
- Simple: การ insert ผู้ใช้ 1 ล้านคนในธุรกรรมเดียว + query ทั้งหมด
- Real: ผู้ใช้ 100 คน บทความคนละ 20 บทความ ความคิดเห็น 20 รายการต่อบทความ (ธุรกรรมแยกกัน)
- Complex: ผู้ใช้ 200 คน บทความ 20,000 บทความ ความคิดเห็น 400,000 รายการ พร้อม query แบบ JOIN ที่ซับซ้อน
- Many: ผู้ใช้ N คนทำการ insert + query ซ้ำ 1,000 ครั้ง (จำลองการอ่านข้อมูลหนัก)
- Large: ผู้ใช้ 10,000 คนพร้อมเนื้อหา N bytes (จำลองฐานข้อมูลขนาดใหญ่)
- Concurrent: ผู้ใช้ 1 ล้านคน + N goroutines ทำการ query (จำลองการอ่านข้อมูลแบบ concurrent)
การยอมรับ SQLite ในการใช้งานจริงที่เพิ่มขึ้น
นอกเหนือจากการอภิปรายเกี่ยวกับประสิทธิภาพทางเทคนิคแล้ว การเปรียบเทียบนี้ได้จุดประกายการสนทนาที่กว้างขึ้นเกี่ยวกับบทบาทของ SQLite ในสภาพแวดล้อมการใช้งานจริง นักพัฒนาหลายคนรายงานการปรับใช้ที่ประสบความสำเร็จโดยใช้ SQLite สำหรับแอปพลิเคชันที่ในอดีตอาจใช้ PostgreSQL หรือ MySQL
SQLite ถูกประเมินต่ำไป ฉันเห็นด้วยว่าในกรณีเฉพาะอาจไม่ใช่ตัวเลือกที่ดีที่สุด แต่บ่อยครั้งที่ฉันเห็นว่า SQLite เป็นฐานข้อมูลที่เพียงพอมากกว่า การใช้ Postgres หรือ MySQL เพื่อความเป็น production grade ไม่เคยเป็นความคิดที่ดี
กุญแจสู่การปรับใช้ SQLite ที่ประสบความสำเร็จมักเกี่ยวข้องกับการเปิดใช้งานโหมด Write-Ahead Logging ( WAL ) ซึ่งอนุญาตให้ readers และ writers ทำงานพร้อมกัน ฟีเจอร์นี้แก้ไขข้อกังวลดั้งเดิมเกี่ยวกับพฤติกรรมการล็อกของ SQLite ในสภาพแวดล้อมแบบ multi-process
Write-Ahead Logging (WAL): ฟีเจอร์ของ SQLite ที่ปรับปรุงการทำงานพร้อมกันโดยอนุญาตให้ readers เข้าถึงฐานข้อมูลในขณะที่ writer กำลังทำการเปลี่ยนแปลง
สภาพแวดล้อมการทดสอบ:
- ระบบปฏิบัติการ: Debian GNU/Linux 12.11 (amd64)
- ซีพียู: Intel Core i7-1165G7 @ 2.80GHz (8 คอร์)
- แรม: 32GB
- หน่วยเก็บข้อมูล: 1TB NVMe SSD
- เวอร์ชัน Go : 1.24.5
- วันที่ทดสอบ: 17 สิงหาคม 2025
บทสรุป
ผลการเปรียบเทียบแสดงให้เห็นว่าภูมิทัศน์ไดรเวอร์ Go SQLite ได้พัฒนาไปอย่างมีนัยสำคัญ โดยโซลูชัน pure Go ตอนนี้เสนอทางเลือกที่เป็นไปได้แทนแนวทางแบบ CGO ดั้งเดิม ในขณะที่แนวทาง stdin/stdout แสดงตัวเลขประสิทธิภาพที่น่าประทับใจ การเลือกไดรเวอร์ควรขึ้นอยู่กับความต้องการของแอปพลิเคชันเฉพาะ ข้อจำกัดในการปรับใช้ และลักษณะประสิทธิภาพที่สำคัญที่สุดสำหรับแต่ละกรณีการใช้งาน
สำหรับนักพัฒนาที่ให้ความสำคัญกับความง่ายในการปรับใช้และ cross-compilation ไดรเวอร์ pure Go เช่น modernc.org/sqlite เป็นตัวเลือกที่น่าสนใจ ผู้ที่แสวงหาประสิทธิภาพสูงสุดอาจพิจารณาแนวทาง sqinn ที่ไม่เป็นทางการ แม้จะมีความซับซ้อนในการปรับใช้ ไดรเวอร์ mattn/go-sqlite3 แบบดั้งเดิมยังคงเป็นตัวเลือกที่มั่นคงสำหรับแอปพลิเคชันที่ยอมรับการพึ่งพา CGO ได้
อ้างอิง: Benchmarks for Golang SQLite Drivers