Hình dung tình huống: cần chọn database cho một internal tool nhỏ, dashboard hiển thị trạng thái deploy cho team. Traffic chỉ vài chục người dùng, data vài trăm MB, deploy trên một con VM duy nhất. Senior trong team nói “dùng SQLite đi, đỡ phải chạy thêm container Postgres”. Nghe mà ngờ ngợ, SQLite chỉ để học và làm prototype chứ ai mà dùng production?
Hoá ra cái “chỉ để học” đó đang chạy trong trình duyệt bạn đang đọc bài này, trong hàng tỷ thiết bị Android/iOS, trong phần mềm điều khiển bay của Boeing, trong Chromium, trong Signal. Và ngày càng nhiều team backend dùng nó cho service thật, không phải vì thiếu tiền mua Postgres, mà vì với đúng workload, SQLite giảm đáng kể complexity vận hành.
Bài này không cổ xuý bạn thay PostgreSQL bằng SQLite. Mục tiêu là giúp bạn hiểu SQLite phù hợp khi nào, cần bật gì để chạy production nghiêm túc, giới hạn thực sự nằm ở đâu, và khi nào nên chuyển sang thứ khác.
SQLite không phải server, đó là một thư viện
Điểm khác biệt cơ bản nhất giữa SQLite và PostgreSQL/MySQL mà nhiều người bỏ qua: SQLite không phải một process chạy riêng chờ nhận kết nối. Nó là một thư viện C mà code ứng dụng của bạn link trực tiếp vào. Toàn bộ database nằm trong một file duy nhất trên disk, file đó chứa header, page data, index, schema, mọi thứ. Không có daemon, không có port, không có connection string dạng host:port.
Kiến trúc này tạo ra một số hệ quả rất quan trọng mà bạn cần nắm trước khi quyết định dùng hay không dùng.
Về latency, mỗi query của SQLite cực thấp vì không có network hop, không có IPC giữa app process và DB process. Khi app gọi sqlite3_exec(), nó chạy ngay trong cùng process space, nhanh hơn đáng kể so với gửi query qua TCP socket rồi chờ response. Với workload read-heavy trên cùng máy, SQLite có thể nhanh hơn Postgres cho những query đơn giản.
Nhưng đổi lại, scale horizontal theo nghĩa truyền thống, nhiều server client cùng truy cập một database, không tồn tại. SQLite lock ở mức file trên filesystem của máy đó. Bạn không thể có hai máy cùng ghi vào một file SQLite qua NFS hay network mount, hoặc đúng hơn là bạn có thể, nhưng sẽ corrupt data. Đây là điểm mà nhiều người mới nghe “SQLite production” hay nhầm lẫn.
Backup và replication cũng phải tự lo bằng công cụ ngoài, SQLite không có replication stream sẵn như PostgreSQL WAL shipping hay MySQL binlog replication. Đây vừa là nhược điểm (phải setup thêm) vừa là ưu điểm (ít moving parts khi bạn không cần).
Khi nào SQLite là lựa chọn tốt
Ứng dụng embedded và client-side
Đây là vương quốc truyền thống của SQLite và không ai tranh cãi. Mọi ứng dụng mobile nghiêm túc đều dùng SQLite dưới nền, Room trên Android, Core Data trên iOS đều wrap SQLite. Desktop app cũng vậy: Chrome dùng SQLite cho bookmarks, history, cookies. Firefox, Slack (cache local), Signal (message store) đều dùng. Game lưu save state, config, leaderboard local, SQLite cả.
Lý do đơn giản: bạn cần SQL đầy đủ trên thiết bị của user mà không muốn chạy thêm một database server. SQLite cho bạn chính xác điều đó, một file, zero config, zero admin.
Workload đọc nhiều, ghi ít trên một máy
Đây là sweet spot mà ngày càng nhiều team backend khai thác. Nếu service của bạn đọc nhiều hơn ghi đáng kể, dashboard internal, static site builder cần index tìm kiếm, cache layer có SQL đầy đủ, read replica cho analytics nhẹ, thì SQLite với WAL mode có thể phục vụ hàng chục nghìn read QPS trên máy hiện đại nhờ mmap và page cache của OS.
Ví dụ: dùng SQLite làm backing store cho một service tra cứu config, vài triệu record, update vài lần mỗi giờ, nhưng đọc liên tục từ nhiều goroutine. Postgres hoàn toàn overkill cho use case này: phải chạy thêm container, setup connection pool, monitor thêm một process. SQLite cho tất cả những gì cần trong một file 200MB.
Service nhỏ, triển khai đơn giản
Blog cá nhân, forum quy mô nhỏ/trung bình, admin tool nội bộ, CLI tool cần persistent state, microservice với data không quá vài trăm GB, đây đều là những nơi SQLite toả sáng. Bạn deploy một binary duy nhất kèm một file database, không cần docker-compose chạy thêm Postgres container, không cần lo connection pool, không cần monitor thêm process.
Một số framework và platform đang đẩy mạnh pattern này. Litestream cho backup streaming liên tục lên S3, biến SQLite từ “mất máy là mất data” thành “point-in-time recovery”. LiteFS của Fly.io cho replication đọc ở edge. rqlite và dqlite wrap Raft consensus quanh SQLite cho HA. Ecosystem này đang phát triển rất nhanh.
Edge computing
Mô hình edge là nơi SQLite phát huy đặc trưng “embedded” một cách hoàn hảo. Mỗi edge node gần user có bản SQLite local, đọc cực nhanh vì không qua mạng. Ghi thì forward về leader node. Fly.io và Turso đang đẩy mạnh mô hình này, deploy SQLite ở 30 region trên thế giới, user đọc từ node gần nhất, latency vài millisecond thay vì vài trăm.
Edge SQLite đang phát triển nhanh, Turso chẳng hạn cho trải nghiệm deploy nhanh, read latency thấp, developer experience mượt.
Phân tích dữ liệu nhẹ
SQLite xử lý file vài GB SQL nhanh đáng ngạc nhiên, import CSV rồi chạy aggregate query trên vài triệu row hoàn thành trong giây. Đối với ad-hoc analysis trên dataset vừa phải, SQLite là tool tuyệt vời vì không cần setup gì. Tuy nhiên, để phân tích thực sự lớn hơn, DuckDB, cùng triết lý embedded nhưng thiết kế cho OLAP, vượt trội hơn nhiều với columnar storage và vectorized execution.
Khi nào không dùng SQLite
Hiểu giới hạn quan trọng không kém hiểu điểm mạnh. Có những tình huống mà dùng SQLite là tự gây khó cho mình, và biết lúc nào cần dừng sẽ giúp bạn tránh được nhiều đau đầu.
Nếu bạn cần nhiều tiến trình trên nhiều máy ghi đồng thời vào cùng một database, đó là lúc cần client-server database như Postgres hay MySQL. SQLite lock ở mức file trên một máy duy nhất. Hai process trên hai máy khác nhau không thể coordinate write qua SQLite thuần.
Nếu workload có write QPS cao liên tục, hàng nghìn write transaction mỗi giây ổn định, thì SQLite sẽ là bottleneck vì chỉ có một writer active tại một thời điểm. Bạn có thể gom batch để tăng throughput, nhưng vẫn có trần. Postgres với nhiều backend process xử lý concurrent write tốt hơn nhiều bậc.
Nếu bạn cần multi-tenant access control phức tạp, nhiều user với quyền khác nhau, audit log, row-level security, thì SQLite không có server-side authentication/authorization. Mọi access control phải xử lý ở application layer.
Nếu yêu cầu replication sẵn có, failover tự động, multi-AZ, tức là HA kiểu enterprise, thì phải tự xây hoặc dùng wrapper như rqlite/dqlite. So với Postgres streaming replication hay managed database (RDS, Cloud SQL) thì effort setup và vận hành có thể ngang nhau hoặc hơn, mà lại thiếu ecosystem tool hỗ trợ.
Và nếu dung lượng data lớn hơn disk một máy, dù SQLite hỗ trợ DB tới 281 TB trên lý thuyết, thì thực tế bạn bị giới hạn bởi IO của một host duy nhất. Sharding trong SQLite không tồn tại như khái niệm native.
Bật các pragma quan trọng
Mặc định của SQLite thiên về tính đúng (correctness) và tương thích ngược, không phải hiệu năng. Nếu bạn chỉ mở SQLite connection mà không set pragma gì thì đang bỏ phí rất nhiều performance và thiếu vài safety net quan trọng. Đây là những pragma nên luôn bật cho mọi project production.
WAL, thay đổi quan trọng nhất
PRAGMA journal_mode = WAL;
Đây có lẽ là pragma quan trọng nhất khi đưa SQLite lên production. Mặc định SQLite dùng rollback journal, khi ghi, nó lock toàn bộ file, không ai đọc được cho đến khi write xong. WAL (Write-Ahead Logging) đảo ngược flow: ghi vào WAL file trước, rồi checkpoint về file chính sau.
Hệ quả thực tế là reader không chặn writer, writer không chặn reader. Nhiều thread có thể đọc song song trong khi một thread đang ghi, đây là điều bắt buộc cho bất kỳ web app nào có concurrent request. Không bật WAL mà chạy web app thì request sẽ queue lên chờ nhau, latency tăng vọt dưới load.
Lưu ý khi dùng WAL: SQLite sinh thêm hai file -wal và -shm bên cạnh file database chính. Khi backup, bạn phải copy cả ba file, thiếu WAL file thì bản backup có thể thiếu data gần nhất. Và vẫn chỉ có một writer tại một thời điểm, WAL không magic thêm concurrent write.
Synchronous mode
PRAGMA synchronous = NORMAL;
Pragma này quyết định SQLite gọi fsync() khi nào. FULL (mặc định) gọi fsync sau mỗi commit, chắc chắn data đã xuống disk, nhưng chậm vì fsync là operation đắt nhất trên hầu hết filesystem. NORMAL với WAL mode vẫn an toàn trong hầu hết kịch bản thực tế, bạn có thể mất vài transaction cuối cùng nếu OS crash (không phải app crash), nhưng database sẽ không bị corrupt. Với server có UPS hoặc filesystem ổn định, NORMAL là lựa chọn production hợp lý.
OFF thì tuyệt đối không dùng ở production, mất điện đột ngột có thể corrupt database hoàn toàn. Chỉ nên dùng OFF khi bulk import data ban đầu mà data source vẫn còn, có thể import lại nếu lỗi.
Các pragma khác đáng bật
PRAGMA busy_timeout = 5000; -- ms; chờ khi bị SQLITE_BUSY thay vì fail ngay
PRAGMA foreign_keys = ON; -- ép kiểm tra FK (mặc định OFF vì lịch sử!)
PRAGMA temp_store = MEMORY; -- bảng tạm ở RAM thay vì disk
PRAGMA cache_size = -64000; -- ~64MB page cache (âm = KB)
PRAGMA mmap_size = 268435456; -- 256MB mmap cho đọc nhanh
Điều mà nhiều người không biết: foreign_keys = ON phải bật mỗi lần mở connection, nó không lưu vào file database. Lý do lịch sử: SQLite từ đầu không có foreign key support, khi thêm vào sau này phải để mặc định OFF để không break ứng dụng cũ. Nếu bạn dùng ORM hay connection pool, hãy viết hook/callback chạy ngay sau khi connection được tạo để set pragma này. Quên bật là data integrity bay ra cửa sổ mà không ai biết cho đến khi query ra data inconsistent.
busy_timeout cũng cực kỳ quan trọng: nếu không set, khi writer đang giữ lock thì mọi write attempt khác fail ngay lập tức với SQLITE_BUSY. Set 5000ms nghĩa là SQLite sẽ tự retry trong 5 giây trước khi trả lỗi, đủ cho hầu hết write transaction hoàn thành.
mmap_size cho phép SQLite đọc file qua memory-mapped IO thay vì read() syscall, giảm số lần copy data giữa kernel space và user space, tăng tốc đọc đáng kể cho workload read-heavy.
Kích thước page
PRAGMA page_size = 4096; -- hoặc 8192 tuỳ workload
VACUUM; -- áp dụng page_size mới
Page size ảnh hưởng đến IO pattern. Mặc định 4096 bytes khớp với page size của hầu hết filesystem hiện đại và thường là lựa chọn tốt. Với workload có nhiều cột text/blob dài, page lớn hơn (8KB hoặc 16KB) có thể giảm overhead vì ít page cần đọc cho một row. Nhưng page lớn hơn cũng tốn RAM cache hơn và write amplification cao hơn.
Quan trọng: thay đổi page_size phải làm trước khi tạo bảng, hoặc phải chạy VACUUM để rebuild toàn bộ database. VACUUM lock database trong suốt quá trình chạy, với database lớn có thể mất vài phút, plan downtime nếu cần.
Concurrency, những gì thực sự xảy ra
Đây là phần mà nhiều developer hiểu sai nhất về SQLite, dẫn đến bug production khó debug.
WAL mode concurrency
Trong WAL mode, nhiều reader có thể chạy song song, mỗi read transaction thấy snapshot của database tại thời điểm BEGIN. Reader A bắt đầu transaction, writer ghi data mới, reader A vẫn thấy data cũ cho đến khi commit transaction đó và mở transaction mới. Đây là MVCC (Multi-Version Concurrency Control) đơn giản nhưng hiệu quả.
Writer thì chỉ có một tại một thời điểm. Nếu writer A đang trong transaction, writer B cố ghi sẽ bị SQLITE_BUSY. Đây không phải bug, đây là thiết kế. SQLite có một writer lock duy nhất, và đó là trade-off cốt lõi cho sự đơn giản của nó.
Xử lý SQLITE_BUSY
Khi writer trùng nhau, bạn sẽ gặp lỗi SQLITE_BUSY. Có hai cách xử lý.
Cách đơn giản nhất là set busy_timeout đủ cao, SQLite sẽ tự retry trong core, transparent với application code. Với hầu hết web app, set 5000ms là đủ.
Nếu cần control chi tiết hơn, ví dụ muốn exponential backoff hoặc custom logging khi busy, thì implement retry ở application level:
import sqlite3, time, random
def execute_with_retry(conn, sql, params=(), max_retry=5):
for i in range(max_retry):
try:
return conn.execute(sql, params)
except sqlite3.OperationalError as e:
if "database is locked" in str(e):
wait = 0.05 * (2 ** i) + random.random() * 0.01
time.sleep(wait)
continue
raise
raise RuntimeError("busy after retries")
Thông thường busy_timeout đủ dùng cho production và chỉ cần implement custom retry khi có yêu cầu đặc biệt, ví dụ cần metric đếm số lần busy xảy ra để monitor contention.
Transaction và write throughput
Đây là tip quan trọng nhất cho performance SQLite: gom nhiều write vào một transaction thay vì autocommit từng dòng. Sự khác biệt có thể lên đến 100-1000 lần throughput.
Lý do: mỗi transaction SQLite cần ít nhất một fsync(), operation tốn vài millisecond trên SSD. Autocommit từng INSERT nghĩa là mỗi INSERT tốn một fsync. Gom 1000 INSERT vào một transaction thì chỉ tốn một fsync cho cả batch. Hay gặp team insert từng row một và phàn nàn “SQLite chậm quá, chỉ được 50 insert/giây”. Wrap trong BEGIN/COMMIT, nhảy lên 50,000 insert/giây trên cùng hardware.
Dùng prepared statement với placeholder (?) cũng giúp đáng kể, SQLite không phải parse và compile SQL plan mỗi lần, chỉ bind parameter mới. Với bulk import lớn, có thể tạm set PRAGMA journal_mode=OFF và dùng transaction lớn, nhưng chỉ khi bạn chấp nhận mất toàn bộ nếu lỗi giữa chừng và có thể import lại từ đầu.
Connection pool
Trong app multi-thread hoặc async, quản lý SQLite connection cần cẩn thận. SQLite có ba threading mode: single-thread, multi-thread, và serialized. Mode serialized (thường là mặc định khi compile) cho phép nhiều thread dùng chung connection với internal mutex, an toàn nhưng không nhanh bằng mỗi thread có connection riêng.
Tốt nhất là dùng một connection pool nhỏ, phù hợp nhất là 1 write connection và vài read connection. Write connection serialize tất cả write qua một chỗ, tránh SQLITE_BUSY. Read connections có thể song song nhờ WAL.
Nếu dùng ORM như SQLAlchemy, Django ORM, hay GORM, đọc kỹ phần config cho SQLite, mỗi ORM có quirk riêng. SQLAlchemy ví dụ cần set connect_args={"check_same_thread": False} cho multi-thread, và nên dùng StaticPool hoặc pool size nhỏ.
Backup và recovery
Không copy file đang chạy bằng cp
Đây là sai lầm phổ biến nhất khi team dùng SQLite production. File database có thể đang ở giữa một transaction khi bạn cp, bản copy ra sẽ không nhất quán, có thể corrupt. Đặc biệt với WAL mode, database state nằm rải giữa ba file (.db, -wal, -shm), copy thiếu file nào thì data không đúng.
Hai cách backup an toàn trong khi app đang chạy.
Cách thứ nhất là dùng API backup chính thức, SQLite xử lý atomic cho bạn:
sqlite3 app.db ".backup '/backup/app-$(date +%F).db'"
Cách thứ hai là VACUUM INTO (từ SQLite 3.27+), tạo bản copy đã defragment:
VACUUM INTO '/backup/app-2026-04-11.db';
Cả hai đều an toàn với app đang ghi, không cần stop service. .backup phù hợp cho backup thường xuyên vì nhanh hơn (không cần rebuild page), và VACUUM INTO khi muốn bản backup clean không fragmentation.
Backup streaming với Litestream
Litestream là game changer cho SQLite production. Nó chạy như sidecar process, đọc WAL file liên tục, và stream changes lên object storage (S3, B2, GCS, Azure Blob). Phục hồi là download file backup gần nhất rồi replay WAL, cho bạn point-in-time recovery thực tế.
Litestream phù hợp cho mọi SQLite production deployment. Setup đơn giản, một file config YAML, chạy kèm app. RPO (Recovery Point Objective) vài giây, tức bạn mất tối đa vài giây data nếu server die. Trước Litestream, đưa SQLite lên production mà không lo mất data là rất khó. Sau Litestream, nó trở thành viable option nghiêm túc.
Kiểm tra bản backup
Quy tắc vàng: backup chưa bao giờ được thử restore = không có backup. Thường xuyên thử restore bản backup ở môi trường staging hoặc CI, chạy PRAGMA integrity_check trên bản restored, query vài table để verify data đúng. Hay gặp tình huống team backup hàng ngày nhưng 6 tháng sau cần restore mới phát hiện backup script lỗi từ tháng thứ 2, file backup toàn bị truncate.
Replication và HA
SQLite không có replication built-in, đây là trade-off của kiến trúc embedded. Nhưng có ba hướng phổ biến tuỳ yêu cầu.
Litestream replica + single writer
Đây là setup đơn giản nhất và phù hợp cho hầu hết use case. Một instance chạy app và ghi SQLite. Litestream stream WAL ra S3. Khi instance fail, instance mới boot lên, restore từ S3, tiếp tục chạy.
RPO vài giây (thời gian giữa lần stream WAL cuối và lúc crash). RTO phụ thuộc tốc độ restore, với database vài GB, thường vài phút. Không có automatic failover, bạn cần orchestrator bên ngoài (systemd restart, Kubernetes pod restart, hoặc script custom) để detect failure và start instance mới.
Với team nhỏ và service không cần zero-downtime, setup này đủ dùng và cực kỳ đơn giản so với Postgres streaming replication.
LiteFS
LiteFS là distributed filesystem layer của Fly.io chạy trên SQLite. Một node leader nhận write, stream changes xuống nhiều follower node. Follower có bản database gần real-time, đọc nhanh ở edge, gần user.
Ghi vẫn phải route về leader, nếu app ở Tokyo mà leader ở US East, mỗi write có latency vài trăm ms. Nhưng đọc thì instant vì follower ở Tokyo có local copy. Pattern này fit cho workload read-heavy, blog, content site, catalog lookup.
LiteFS tích hợp chặt với Fly.io platform, dùng ngoài Fly thì phức tạp hơn.
rqlite và dqlite
Cả hai wrap Raft consensus protocol quanh SQLite, cho bạn cluster multi-node với automatic leader election và data replication. rqlite expose HTTP API, dqlite embed như library (Canonical dùng cho LXD).
Trade-off: bạn mất tính “một file đơn giản”, giờ có cluster cần quản lý. Write throughput cũng thấp hơn Postgres tương đương vì overhead Raft consensus. Hợp khi bạn cần HA cluster đơn giản cho workload nhẹ, ví dụ distributed config store, metadata service, mà không muốn vận hành Postgres/etcd.
Thực tế: nếu yêu cầu đã đến mức cần Raft-based HA, Postgres thường là lựa chọn tốt hơn rqlite/dqlite. Postgres có ecosystem tool vận hành trưởng thành hơn nhiều, Patroni, pg_basebackup, pgBackRest đều battle-tested.
Bảo mật thực dụng
SQLite file nằm trên disk, ai có quyền đọc file thì đọc được toàn bộ database, không có authentication layer như Postgres. Bảo mật phải xử lý ở tầng OS và application.
File permission là tuyến phòng thủ đầu tiên: chown cho user chạy app, chmod 600, chỉ owner đọc/ghi, không ai khác. Nghe cơ bản nhưng hay gặp SQLite file production có permission 644, bất kỳ user nào trên máy đọc được hết data.
Encryption at rest: SQLite bản thường không encrypt file. Nếu data có PII hoặc nhạy cảm, dùng SQLCipher, fork của SQLite thêm AES-256 encryption transparent, API giống hệt. Hoặc encrypt ở tầng filesystem (LUKS trên Linux, BitLocker trên Windows, eCryptfs cho directory-level).
Backup cũng phải encrypt nếu chứa PII, S3 server-side encryption (SSE-S3 hoặc SSE-KMS) là tối thiểu. Đừng để backup file SQLite nằm trần trên S3 public bucket, nghe hiển nhiên nhưng đã xảy ra nhiều lần trong thực tế.
Về migration: SQLite có ALTER TABLE giới hạn hơn so với Postgres, trước version 3.35 không rename column được, không drop column. Từ 3.35+ đã cải thiện nhiều, nhưng thay đổi type cột vẫn phải qua bước tạo bảng mới, copy data, đổi tên. Dùng migration tool (Alembic, Flyway, Goose, golang-migrate) để quản lý schema changes, đừng chạy ALTER TABLE ad-hoc trên production.
Monitoring và troubleshooting
SQLite không có pg_stat_activity hay slow query log sẵn như Postgres. Monitoring phải tự build, nhưng không phức tạp.
Kích thước file là metric đơn giản nhất: ls -lh app.db* cho bạn biết size của database file chính, WAL file, và shared memory file. WAL file lớn bất thường (hàng trăm MB khi database chỉ vài chục MB) là dấu hiệu checkpoint không chạy, thường vì có long-running read transaction giữ snapshot cũ, ngăn WAL truncate. Fix bằng cách đảm bảo read transaction ngắn, hoặc ép checkpoint thủ công:
PRAGMA wal_checkpoint(TRUNCATE);
Slow query monitoring phải implement ở application layer, wrap database calls với timing, log query nào chậm hơn threshold. Trong SQLite CLI, .timer on cho benchmark nhanh. Với code, sqlite3_trace_v2 callback cho bạn mọi query đang chạy.
Integrity check nên chạy định kỳ, nên schedule weekly trong maintenance window:
PRAGMA integrity_check;
Query này scan toàn bộ database verify B-tree structure, page consistency, index correctness. Với database lớn có thể mất vài phút, nhưng peace of mind là đáng.
Fragmentation tích luỹ theo thời gian khi data được delete rồi insert, page có “lỗ trống” không được tái sử dụng hiệu quả. VACUUM rebuild toàn bộ database, loại bỏ fragmentation, nhưng lock database trong suốt quá trình. Với database nhỏ (dưới vài GB) thì chạy VACUUM monthly trong maintenance window. Với database lớn, dùng incremental vacuum để dọn dần:
PRAGMA auto_vacuum = INCREMENTAL;
PRAGMA incremental_vacuum;
Ước lượng năng lực thực tế
Không có con số “chính thức” vì phụ thuộc rất nhiều vào hardware, schema, query pattern. Nhưng từ benchmark cộng đồng, có thể tham khảo vài con số.
Về đọc, SQLite trên máy hiện đại với SSD có thể đạt hàng chục nghìn QPS nhờ mmap và OS page cache. Với query đơn giản (primary key lookup, index scan nhỏ), con số này rất ấn tượng, nhanh hơn Postgres cho cùng query vì không có network overhead và protocol parsing.
Về ghi, con số phụ thuộc rất nhiều vào cách bạn batch. Commit từng transaction riêng lẻ: vài trăm TPS (bị bottleneck bởi fsync). Gom batch trong một transaction: vài nghìn đến hàng chục nghìn TPS. Sự khác biệt rất lớn, nếu bạn thấy SQLite write chậm, kiểm tra đầu tiên luôn là “có đang autocommit từng row không?”
Về kích thước database, hàng chục GB chạy rất thoải mái, không cần tuning đặc biệt. Trăm GB vẫn được với page cache đủ lớn và index hợp lý. Lên đến TB thì nên cân nhắc nghiêm túc, không phải không chạy được, nhưng VACUUM sẽ mất rất lâu, backup nặng, và bạn đã ở territory mà Postgres sẽ phục vụ tốt hơn với tooling trưởng thành hơn.
Quy tắc ngón tay cái: nếu bạn chưa đụng giới hạn write throughput hoặc database size, và app chạy trên một máy duy nhất, thì SQLite là lựa chọn rất nghiêm túc, không phải đồ chơi, không phải prototype.
Tóm tắt
SQLite là thư viện, không phải server, triển khai cực gọn, zero-admin, latency cực thấp vì không qua mạng. Nhưng “đơn giản” không có nghĩa “không cần config”, bật WAL, set synchronous=NORMAL, busy_timeout, và foreign_keys=ON là bắt buộc cho production.
Một writer tại một thời điểm là constraint cốt lõi, thiết kế app để ghi qua một entry point, gom batch write vào transaction lớn. Backup bằng .backup, VACUUM INTO, hoặc Litestream, tuyệt đối không cp file đang chạy. Replication không có sẵn; chọn Litestream, LiteFS, hoặc rqlite tuỳ yêu cầu HA cụ thể.
Không dùng SQLite khi cần nhiều writer cross-host, write QPS rất cao liên tục, dung lượng data vượt disk một máy, hoặc HA đa vùng phức tạp. Đây là lúc Postgres hay MySQL thể hiện giá trị của kiến trúc client-server.
Với đúng workload, service single-node, read-heavy, data vừa phải, SQLite cho bạn một hệ thống ít bộ phận chuyển động nhất có thể: không container database riêng, không connection pool phải tune, không process phải monitor. Đó là giá trị cực lớn cho team nhỏ, cho internal tool, cho edge deployment, và cho bất kỳ ai muốn giảm operational overhead mà vẫn có SQL đầy đủ.