Menu

Lỗi SQLite thường gặp: database is locked, readonly, malformed

Những lỗi SQLite bạn sẽ thực sự gặp trên production như database is locked, readonly database, disk image malformed, vi phạm ràng buộc, và cách sửa từng lỗi.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

Lỗi chỉ là cách SQLite báo cho bạn biết điều gì đó

Thông báo lỗi của SQLite ngắn và đôi khi khó hiểu, nhưng chúng đều quy về một nhóm nhỏ vấn đề gốc. Phần lớn lỗi bạn gặp trên production rơi vào năm nhóm: khóa (locking), quyền truy cập, tệp bị hỏng, lệch schema và vi phạm ràng buộc (constraint). Trang này đi qua từng nhóm: nguyên nhân gây ra lỗi, ý nghĩa thật sự của nó và cách sửa.

Mỗi chuỗi lỗi đi kèm một mã số (mã mở rộng còn cụ thể hơn nữa). Bạn sẽ thấy cả hai dạng trong log:

Error: database is locked          -- code 5 (SQLITE_BUSY)
Error: unable to open database     -- code 14 (SQLITE_CANTOPEN)
Error: attempt to write a readonly -- code 8 (SQLITE_READONLY)
Error: database disk image is      -- code 11 (SQLITE_CORRUPT)

Biết mã lỗi giúp bạn tìm kiếm dễ hơn: tìm SQLITE_BUSY cho kết quả tốt hơn nhiều so với tìm thông báo tiếng Anh thông thường.

database is locked (SQLITE_BUSY)

Đây là lỗi SQLite phổ biến nhất ở mọi ứng dụng ghi dữ liệu từ nhiều nơi. SQLite tuần tự hóa các thao tác ghi: tại một thời điểm chỉ một kết nối được giữ khóa ghi. Nếu một kết nối ghi thứ hai không lấy được khóa trong khoảng busy timeout, bạn sẽ gặp lỗi này.

Có ba cách sửa, xếp theo mức hiệu quả:

Chỉ riêng chế độ WAL đã giải quyết vấn đề khóa cho hầu hết khối lượng công việc. Busy timeout là lưới an toàn khi thật sự có tranh chấp ghi. Ngoài phần cài đặt, hãy rà lại code: một giao dịch (transaction) bị để mở trong lúc chương trình chờ I/O mạng sẽ giữ khóa suốt khoảng thời gian đó. Giữ giao dịch thật ngắn, và COMMIT (hoặc ROLLBACK) ngay khi xong việc.

unable to open database file (SQLITE_CANTOPEN)

SQLite cố mở tệp và hệ điều hành từ chối. Trong 95% trường hợp, vấn đề nằm ở đường dẫn tệp hoặc thư mục chứa nó:

-- Things to check:
-- 1. Does the path exist?            ls -l /path/to/db.sqlite
-- 2. Does the parent directory exist? SQLite creates the file
--    but not the directory above it.
-- 3. Does the user running your process have read+write
--    permission on the directory (not just the file)?
-- 4. Is the volume mounted, not full, and not read-only?

Một trường hợp khó thấy: SQLite cần tạo các tệp phụ (-journal, -wal, -shm) nằm cạnh cơ sở dữ liệu. Nếu bản thân tệp ghi được nhưng thư mục thì không, thao tác mở sẽ thành công còn thao tác ghi thất bại. Luôn cấp quyền ghi ở cấp thư mục.

attempt to write a readonly database (SQLITE_READONLY)

Lỗi này rất gần với lỗi trước. Tệp mở bình thường nhưng ghi thì thất bại. Các nguyên nhân, xếp theo mức độ thường gặp:

  • Người dùng hệ điều hành không có quyền ghi trên tệp hoặc thư mục chứa nó.
  • Kết nối được mở với cờ chỉ đọc (SQLITE_OPEN_READONLY, hoặc mode=ro trong URI).
  • Volume được mount ở chế độ chỉ đọc (hay gặp với bind mount của Docker và một số hệ thống tệp trên cloud).
  • Cơ sở dữ liệu nằm trên hệ thống tệp mạng không hỗ trợ cơ chế khóa mà SQLite cần.

Sửa lại quyền truy cập hoặc mount lại volume. Nếu bạn dùng Docker, kiểm tra bind mount không có :ro và người dùng trong container sở hữu thư mục đó.

database disk image is malformed (SQLITE_CORRUPT)

Các byte trong tệp không còn khớp với định dạng của SQLite. Nguyên nhân thật thường đến từ môi trường: tiến trình bị tắt giữa lúc ghi trên hệ thống tệp không fsync đúng cách, sao chép cơ sở dữ liệu khi đang có kết nối ghi, lỗi phần cứng, hoặc đồng bộ tệp qua Dropbox/iCloud.

Trước tiên, xác nhận mức độ hư hỏng:

Nếu integrity_check trả về ok, cơ sở dữ liệu vẫn ổn và lỗi đến từ chỗ khác (thường là một kết nối cũ). Nếu nó trả về danh sách vấn đề, bạn cần khôi phục.

Cách khôi phục gọn nhất là dùng lệnh .recover của CLI, lệnh này trích xuất mọi dữ liệu còn đọc được sang một cơ sở dữ liệu mới:

sqlite3 corrupt.db ".recover" | sqlite3 recovered.db
sqlite3 recovered.db "PRAGMA integrity_check;"

Nếu có bản sao lưu gần đây, hãy khôi phục từ đó: nhanh hơn và tránh được cảnh mơ hồ kiểu "lấy lại được gần hết rồi." Xem trang sao lưu và khôi phục để biết cách sao chép đúng một cơ sở dữ liệu đang chạy (gợi ý: không phải cp).

no such table và no such column

Hai lỗi này nói đúng điều chúng nói, nhưng nguyên nhân thường là một trong hai: bạn đang kết nối tới một cơ sở dữ liệu khác với cái bạn nghĩ, hoặc một migration chưa chạy.

Kiểm tra connection string của ứng dụng. Đường dẫn tương đối được tính theo thư mục làm việc hiện tại, mà thư mục này khác nhau giữa terminal, IDE và tiến trình production. Cơ sở dữ liệu trong bộ nhớ (:memory:) luôn mới tinh mỗi lần mở, điều này hay làm người dùng bất ngờ khi họ tưởng dữ liệu được giữ lại.

Việc đặt định danh trong dấu ngoặc kép cũng quan trọng. Tên không có dấu ngoặc kép thì không phân biệt hoa thường, nhưng "User" và "user" là hai định danh khác nhau. Nếu bạn tạo bảng với tên đặt trong dấu ngoặc kép, bạn phải luôn đặt nó trong dấu ngoặc kép.

Vi phạm ràng buộc

SQLite từ chối mọi thao tác ghi làm hỏng một ràng buộc. Thông báo lỗi cho biết đó là ràng buộc nào:

Mỗi lỗi ứng với một mã khác nhau bên dưới (SQLITE_CONSTRAINT_UNIQUE, SQLITE_CONSTRAINT_CHECK, SQLITE_CONSTRAINT_NOTNULL). Cách sửa gần như luôn nằm ở tầng ứng dụng: kiểm tra dữ liệu đầu vào trước khi ghi, hoặc dùng INSERT ... ON CONFLICT để xử lý bản ghi trùng một cách có chủ đích.

FOREIGN KEY constraint failed đáng được lưu ý riêng: khóa ngoại mặc định bị tắt trong SQLite. Nếu bạn không bật, các tham chiếu không hợp lệ sẽ lặng lẽ lọt vào, rồi sau này mới gây lỗi khi bạn cuối cùng bật kiểm tra lên. Hãy đặt pragma này trên mọi kết nối:

cannot start a transaction within a transaction

Bạn đã gọi BEGIN trong khi một giao dịch vẫn đang mở. SQLite không cho phép giao dịch lồng nhau, nhưng cho phép savepoint lồng nhau, và cách này cho kết quả tương tự:

Nếu ORM hoặc framework của bạn quản lý giao dịch, nhiều khả năng bạn đã bảo nó bắt đầu giao dịch hai lần. Kiểm tra xem autocommit có đang bật không, và connection pool có đang dùng lại một kết nối còn giao dịch mở hay không.

disk I/O error (SQLITE_IOERR)

Hệ điều hành từ chối một thao tác đọc hoặc ghi. Có thể ổ đĩa đầy, hệ thống tệp mạng trục trặc, hoặc tệp bị xóa ngay khi SQLite đang dùng. Việc đầu tiên cần kiểm tra là df -h. Việc thứ hai là xem cơ sở dữ liệu có nằm ở chỗ kém ổn định như NFS hay thư mục đồng bộ với cloud không. SQLite giả định một hệ thống tệp POSIX cục bộ có fsync hoạt động đúng. Nếu không thể chuyển nó đi, bạn phải chấp nhận rủi ro hỏng dữ liệu cao hơn.

syntax error near "..."

Trình phân tích cú pháp của SQLite cho bạn biết token nào làm nó bối rối. Chỗ cần sửa thường nằm trước vị trí báo lỗi khoảng ba dòng: thiếu dấu phẩy, một định danh không có dấu ngoặc kép trùng với từ khóa, hoặc một chuỗi có dấu nháy đơn cần escape ('it''s', không phải 'it's').

Dùng ràng buộc tham số (placeholder ?) cho dữ liệu người dùng nhập thay vì ghép chuỗi để tạo câu SQL. Bạn sẽ tránh được cả một nhóm lỗi cú pháp lẫn SQL injection cùng lúc.

Danh sách kiểm tra khi chẩn đoán

Khi có sự cố trên production, chuỗi lệnh này bao quát hầu hết trường hợp trong chưa đầy một phút:

Năm pragma, năm câu trả lời. Kết hợp với mã lỗi của truy vấn bị lỗi, bạn sẽ biết vấn đề thuộc nhóm nào và nên mở trang tài liệu nào tiếp theo.

Khép lại chương trình học

Vậy là hết chặng đường. Bạn đã đi từ CREATE TABLE qua join, chỉ mục, giao dịch, chế độ WAL, sao lưu, và giờ là những kiểu lỗi xuất hiện khi SQLite gặp thế giới thực. Các mẫu cứ lặp lại: giao dịch ngắn, bật khóa ngoại, chế độ WAL, sao lưu định kỳ, và luôn coi trọng PRAGMA integrity_check. Giữ những thói quen đó, SQLite sẽ lặng lẽ chạy ổn định nhiều năm liền.

Câu hỏi thường gặp

Vì sao SQLite báo 'database is locked'?

Một kết nối khác đang giữ khóa ghi và kết nối của bạn đã hết thời gian chờ. Cách sửa thường dùng là bật chế độ WAL bằng PRAGMA journal_mode=WAL để thao tác đọc không chặn thao tác ghi, tăng busy timeout bằng PRAGMA busy_timeout = 5000, và commit giao dịch ngay khi xong thay vì để chúng mở.

Làm sao sửa lỗi 'attempt to write a readonly database' trong SQLite?

Gần như lúc nào đây cũng là vấn đề quyền truy cập của hệ thống tệp, không phải của SQLite. Người dùng hệ điều hành chạy tiến trình của bạn cần quyền ghi vào cả tệp cơ sở dữ liệu lẫn thư mục chứa nó (SQLite tạo các tệp phụ -journal hoặc -wal ở đó). Kiểm tra chủ sở hữu, các bit quyền, và chắc chắn volume không được mount ở chế độ chỉ đọc.

'database disk image is malformed' nghĩa là gì?

SQLite đọc được các byte không khớp với định dạng nó mong đợi, thường do tệp bị hỏng vì tiến trình bị tắt ngang, ổ đĩa lỗi, hoặc sao chép tệp khi nó đang mở. Chạy PRAGMA integrity_check để xác nhận, rồi khôi phục bằng .recover trong CLI để đổ phần dữ liệu còn cứu được sang một cơ sở dữ liệu mới. Nếu có bản sao lưu, khôi phục từ bản sao lưu sẽ nhanh hơn.

Vì sao lại gặp lỗi 'no such table' hoặc 'no such column'?

Bạn đang kết nối tới một tệp cơ sở dữ liệu khác với tệp bạn nghĩ, hoặc một migration chưa chạy. Dùng PRAGMA database_list để xem đường dẫn tệp mà SQLite thực sự đã mở, và .schema tablename để xem các cột thật. Gõ sai và lệch chữ hoa chữ thường trong định danh cũng rất hay gặp: SQLite không phân biệt hoa thường với tên không đặt trong dấu ngoặc kép, nhưng có phân biệt với tên được đặt trong dấu ngoặc kép.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu