DELETE chỉ xóa hàng, không gì khác
DELETE lấy các hàng ra khỏi bảng. Nó không xóa bảng, không đổi schema, không đụng tới bảng khác (trừ khi bạn đã thiết lập xóa dây chuyền). Cú pháp rất ngắn:
DELETE FROM users WHERE id = 2; tìm các hàng khớp điều kiện và xóa chúng. Hai hàng còn lại không bị đụng tới. Bản thân bảng vẫn còn, bạn vẫn có thể tiếp tục chèn dữ liệu vào.
Cách hình dung: DELETE là một câu SELECT vứt bỏ các hàng khớp thay vì trả chúng về.
Mệnh đề WHERE gánh toàn bộ công việc
Mọi câu DELETE nghiêm túc đều sống còn nhờ mệnh đề WHERE. Viết đúng thì bạn xóa đúng thứ mình muốn. Viết sai thì bạn xóa nhiều hơn, đôi khi là cả bảng.
Cả hai bản nháp chưa xuất bản và không có lượt xem đều đã biến mất. Các hàng đã xuất bản còn nguyên vì điều kiện không khớp với chúng. Bạn có thể dùng bất kỳ biểu thức nào mà WHERE chấp nhận: IN, LIKE, BETWEEN, truy vấn con, kết hợp AND/OR.
Một thói quen nên tập: trước khi chạy DELETE, hãy chạy cùng mệnh đề WHERE đó trong một câu SELECT trước.
-- Preview what will go:
SELECT * FROM posts WHERE published = 0 AND views = 0;
-- Happy with the rows? Now delete them:
DELETE FROM posts WHERE published = 0 AND views = 0;
Hai bước nhỏ đó đã cứu nhiều cơ sở dữ liệu hơn mọi công cụ sao lưu cộng lại.
DELETE không có WHERE làm trống cả bảng
Bỏ WHERE đi thì DELETE xóa mọi hàng:
Bảng trống nhưng vẫn tồn tại. SQLite không có câu lệnh TRUNCATE: DELETE FROM table; là cách tương đương, và SQLite áp dụng một "truncate optimization" nội bộ, bỏ toàn bộ các trang cùng lúc thay vì xóa từng hàng. Nhanh, nhưng vẫn là một thao tác có giao dịch mà bạn có thể rollback.
Nếu bạn dùng AUTOINCREMENT cho khóa chính, bộ đếm không tự đặt lại. Để id bắt đầu lại từ 1, hãy xóa luôn hàng sequence:
DELETE FROM log;
DELETE FROM sqlite_sequence WHERE name = 'log';
Với INTEGER PRIMARY KEY thông thường (không có AUTOINCREMENT), SQLite vốn đã tự do dùng lại id, nên không cần bước này.
Xóa nhiều hàng cụ thể
IN là cách gọn nhất để xóa một tập hàng đã biết:
Bạn cũng có thể điều khiển việc xóa bằng một truy vấn con, rất tiện khi các hàng cần xóa được xác định bởi một phép join hoặc một bảng khác:
SQLite không hỗ trợ cú pháp DELETE ... JOIN như MySQL, nhưng một truy vấn con trong WHERE làm được cùng việc đó.
RETURNING: xem những gì đã xóa
Thêm RETURNING để nhận lại các hàng đã xóa dưới dạng một tập kết quả, giống như một câu SELECT:
Bạn nhận lại id và email của mọi hàng đã xóa. Điều này cực kỳ hữu ích để:
- Ghi log chính xác những gì đã bị xóa.
- Làm tính năng hoàn tác (cất các hàng trả về vào đâu đó).
- Xác nhận lệnh xóa đã tác động đúng các hàng bạn mong đợi, chỉ trong một lần gọi.
RETURNING dùng được với INSERT, UPDATE và DELETE. Nó được trình bày chi tiết ở trang riêng.
ON DELETE CASCADE cho các hàng liên quan
Khi bảng cha và bảng con được liên kết bằng khóa ngoại, xóa hàng cha sẽ để lại các hàng con mồ côi, trừ khi bạn bảo SQLite xóa dây chuyền:
Xóa tác giả cũng xóa luôn sách của họ. Không có ON DELETE CASCADE, cùng câu DELETE đó hoặc sẽ thành công và để lại sách mồ côi (nếu khóa ngoại đang tắt), hoặc thất bại với lỗi ràng buộc (nếu khóa ngoại đang bật).
Cái bẫy lớn: khóa ngoại mặc định bị tắt trong SQLite. Bạn phải chạy PRAGMA foreign_keys = ON; cho mọi kết nối. Nếu chưa đặt pragma, ON DELETE CASCADE bị lặng lẽ bỏ qua và sách vẫn còn đó. Hầu hết driver của ứng dụng hoặc tự đặt giúp bạn, hoặc có một tùy chọn; hãy kiểm tra driver của bạn.
Các tùy chọn khác nên biết: ON DELETE SET NULL (xóa giá trị khóa ngoại), ON DELETE RESTRICT (từ chối xóa nếu còn hàng con), ON DELETE NO ACTION (mặc định, trong hầu hết trường hợp giống RESTRICT).
DELETE với LIMIT (tùy chọn khi biên dịch)
Một số bản build SQLite hỗ trợ DELETE ... LIMIT, hữu ích để xóa dần từng đợt trên những bảng rất lớn:
DELETE FROM logs
WHERE created_at < '2024-01-01'
ORDER BY created_at
LIMIT 1000;
Tính năng này yêu cầu SQLite được biên dịch với SQLITE_ENABLE_UPDATE_DELETE_LIMIT. Các bản binary chính thức và hầu hết binding của ngôn ngữ (sqlite3 của Python, better-sqlite3 của Node) đều đã bật nó. Nếu bản của bạn không có, bạn sẽ gặp lỗi cú pháp; khi đó hãy dùng truy vấn con:
DELETE FROM logs
WHERE id IN (
SELECT id FROM logs
WHERE created_at < '2024-01-01'
ORDER BY created_at
LIMIT 1000
);
Xóa theo đợt giúp giao dịch luôn nhỏ, điều này quan trọng khi các kết nối khác đang đọc cơ sở dữ liệu.
Bọc các lệnh xóa lớn trong một giao dịch
Một câu DELETE ngầm định có giao dịch: hoặc mọi hàng khớp đều bị xóa, hoặc không hàng nào bị xóa. Nhưng khi sắp xóa rất nhiều, bọc nó trong một giao dịch tường minh nghĩa là bạn có thể ROLLBACK nếu thấy có gì đó không ổn:
ROLLBACK hoàn tác toàn bộ lệnh xóa. Trong một phiên làm việc thật, bạn sẽ COMMIT khi số lượng trông đã đúng. Giao dịch còn nhanh hơn rất nhiều khi xóa nhiều hàng bằng từng câu lệnh riêng: bọc vòng lặp trong BEGIN/COMMIT giúp tránh một lần fsync cho mỗi lệnh xóa.
Những thứ không bị xóa
Vài điểm hay nhầm lẫn đáng lưu ý:
DELETE FROM table;làm trống bảng nhưng không xóa bảng. DùngDROP TABLE table;để xóa chính bảng đó.DELETEkhông làm tệp cơ sở dữ liệu nhỏ lại. Các trang được đánh dấu là trống để dùng lại. Để lấy lại dung lượng ổ đĩa, hãy chạyVACUUM;(được trình bày ở chương về hiệu năng).- Xóa một hàng không xóa các hàng con trong bảng khác, trừ khi đã đặt
ON DELETE CASCADEvà đã bật khóa ngoại. - Một câu
DELETEkhông khớp hàng nào không phải là lỗi. Đó là một câu lệnh thành công vớichanges() = 0. Hãy kiểm tra số hàng nếu bạn cần biết.
Tiếp theo: UPSERT
Nhiều khi bạn không thật sự muốn xóa, mà muốn chèn nếu hàng còn mới, hoặc cập nhật nếu nó đã tồn tại. SQLite gọi đó là UPSERT, và mệnh đề ON CONFLICT gói nó vào một câu lệnh thay vì ba. Đó là nội dung tiếp theo.
Câu hỏi thường gặp
Làm sao xóa một hàng trong SQLite?
Dùng DELETE FROM table_name WHERE condition;. Mệnh đề WHERE chọn những hàng sẽ bị xóa. Ví dụ, DELETE FROM users WHERE id = 7; xóa đúng người dùng có id 7. Không có WHERE, mọi hàng trong bảng đều bị xóa.
Làm sao xóa toàn bộ hàng trong một bảng SQLite?
Chạy DELETE FROM table_name; không kèm mệnh đề WHERE. SQLite không có câu lệnh TRUNCATE: DELETE không có bộ lọc là cách tương đương, và SQLite tối ưu nó ở bên trong (gọi là 'truncate optimization'). Để đặt lại cả bộ đếm AUTOINCREMENT, hãy xóa thêm trong sqlite_sequence sau đó.
SQLite có xóa dây chuyền sang các bảng liên quan được không?
Được, nếu bạn khai báo ON DELETE CASCADE trên khóa ngoại và đã bật khóa ngoại bằng PRAGMA foreign_keys = ON;. Khóa ngoại mặc định bị tắt trong SQLite, nên pragma này rất quan trọng: không có nó, việc xóa dây chuyền bị lặng lẽ bỏ qua.
Làm sao biết những hàng nào đã bị xóa?
Thêm mệnh đề RETURNING: DELETE FROM users WHERE active = 0 RETURNING id, email; trả về các hàng đã xóa giống như một câu SELECT. Cách này hữu ích để ghi log, làm tính năng hoàn tác, hoặc xác nhận bạn đã xóa đúng thứ mình định xóa.