SQLite cũng cần các edition kiểu Rust
(mort.coffee)- SQLite là một RDBMS tự chứa được dùng rộng rãi cho lưu trữ cục bộ và các dự án nhúng, nhưng vì tính tương thích ngược nên vẫn giữ các giá trị mặc định bất lợi cho độ an toàn và hiệu năng
- Mặc định, nó không ép buộc ràng buộc khóa ngoại và có thể tái sử dụng
ROWID, dẫn tới vấn đề toàn vẹn dữ liệu khi bài viết của người dùng đã bị xóa có thể bị gắn sang người dùng khác nhận cùng ID - Bảng thông thường vẫn lưu được kiểu dữ liệu khác với cột đã khai báo; bảng STRICT ngăn điều này nhưng phải chỉ định
strictcho từng bảng và còn xung đột với cách tận dụng tên kiểu do người dùng tự định nghĩa - Trong ghi đồng thời, lỗi
SQLITE_BUSYxảy ra ngay lập tức, WAL bị tắt và thiết lập đồng bộ hóa quá thận trọng; mỗi vấn đề có thể được cải thiện bằng các pragmabusy_timeout,journal_mode,synchronous - Nếu cho phép chọn một gói giá trị mặc định an toàn bằng edition theo năm như
PRAGMA edition = 2026, SQLite có thể tiếp tục cải tiến cấu hình mặc định mà không phá vỡ hành vi cũ
Điểm mạnh của SQLite và các giá trị mặc định đã lỗi thời
- SQLite có thể được xem là tiêu chuẩn thực tế của ngành cho cơ sở dữ liệu trong các dự án nhúng và lưu trữ dữ liệu cục bộ; thậm chí một số phần mềm máy chủ như lobste.rs cũng đang chạy trên SQLite
- Khác với RDBMS truyền thống chạy như một tiến trình riêng biệt, đây là RDBMS dưới dạng thư viện, nên phần mềm có thể được giữ ở dạng tự chứa
- Không cần tự viết serializer và parser như với một định dạng tệp thông thường, nên nó kết hợp được ưu điểm của RDBMS và lưu trữ dựa trên tệp
- Tuy vậy, vẫn còn vấn đề là nhiều giá trị mặc định không phù hợp ảnh hưởng đến toàn vẹn dữ liệu, tính đồng thời và hiệu năng
Vấn đề toàn vẹn do không ép buộc khóa ngoại
- Khóa ngoại là công cụ cốt lõi để giữ tính nhất quán của cơ sở dữ liệu và ngăn tham chiếu bị đứt, nhưng SQLite mặc định không ép buộc điều này
- Ngay cả khi khai báo
posts.user_idtham chiếu tớiusers.id, với cấu hình mặc định nó vẫn không ngăn được các hành vi sau- Tạo bài viết với ID người dùng không hợp lệ
- Xóa người dùng nhưng vẫn để lại các bài viết của người đó
INTEGER PRIMARY KEYtrở thành bí danh củaROWID, và thuật toán cấp phátROWIDcủa SQLite sẽ tái sử dụng ID trong một số tình huống- Bob viết bài với ID
1, rồi dù chỉ xóa tài khoản thì cũng không phát sinh lỗi khóa ngoại - Sau đó Alice nhận lại ID
1đã bị tái sử dụng, và các bài viết cũ của Bob sẽ bị gắn sang Alice - Truy vấn vẫn thành công bình thường, nên khó phát hiện hơn cả một tham chiếu bị đứt đơn thuần
- Bob viết bài với ID
- Có thể ép buộc khóa ngoại bằng cách bật pragma sau cho mỗi kết nối
PRAGMA foreign_keys = ON;
- Nếu áp dụng điều này ngay từ đầu, việc xóa người dùng vẫn còn bài viết sẽ bị chặn với lỗi
FOREIGN KEY constraint failed
Bảng thông thường cho phép kiểu dữ liệu khác với khai báo
- Giá trị trong SQLite thuộc một trong các loại
NULL,INTEGER,REAL,TEXT,BLOB, nhưng khai báo cột trong bảng thông thường không giới hạn nghiêm ngặt kiểu có thể lưu, mà chỉ quyết định type affinity - Với cột có affinity
INTEGER, các quy tắc lưu sau sẽ được áp dụngTEXTcó thể được diễn giải thành số nguyên sẽ được chuyển thànhINTEGERTEXTcó thể được diễn giải thành số thực sẽ được chuyển thànhREALkiểu dấu phẩy động chính xác kép- Các giá trị khác sẽ được lưu nguyên với kiểu ban đầu
- Các affinity khác cũng có quy tắc chuyển đổi riêng
- Affinity
BLOBlưu nguyên giá trị - Affinity
TEXTgiữ nguyênBLOB,TEXT,NULLvà chuyển số thànhTEXT - Affinity
REALhoạt động như affinityINTEGERnhưng chuyển số nguyên thànhREAL
- Affinity
- Vì vậy, ngay cả cột
duration_sec INTEGERcũng có thể lưu chuỗi không phải số như'Way too long, I mean come on' - Trong dự án thực tế, cũng từng có trường hợp thay vì số boolean
1,0thì chuỗi'1','0'lại bị ghi vào, buộc phải dọn dữ liệu và debug
Xung đột giữa bảng STRICT và kiểu do người dùng tự định nghĩa
- Bảng STRICT sẽ phát sinh lỗi kiểu như
cannot store TEXT value in INTEGER columnkhi đưa dữ liệu sai kiểu vào cột
CREATE TABLE music (
id INTEGER PRIMARY KEY,
name TEXT,
duration_sec INTEGER
) strict;
-
Không có pragma nào để biến mọi bảng thành nghiêm ngặt trên toàn cục, nên phải thêm
strictvào từng câu lệnhCREATE TABLEmà không được sót -
SQLite có quan điểm rằng kiểu linh hoạt là hữu ích, nhưng ngay cả với bảng STRICT, nếu chỉ định kiểu
ANYthì vẫn có thể tạo cột lưu mọi loại giá trị- Có thể phân biệt rõ ràng giữa các cột cho phép kiểu tùy ý và các cột buộc kiểu cụ thể
-
Vấn đề tương thích thực tế hơn là bảng STRICT không chỉ ép kiểu dữ liệu mà còn thay đổi cả quy tắc diễn giải type specifier
-
Cách bảng thông thường diễn giải tên kiểu
- Bảng SQLite thông thường xác định affinity theo chuỗi kiểu đã khai báo
- Nếu chứa
"INT"thì được xử lý làINTEGER - Nếu chứa một trong
"CHAR","CLOB","TEXT"thì thànhTEXT - Nếu chứa
"BLOB"hoặc bỏ qua kiểu thì được xử lý làBLOB - Nếu chứa một trong
"REAL","FLOA","DOUB"thì thànhREAL - Nếu không khớp điều kiện nào thì được xử lý là
NUMERIC - Khi kết hợp kiểu lỏng với các quy tắc này, có thể gắn tên kiểu do người dùng tự định nghĩa như
DATETIME,KEY_VALUE_SET,COLORcho cột - Connector hoặc wrapper cơ sở dữ liệu có thể nhìn vào tên kiểu để tự động serialize/deserialze
- Kể cả không có xử lý riêng, bản thân tên kiểu cũng giúp tài liệu hóa dữ liệu dự kiến của cột
- Nếu đổi mặc định sang STRICT như hiện tại thì sẽ phải từ bỏ cách dùng này, nên cần có alias kiểu do người dùng định nghĩa một cách tường minh
CREATE TYPE KEY_VALUE_SET = TEXT;- Nếu trong bảng nghiêm ngặt vẫn dùng được alias như
KEY_VALUE_SET, có thể ghi lại trong schema mẫu dữ liệu dự kiến của cộtTEXTmà ứng dụng phải tự parse - Tính năng gắn
CHECKconstraint vào kiểu tự định nghĩa cũng sẽ hữu ích - Chuẩn SQL:1999 đã có sẵn cú pháp alias kiểu hỗ trợ cả constraint là
CREATE DOMAIN, nên SQLite có thể triển khai bằng cách hỗ trợ chính câu lệnh chuẩn này
Ghi đồng thời và SQLITE_BUSY
- SQLite cho phép nhiều lần đọc đồng thời, nhưng chỉ xử lý một lần ghi tại một thời điểm
- Với cấu hình mặc định, khi hai tiến trình cùng cố lấy khóa ghi thì một bên sẽ không chờ mà nhận ngay lỗi
SQLITE_BUSY- Ghi có thực hiện I/O đĩa vốn dĩ có thể chậm, nên việc chờ khóa được nhả ra trong một khoảng thời gian sẽ tự nhiên hơn
- Trong thực tế cũng từng có trường hợp hệ thống bị gián đoạn lẻ tẻ và phải tự viết vòng lặp retry
- Thiết lập sau chỉ trả về
SQLITE_BUSYsau khi đã thử lại việc lấy khóa trong tối đa 5 giây
PRAGMA busy_timeout = 5000;
- Trong vận hành bình thường, phù hợp nhất là gom mọi lần ghi về một tiến trình duy nhất, lý tưởng nhất là một luồng duy nhất, và ghi đồng thời không thể nhanh hơn
- Tuy vậy, vẫn có những trường hợp vận hành hợp lệ cần ghi đồng thời
- Dọn dẹp cơ sở dữ liệu tương tác bằng công cụ dòng lệnh
sqlite3 - Chạy script cho các tác vụ quản trị hiếm gặp khi không có frontend riêng
- Dọn dẹp cơ sở dữ liệu tương tác bằng công cụ dòng lệnh
- Các công việc như vậy có thể gây ra
SQLITE_BUSYngoài dự kiến do cấu hình mặc định, và làm gián đoạn phần mềm đang chạy
WAL và các giá trị mặc định về hiệu năng
- Nếu được cấu hình đúng, SQLite có thể đủ nhanh để đảm nhận một phần vai trò vốn dành cho các máy chủ lớn như PostgreSQL hay MySQL, nhưng hiệu năng mặc định lại không tốt
- Có thể xem thêm phần tinh chỉnh chi tiết cho môi trường máy chủ tại Optimizing SQLite for servers
- Vấn đề giá trị mặc định quan trọng nhất là Write-Ahead Log, tức WAL đang bị tắt
PRAGMA journal_mode = WAL;
- WAL giúp tăng đáng kể tốc độ ghi trong phần lớn tình huống
- Nếu dùng cùng thiết lập sau, có thể giảm mạnh số lần đồng bộ hóa đĩa mà không làm tăng rủi ro hỏng dữ liệu
PRAGMA synchronous = NORMAL;
- Hành vi cụ thể của
synchronousđược tổng hợp trong tài liệu pragma của SQLite
SQLite edition theo năm
- Nếu thay đổi trực tiếp các giá trị mặc định hiện tại, phần mềm cũ có thể bị hỏng và người dùng có thể sợ nâng cấp SQLite về sau
- Có thể thêm một super pragma duy nhất để chọn các giá trị mặc định đã được cải thiện mà vẫn giữ tương thích ngược
PRAGMA edition = 2026;
- Edition
2026sẽ hoạt động ít nhất như alias của các thiết lập sau
PRAGMA foreign_keys = ON;
PRAGMA busy_timeout = 5000;
PRAGMA journal_mode = WAL;
PRAGMA synchronous = NORMAL;
- Trong edition này, chế độ
strictcho bảng mới cũng được áp dụng làm mặc định - Cách làm này giống Rust editions: bảo toàn hành vi của mã cũ trong khi cho phép chọn tường minh hành vi mặc định mới
- Khác với một chế độ đơn lẻ như
"use strict";của JavaScript, edition theo năm có thể thay đổi gói giá trị mặc định hợp lý theo thời đại - Ví dụ, nếu WAL2 của Hctree được đưa vào nhánh chính năm 2034, thì
PRAGMA edition = 2034trong tương lai có thể được cấu hình để chọnjournal_mode = WAL2 - Hệ thống edition có thể là một giải pháp dung hòa để phát triển các giá trị mặc định về an toàn và hiệu năng của SQLite mà không phá vỡ hành vi mặc định của các chương trình hiện có
2 bình luận
Về lý thuyết thì nghe có vẻ hay, nhưng nếu nghĩ đến xung đột tương thích và overhead thì tôi thấy có lẽ sẽ lợi bất cập hại.
SQLite là một DBMS ra đời để chạy trong các môi trường nhúng như Aegis,
thế giới nhúng là nơi người ta cứ thoải mái thao tác với
void**, khai báo__weak__rồi ghi đè hàm. Trong một thế giới như vậy, tôi lại nghĩ: có nhất thiết phải làm chuyện đó không?Các ý kiến trên Lobste.rs
Hôm qua tôi viết bài này sau khi thấy tin lobste.rs đã chuyển sang SQLite (https://lobste.rs/s/ko1ji1/lobste_rs_is_now_running_on_sqlite)
Khi đọc cuộc thảo luận lúc đó, tôi nhớ đến những bất tiện nhỏ trong các giá trị mặc định của SQLite nên đã tổng hợp thành bài blog
Nếu có thời gian, cũng đáng chia sẻ bài này lên diễn đàn SQLite: https://sqlite.org/forum/forum
Trong SQL,
CREATE TYPEđược dùng để tạo kiểu phức hợp, và Postgres hỗ trợ thêm cả range, enum, array, cũng như các kiểu nền tảng khá phức tạpTính năng mong muốn gần với
CREATE DOMAINhơn. Nó cho phép tạo newtype dựa trên một kiểu cụ thể nền tảng, đồng thời chỉ định cả giá trị mặc định và ràng buộc, nhờ đó các schema phải lặp lại cùng một ràng buộcCHECKtrên nhiều bảng sẽ gọn hơn nhiều. Ví dụ có thể viết nhưcreate domain mything as text;hoặccreate domain mything as integer not null check (VALUE < 5);Cũng cần đề cập đến một cái bẫy khá nguy hiểm: chỉ riêng
busy_timeoutkhông thể ngăn lỗiSQLITE_BUSYxảy ra ngay lập tứcSau khi bắt đầu transaction bằng một truy vấn chỉ đọc, nếu chạy truy vấn ghi để cố nâng cấp khóa đang giữ mà có thao tác ghi khác, nó sẽ thất bại ngay lập tức bất kể thiết lập
busy_timeout. Cách giải quyết là dùngbegin immediateCần cân nhắc rằng trong số các thiết lập được gom vào edition được đề xuất, một số là thuộc tính của kết nối trong mô hình SQLite, còn phần còn lại là thuộc tính của file cơ sở dữ liệu hoặc của từng bảng riêng lẻ
PRAGMA journal_mode = WALđược duy trì trong file cơ sở dữ liệu, nhưngPRAGMA foreign_keys = ONphải được thiết lập lại cho mỗi kết nối.STRICTlà thuộc tính của từng bảng, nên trong cùng một cơ sở dữ liệu cũng có thể trộn bảng strict và bảng không strict. Vì vậy cần quyết định PRAGMA hợp nhất này sẽ được định nghĩa là thuộc tính của file cơ sở dữ liệu hay của kết nối. Tôi cũng tò mò liệu có cơ chế tích hợp nào cảnh báo để thư viện phiên bản cũ không chỉnh sửa rồi làm hỏng file có chứa tính năng mà nó không biết hay khôngCách tiếp cận edition về cơ bản có vẻ là một thỏa hiệp tốt giữa mục tiêu duy trì tương thích ngược của đội ngũ phát triển SQLite và các giá trị mặc định ít gây rối hơn cho người dùng muốn các thiết lập khuyến nghị mới nhất
Gần đây tôi rất mê RocksDB. Nó cũng là thư viện cơ sở dữ liệu nhúng như SQLite, nhưng việc không có SQL lại là một lợi thế lớn theo tôi
Tôi cảm thấy ràng buộc phải biểu diễn mọi thao tác bằng SQL chỉ cản trở việc làm việc với cơ sở dữ liệu
Khó hình dung việc chuyển một hệ thống dùng SQL như lobste.rs sang RocksDB hay một cơ sở dữ liệu key-value khác sẽ như thế nào
Tôi không đồng ý rằng đặt
busy_timeoutlà giải pháp. SQLite có hiệu năng rất tốt nếu dùng một luồng ghi duy nhất và xử lý theo lô các transaction logic không liên quan với nhauCách làm cụ thể được giải thích tại https://andersmurphy.com/2025/12/…
Nhưng cũng thường có việc chỉnh sửa trực tiếp cơ sở dữ liệu để dọn dẹp, hoặc chạy script cho các tác vụ quản trị không thường xuyên đến mức không cần xây riêng màn hình quản trị. Khi đó, việc server hay ứng dụng có thể dừng ngẫu nhiên chỉ vì không may thời điểm chạy bị trùng nhau là điều không tốt