Buổi 6: Thay đổi dữ liệu: INSERT, UPDATE, DELETE
Năm buổi đầu, chúng ta chỉ đọc dữ liệu với SELECT. Từ buổi này, mỗi câu lệnh bạn viết có thể thay đổi dữ liệu thật: nạp tiền vào tài khoản, đổi trạng thái khách hàng, xóa lệnh giao dịch. Trong công ty chứng khoán, một lệnh UPDATE sai có thể làm sai số dư của hàng nghìn khách hàng chỉ trong một phần nghìn giây. Vì vậy buổi học này không chỉ dạy cú pháp, mà còn dạy thói quen an toàn: kiểm tra trước khi sửa, dùng transaction, sao lưu, và phân quyền.
Bộ dữ liệu mẫu vẫn là hnx_securities (xem mục "Bộ dữ liệu mẫu của khóa học" ở Buổi 1). "Hôm nay" cố định là 2026-09-25. Trên Codelab, mỗi lần chạy bài (và mỗi lần bấm Chạy thử), hệ thống dùng một bản sao mới của cơ sở dữ liệu, nên bạn cứ thoải mái thử INSERT/UPDATE/DELETE: dữ liệu gốc không bị ảnh hưởng — đúng tinh thần "thao tác trên bảng thử nghiệm" của syllabus. Với các bài loại này, hệ thống không so sánh kết quả in ra mà chạy thêm một câu kiểm tra để xem trạng thái dữ liệu sau khi script của bạn chạy xong.
Nguyên tắc số 1 của buổi học: trước mỗi
UPDATE/DELETE, hãy viết một câuSELECTcùng điều kiệnWHEREđể xem chính xác những dòng sẽ bị tác động.
Mục tiêu buổi học
- Sau buổi này bạn có thể viết
INSERTmột dòng, nhiều dòng vàINSERT ... SELECT; hiểu giá trị mặc định và cột tự tăng (IDENTITY/AUTO_INCREMENT). - Sau buổi này bạn có thể viết
UPDATE(một/nhiều cột, từ subquery) và nhận biết nguy hiểm củaUPDATEthiếuWHERE. - Sau buổi này bạn có thể viết
DELETEđúng thứ tự khóa ngoại; phân biệtDELETE,TRUNCATEvà biết các cách phục hồi dữ liệu. - Sau buổi này bạn có thể dùng transaction (
BEGIN/COMMIT/ROLLBACK), hiểu ACID và ứng dụng để bảo đảm toàn vẹn dữ liệu. - Sau buổi này bạn có thể áp dụng best practices: backup trước khi thao tác,
SELECTkiểm tra trước, quản lý quyền truy cập.
Bản đồ buổi học
Buổi học 180 phút: khoảng 26% lý thuyết + demo (~46 phút) và 74% thực hành (~134 phút, gồm cả thời gian chữa bài và kiểm tra lại dữ liệu bằng SELECT). Bài tập nằm xen kẽ trong bài giảng — học xong phần nào, làm ngay bài của phần đó. Buổi có 15 bài (đa số là thao tác ngắn, kiểm tra bằng trạng thái dữ liệu) nên lý thuyết giảng gọn; bài khó chưa xong trong giờ thì hoàn thành ở nhà.
| Phần | Nội dung | Thời lượng |
|---|---|---|
| Mục 1 | INSERT: một dòng, nhiều dòng, INSERT ... SELECT, cột tự tăng |
9 phút |
| Thực hành 6.1 | Thêm khách hàng, lệnh, giao dịch; nhân bản dữ liệu; nhật ký (4 bài) | 31 phút |
| Mục 2 | UPDATE: nhiều cột, từ subquery, cảnh báo thiếu WHERE |
7 phút |
| Thực hành 6.2 | Nạp tiền, kích hoạt khách hàng, nâng loại VIP, đổi loại lệnh (4 bài) | 28 phút |
| Mục 3 | DELETE, khóa ngoại, TRUNCATE, phục hồi dữ liệu |
7 phút |
| Thực hành 6.3 | Xóa giao dịch, lệnh cũ, khách hàng không hoạt động, dọn nhật ký (4 bài, có 1 bài khó) | 38 phút |
| Mục 4 | Transaction: BEGIN/COMMIT/ROLLBACK, ACID |
10 phút |
| Thực hành 6.4 | Chuyển tiền, rollback/commit, khớp lệnh nguyên tử (3 bài, có 1 bài khó) | 37 phút |
| Mục 5 | Best practices: backup, SELECT trước, phân quyền |
5 phút |
| Tổng kết | Sai lầm thường gặp, hỏi đáp, bài kiểm tra nhanh | 8 phút |
| Tổng | 180 phút — lý thuyết + demo ~46 phút, thực hành ~134 phút (74%) |
1. Lệnh INSERT — thêm dữ liệu
1.1. Cú pháp cơ bản
INSERT INTO tên_bảng (cột1, cột2, ...)
VALUES (giá_trị1, giá_trị2, ...);Luôn liệt kê tên cột dù có thể bỏ qua. Nếu bạn viết INSERT INTO Customers VALUES (...) không có danh sách cột, câu lệnh phụ thuộc vào thứ tự cột hiện tại của bảng; khi ai đó thêm cột mới, câu lệnh cũ sẽ hỏng hoặc — tệ hơn — chèn nhầm dữ liệu vào sai cột.
Ví dụ thêm một khách hàng cá nhân mới, rồi kiểm tra ngay bằng SELECT:
INSERT INTO Customers (customer_id, customer_code, full_name, customer_type,
birth_date, city, email, status, created_date)
VALUES (41, 'KH100287', 'Le Minh Quan', 'INDIVIDUAL',
'1990-05-15', 'Ha Noi', '[email protected]', 'NEW', '2026-09-25');
SELECT customer_id, customer_code, full_name, status FROM Customers WHERE customer_id = 41;| customer_id | customer_code | full_name | status |
|---|---|---|---|
| 41 | KH100287 | Le Minh Quan | NEW |
Các cột không nêu (phone, referrer_id) nhận giá trị mặc định của cột — nếu không khai báo DEFAULT thì là NULL. Riêng cột khai báo NOT NULL mà không có DEFAULT (như customer_code, full_name, customer_type, city, status, created_date) bắt buộc phải được cung cấp giá trị, nếu không câu lệnh báo lỗi.
Lưu ý: hằng chuỗi đặt trong dấu nháy đơn
'...'; ngày viết'yyyy-mm-dd'; số không đặt trong nháy. Dấu nháy đơn trong dữ liệu phải nhân đôi ('O''Brien').
1.2. INSERT nhiều dòng cùng lúc
Đưa nhiều bộ giá trị vào một câu lệnh, phân cách bằng dấu phẩy:
INSERT INTO Customers (customer_id, customer_code, full_name, customer_type, birth_date, city, email, status, created_date)
VALUES (42, 'KH100294', 'Pham Thu Trang', 'INDIVIDUAL', '1995-11-02', 'Da Nang', '[email protected]', 'NEW', '2026-09-25'),
(43, 'KH100301', 'Nguyen Duc Thang', 'INDIVIDUAL', '1988-03-27', 'TP.HCM', '[email protected]', 'NEW', '2026-09-25');
SELECT customer_id, full_name, city FROM Customers WHERE customer_id >= 42 ORDER BY customer_id;Cú pháp này dùng được ở cả SQL Server (từ 2008) và MySQL. Lợi ích: nhanh hơn nhiều so với nhiều câu INSERT rời rạc, và nguyên tử — cả câu lệnh hoặc thành công toàn bộ, hoặc không dòng nào được thêm.
SQL Server vs MySQL: SQL Server cho tối đa 1.000 bộ giá trị trong một
VALUES; MySQL giới hạn theo kích thước gói (max_allowed_packet), thực tế chèn được hàng nghìn dòng mỗi câu.
1.3. Khóa chính và khóa ngoại khi INSERT
Dữ liệu chèn vào phải thỏa mọi ràng buộc, nếu không toàn bộ câu lệnh bị từ chối:
| Vi phạm | SQL Server báo | MySQL báo |
|---|---|---|
Trùng khóa chính (customer_id đã tồn tại) |
Violation of PRIMARY KEY constraint | Duplicate entry ... for key 'PRIMARY' |
Khóa ngoại không tồn tại (thêm tài khoản cho customer_id = 999) |
The INSERT statement conflicted with the FOREIGN KEY constraint | Cannot add or update a child row |
Trùng UNIQUE (customer_code, account_no) |
Violation of UNIQUE KEY constraint | Duplicate entry |
Thiếu cột NOT NULL |
Cannot insert the value NULL into column | Field ... doesn't have a default value |
Hệ quả của khóa ngoại: phải thêm bảng cha trước, bảng con sau (Customers → Accounts → Orders → Trades).
1.4. Thêm dòng cho khách hàng: ghi lệnh và giao dịch
Thêm giao dịch mới thường cần tra cứu khóa ngoại thay vì gõ cứng. Ví dụ: khách hàng KH100007 đặt lệnh mua 100 cổ phiếu FPT trên tài khoản CASH đang hoạt động của họ. Ta không nhớ account_id, nên dùng INSERT ... SELECT để lấy từ Accounts:
INSERT INTO Orders (order_id, account_id, symbol, side, order_type, quantity, price, order_date, status)
SELECT 121, a.account_id, 'FPT', 'BUY', 'LO', 100, 120000, '2026-09-25', 'MATCHED'
FROM Accounts a JOIN Customers c ON c.customer_id = a.customer_id
WHERE c.customer_code = 'KH100007' AND a.account_type = 'CASH' AND a.status = 'ACTIVE';
INSERT INTO Trades (trade_id, order_id, symbol, quantity, price, trade_date)
VALUES (87, 121, 'FPT', 100, 120000, '2026-09-25');
SELECT t.trade_id, o.order_id, o.account_id, t.symbol, t.quantity, t.price
FROM Trades t JOIN Orders o ON o.order_id = t.order_id
WHERE t.trade_id = 87;Chú ý thứ tự: lệnh (Orders) phải có trước giao dịch (Trades) vì Trades.order_id là khóa ngoại. Cách này an toàn hơn gõ cứng: nếu khách hàng có tài khoản khác, câu lệnh vẫn chọn đúng tài khoản thỏa điều kiện. Nếu SELECT không trả dòng nào (khách hàng không có tài khoản phù hợp), không có gì được chèn và cũng không báo lỗi — hãy kiểm tra số dòng bị ảnh hưởng (mục 5).
Vừa học xong INSERT một dòng, nhiều dòng, khóa ngoại và tra cứu khóa ngoại bằng INSERT ... SELECT, hãy làm bài 6.1a (thêm khách hàng mới, một dòng và nhiều dòng) và 6.1b (thêm lệnh và giao dịch mới cho khách hàng).
1.5. INSERT ... SELECT: nhân bản dữ liệu từ bảng khác
INSERT ... SELECT chèn kết quả của một truy vấn vào bảng đích. Dùng để sao lưu, lưu trữ, hoặc đổ dữ liệu trung gian:
CREATE TABLE AccountArchive (
account_id INT PRIMARY KEY,
customer_id INT NOT NULL,
balance DECIMAL(18,2) NOT NULL
);
INSERT INTO AccountArchive (account_id, customer_id, balance)
SELECT account_id, customer_id, balance
FROM Accounts
WHERE status = 'CLOSED';
SELECT account_id, customer_id, balance FROM AccountArchive ORDER BY account_id;Với dữ liệu mẫu, 5 tài khoản CLOSED (ID 8, 9, 21, 22, 23) được sao chép. Không dùng VALUES khi dùng SELECT. Số cột và kiểu dữ liệu ở hai vế phải khớp theo thứ tự.
SQL Server vs MySQL: tạo bảng mới từ kết quả truy vấn không cần
CREATE TABLEtrước, nhưng cú pháp khác nhau.
SQL Server:
SELECT account_id, customer_id, balance INTO AccountArchive2 FROM Accounts WHERE status = 'CLOSED';
SELECT COUNT(*) AS so_dong FROM AccountArchive2;MySQL:
CREATE TABLE AccountArchive2 AS SELECT account_id, customer_id, balance FROM Accounts WHERE status = 'CLOSED';
SELECT COUNT(*) AS so_dong FROM AccountArchive2;Lưu ý: cách này tiện nhưng không sao chép khóa chính, index, ràng buộc; nếu cần đầy đủ, hãy
CREATE TABLEtrước rồiINSERT ... SELECT.
1.6. Giá trị mặc định và cột tự tăng (IDENTITY / AUTO_INCREMENT)
Hai cơ chế giúp bạn không phải cung cấp giá trị cho một số cột:
- DEFAULT: giá trị mặc định khai báo trong bảng (ví dụ
changed_atmặc định là thời điểm ghi). Bỏ cột khỏi danh sáchINSERT, hoặc viếtDEFAULTtrongVALUES. - Cột tự tăng: hệ thống tự cấp số tiếp theo. SQL Server khai báo
IDENTITY(1,1); MySQL khai báoAUTO_INCREMENT.
Bảng AuditLog của khóa học dùng cả hai (log_id tự tăng, changed_at mặc định):
INSERT INTO AuditLog (customer_id, old_status, new_status) VALUES (5, 'NEW', 'ACTIVE');
INSERT INTO AuditLog (customer_id, old_status, new_status) VALUES (12, 'NEW', 'ACTIVE');
SELECT log_id, customer_id, old_status, new_status FROM AuditLog ORDER BY log_id;| log_id | customer_id | old_status | new_status |
|---|---|---|---|
| 1 | 5 | NEW | ACTIVE |
| 2 | 12 | NEW | ACTIVE |
Những điều cần biết về cột tự tăng:
- Không được chỉ định giá trị cho cột
IDENTITY(SQL Server sẽ báo lỗi) trừ khi bậtSET IDENTITY_INSERT AuditLog ON. Sau khi chèn taylog_id = 100, dòng kế tiếp tự nhận 101. - Số đã cấp không được tái sử dụng: nếu bạn
DELETEtoàn bộ bảng rồi chèn tiếp,log_idtiếp nối (ví dụ 3) chứ không quay lại 1. Nếu chèn trong transaction rồiROLLBACK, số đó cũng bị "cháy" (ở cả hai hệ, dòng chèn kế tiếp sẽ cólog_id = 2). Vì vậy đừng dùng cột tự tăng làm số liên tục kế toán. - Lấy giá trị vừa được cấp: SQL Server
SELECT SCOPE_IDENTITY();, MySQLSELECT LAST_INSERT_ID();(dùng để chèn tiếp vào bảng con).
Ví dụ nghiệp vụ:
AuditLoglà "hộp đen" của phòng Tuân thủ: mỗi lần trạng thái khách hàng đổi, một dòng mới được ghi kèm thời điểm. Nhờlog_idtự tăng vàchanged_atmặc định, ứng dụng chỉ cần gửi 3 giá trị — ít lỗi hơn và không thể "quên" ghi thời điểm.
Vừa học xong INSERT ... SELECT để nhân bản dữ liệu, giá trị mặc định và cột tự tăng, hãy làm bài 6.1c (nhân bản tài khoản đã đóng vào bảng lưu trữ) và 6.1d (ghi nhật ký với cột tự tăng và giá trị mặc định).
2. Lệnh UPDATE — sửa dữ liệu
2.1. Cú pháp
UPDATE tên_bảng
SET cột1 = giá_trị1, cột2 = giá_trị2
WHERE điều_kiện;Vế phải của SET có thể là hằng, biểu thức dựa trên chính cột đó, hoặc subquery. Ví dụ nạp 5.000.000 vào tài khoản ACC00003:
UPDATE Accounts
SET balance = balance + 5000000
WHERE account_no = 'ACC00003';
SELECT account_no, balance FROM Accounts WHERE account_no = 'ACC00003';Số dư tài khoản này đang là 32.000.000 → 37.000.000. Lưu ý dùng balance = balance + số tiền (cộng dồn) thay vì gán cứng số mới: nếu có giao dịch khác cùng lúc, cách cộng dồn không ghi đè mất thay đổi của người khác.
2.2. UPDATE nhiều cột cùng lúc
Đưa nhiều phép gán trong một SET, cách nhau dấu phẩy. Ví dụ chuyển lệnh thị trường (MP) chưa khớp thành lệnh giới hạn (LO) với giá đóng cửa 2026-09-25 — vừa đổi hai cột, vừa lấy giá từ subquery:
UPDATE Orders
SET order_type = 'LO',
price = (SELECT m.close_price FROM MarketPrices m
WHERE m.symbol = Orders.symbol AND m.trade_date = '2026-09-25')
WHERE order_type = 'MP' AND status = 'PENDING';
SELECT order_id, symbol, order_type, price FROM Orders WHERE order_id IN (6, 34, 62, 104, 118) ORDER BY order_id;Câu lệnh này chuyển 5 lệnh (ID 6, 34, 62, 104, 118). Hiểu quy tắc của SQL: toàn bộ vế phải được tính dựa trên giá trị cũ của dòng, rồi mới gán.
SQL Server vs MySQL: vì quy tắc trên,
SET a = b, b = asẽ hoán đổi giá trị trong SQL Server, còn MySQL xử lý gán từ trái sang phải nên kết quả khác! Tránh viết những câu như vậy.
Vừa học xong UPDATE một cột (cộng dồn) và nhiều cột với giá trị lấy từ subquery, hãy làm bài 6.2a (nạp tiền vào tài khoản — nhớ WHERE) và 6.2d (chuyển lệnh MP chờ khớp thành LO theo giá đóng cửa).
2.3. Cập nhật có điều kiện theo dữ liệu bảng khác
Thay đổi loại khách hàng: khách hàng có tổng tài sản lớn hơn 5 tỷ thành VIP:
UPDATE Customers
SET customer_type = 'VIP'
WHERE customer_id IN (SELECT customer_id
FROM Accounts
GROUP BY customer_id
HAVING SUM(balance) > 5000000000);
SELECT customer_id, full_name, customer_type FROM Customers WHERE customer_type = 'VIP' ORDER BY customer_id;Với dữ liệu mẫu, 3 khách hàng được nâng: ID 2 (8,64 tỷ), 17 (12,07 tỷ), 40 (9,5 tỷ). Chú ý so sánh > (không phải >=): khách hàng có đúng 5 tỷ không đủ điều kiện. Cách viết IN (subquery) (Buổi 5) chạy giống nhau trên cả hai hệ.
Khi muốn nối bảng (JOIN) trong UPDATE, cú pháp khác nhau. Ví dụ khóa các tài khoản ACTIVE của khách hàng INACTIVE:
SQL Server — UPDATE alias SET ... FROM bảng alias JOIN ...:
UPDATE a SET a.status = 'LOCKED'
FROM Accounts a JOIN Customers c ON c.customer_id = a.customer_id
WHERE c.status = 'INACTIVE' AND a.status = 'ACTIVE';
SELECT COUNT(*) AS so_tai_khoan_bi_khoa FROM Accounts WHERE status = 'LOCKED';MySQL — UPDATE bảng JOIN ... SET ...:
UPDATE Accounts a JOIN Customers c ON c.customer_id = a.customer_id
SET a.status = 'LOCKED'
WHERE c.status = 'INACTIVE' AND a.status = 'ACTIVE';
SELECT COUNT(*) AS so_tai_khoan_bi_khoa FROM Accounts WHERE status = 'LOCKED';Cả hai khóa 7 tài khoản ACTIVE của các khách hàng INACTIVE (17, 23, 36). Cách dùng IN (subquery) như trên không phụ thuộc dialect, nên là lựa chọn dễ mang đi giữa hai hệ.
2.4. Chuyển trạng thái hàng loạt
UPDATE Customers SET status = 'ACTIVE' WHERE status = 'NEW';Bốn khách hàng đang NEW (ID 5, 12, 19, 26) chuyển thành ACTIVE; 5 khách hàng INACTIVE không bị đụng tới. Trước khi chạy, hãy đếm: SELECT COUNT(*) FROM Customers WHERE status = 'NEW'; (kết quả 4) và so khớp số dòng bị ảnh hưởng sau khi chạy.
Vừa học xong UPDATE theo dữ liệu bảng khác và chuyển trạng thái hàng loạt, hãy làm bài 6.2b (chuyển khách hàng mới sang trạng thái kích hoạt) và 6.2c (nâng loại khách hàng lên VIP theo tổng tài sản).
2.5. CẢNH BÁO: UPDATE không có WHERE
Hãy xem chuyện gì xảy ra nếu quên WHERE:
UPDATE Accounts SET balance = 0; -- KHÔNG CÓ WHERE!
SELECT SUM(balance) AS tong_so_du FROM Accounts;Kết quả 0.00: câu lệnh đặt số dư của cả 93 tài khoản về 0. Không có cảnh báo, không hỏi lại. Đây là lỗi vận hành nổi tiếng: quên WHERE, hoặc bôi đen chỉ một phần câu lệnh trong SSMS rồi bấm chạy (chỉ chạy đoạn bôi đen, mất WHERE).
Cảnh báo: trên hệ thống thật, câu lệnh trên xóa sạch số dư của toàn công ty trong tích tắc. Bài 6.2a được chấm bằng cả tổng số dư toàn bảng — nếu bạn quên
WHERE, kết quả sai ngay lập tức.
Cách phòng tránh:
- Viết
WHEREtrước, gõUPDATE/SETsau; hoặc viết dạngSELECTtrước rồi đổi thànhUPDATE. - Dùng transaction (mục 4): kiểm tra số dòng bị ảnh hưởng, sai thì
ROLLBACK. - MySQL Workbench có chế độ Safe Updates (
SET sql_safe_updates = 1;) từ chốiUPDATE/DELETEkhông cóWHEREdùng khóa; nên bật trên môi trường làm việc.
Một lỗi khác: viết WHERE cột = NULL. NULL không bằng gì cả (kể cả NULL); phải dùng WHERE cột IS NULL và SET cột = NULL để gán.
3. Lệnh DELETE — xóa dữ liệu
3.1. Cú pháp
DELETE FROM tên_bảng
WHERE điều_kiện;Ví dụ xóa toàn bộ giao dịch (Trades) của khách hàng KH100014. Bảng Trades không có cột customer_id, nên phải đi qua chuỗi Trades → Orders → Accounts → Customers bằng subquery. Theo nguyên tắc số 1, ta xem trước rồi mới xóa:
-- Bước 1: xem trước bao nhiêu dòng sẽ bị xóa
SELECT COUNT(*) AS so_dong_se_xoa FROM Trades
WHERE order_id IN (SELECT o.order_id FROM Orders o
JOIN Accounts a ON a.account_id = o.account_id
JOIN Customers c ON c.customer_id = a.customer_id
WHERE c.customer_code = 'KH100014');Kết quả là 4. Bước 2 — xóa (cùng điều kiện), rồi đếm lại toàn bảng:
DELETE FROM Trades
WHERE order_id IN (SELECT o.order_id FROM Orders o
JOIN Accounts a ON a.account_id = o.account_id
JOIN Customers c ON c.customer_id = a.customer_id
WHERE c.customer_code = 'KH100014');
SELECT COUNT(*) AS so_giao_dich_con_lai FROM Trades;Trước khi xóa Trades có 86 dòng, sau khi xóa còn 82. Lưu ý: DELETE chỉ xóa bảng đích; Orders của khách hàng này vẫn còn.
3.2. Xóa và ràng buộc khóa ngoại
Khóa ngoại bảo vệ dữ liệu: bạn không thể xóa dòng cha nếu còn dòng con tham chiếu:
DELETE FROM Customers WHERE customer_id = 2;
-- SQL Server: The DELETE statement conflicted with the REFERENCE constraint ...
-- MySQL: Cannot delete or update a parent row: a foreign key constraint failsQuy tắc: xóa từ bảng con lên bảng cha. Với hệ thống chứng khoán của khóa học:
Trades → Orders → Portfolio → Accounts → CustomerStats → Customers
Ví dụ bài 6.3b: xóa các lệnh CANCELLED trước 2026-07-01 (7 lệnh). Nếu có lệnh còn giao dịch đi kèm thì phải xóa giao dịch trước:
DELETE FROM Trades WHERE order_id IN (SELECT order_id FROM Orders WHERE status = 'CANCELLED' AND order_date < '2026-07-01');
DELETE FROM Orders WHERE status = 'CANCELLED' AND order_date < '2026-07-01';Cảnh báo: thay vì xóa thủ công từng tầng, cơ sở dữ liệu có thể khai báo
ON DELETE CASCADE(xóa dây chuyền) — thuận tiện nhưng nguy hiểm vì một câuDELETEcó thể xóa lan rất rộng; hệ thống tài chính thường không bật cascade cho dữ liệu giao dịch.
Vừa học xong DELETE với WHERE, subquery và thứ tự khóa ngoại, hãy làm bài 6.3a (xóa giao dịch của một khách hàng cụ thể) và 6.3b (xóa lệnh đã hủy cũ, tôn trọng khóa ngoại).
3.3. Xóa khách hàng không hoạt động (bài toán tổng hợp)
Bài 6.3c yêu cầu xóa các khách hàng INACTIVE và không có giao dịch từ 2025-09-25 (12 tháng), cùng toàn bộ dữ liệu phụ thuộc. Mẫu giải quyết chuẩn: chốt danh sách trước bằng bảng tạm (Buổi 5), rồi xóa từ dưới lên.
SQL Server:
SELECT c.customer_id INTO #del
FROM Customers c
WHERE c.status = 'INACTIVE'
AND NOT EXISTS (SELECT 1 FROM Trades t JOIN Orders o ON o.order_id = t.order_id
JOIN Accounts a ON a.account_id = o.account_id
WHERE a.customer_id = c.customer_id AND t.trade_date >= '2025-09-25');MySQL — CREATE TEMPORARY TABLE del AS SELECT c.customer_id FROM Customers c WHERE ... (cùng điều kiện). Sau đó, ở cả hai hệ, xóa từ bảng con lên bảng cha (dưới đây viết cho SQL Server với #del; MySQL đổi #del thành del):
DELETE FROM Trades WHERE order_id IN (SELECT o.order_id FROM Orders o JOIN Accounts a ON a.account_id = o.account_id
WHERE a.customer_id IN (SELECT customer_id FROM #del));
DELETE FROM Orders WHERE account_id IN (SELECT account_id FROM Accounts WHERE customer_id IN (SELECT customer_id FROM #del));
DELETE FROM Portfolio WHERE account_id IN (SELECT account_id FROM Accounts WHERE customer_id IN (SELECT customer_id FROM #del));
DELETE FROM Accounts WHERE customer_id IN (SELECT customer_id FROM #del);
DELETE FROM CustomerStats WHERE customer_id IN (SELECT customer_id FROM #del);
DELETE FROM Customers WHERE customer_id IN (SELECT customer_id FROM #del);Vì sao phải chốt danh sách trước? Tiêu chí "không có giao dịch trong 12 tháng" được tính qua chuỗi nối Trades → Orders → Accounts; khi bạn xóa dần từng tầng thì chuỗi nối này bị đứt, không thể tính lại tiêu chí ở các câu DELETE sau. Bảng tạm #del giữ cố định danh sách gốc cho mọi câu lệnh. Với dữ liệu mẫu, khách hàng 4 và 9 bị xóa; khách hàng 17, 23, 36 dù INACTIVE vẫn được giữ vì có giao dịch gần đây. Sau đó Customers còn 38 dòng, Accounts còn 88, Orders giữ 120, Trades giữ 86.
3.4. DELETE, TRUNCATE và DROP
DELETE |
TRUNCATE TABLE |
DROP TABLE |
|
|---|---|---|---|
| Xóa cái gì | các dòng thỏa WHERE (hoặc tất cả) |
tất cả dòng | cả bảng (cấu trúc + dữ liệu) |
Có WHERE |
có | không | không |
| Cột tự tăng | không đặt lại | đặt lại về giá trị khởi đầu | mất cùng bảng |
| Tốc độ (bảng lớn) | chậm (ghi log từng dòng) | rất nhanh | rất nhanh |
| Kích hoạt trigger | có | không | không |
| Bảng có khóa ngoại tham chiếu tới | được (nếu không còn dòng con) | không được (cả hai hệ) | không được |
| Rollback trong transaction | có | SQL Server: có; MySQL: không (tự động COMMIT) | tương tự |
Hãy tự kiểm chứng sự khác biệt về cột tự tăng. Với TRUNCATE, dòng chèn sau cùng có log_id = 1:
INSERT INTO AuditLog (customer_id, old_status, new_status) VALUES (5, 'NEW', 'ACTIVE');
INSERT INTO AuditLog (customer_id, old_status, new_status) VALUES (12, 'NEW', 'ACTIVE');
TRUNCATE TABLE AuditLog;
INSERT INTO AuditLog (customer_id, old_status, new_status) VALUES (19, 'NEW', 'ACTIVE');
SELECT log_id, customer_id FROM AuditLog;Còn nếu thay TRUNCATE TABLE AuditLog; bằng DELETE FROM AuditLog;, dòng mới mang số tiếp nối:
INSERT INTO AuditLog (customer_id, old_status, new_status) VALUES (5, 'NEW', 'ACTIVE');
INSERT INTO AuditLog (customer_id, old_status, new_status) VALUES (12, 'NEW', 'ACTIVE');
DELETE FROM AuditLog;
INSERT INTO AuditLog (customer_id, old_status, new_status) VALUES (19, 'NEW', 'ACTIVE');
SELECT log_id, customer_id FROM AuditLog;Kết quả log_id = 3 (tiếp nối) thay vì 1.
Vừa học xong xóa nhiều tầng bằng bảng tạm (mục 3.3) và sự khác nhau giữa DELETE với TRUNCATE, hãy làm bài 6.3c (xóa khách hàng không hoạt động và toàn bộ dữ liệu liên quan) và 6.3d (dọn bảng nhật ký, đánh số lại).
SQL Server vs MySQL:
TRUNCATE,CREATE,DROP,ALTERtrong MySQL là lệnh DDL và gây implicit commit — mọi thay đổi đang treo trong transaction sẽ bị chốt ngay, không thểROLLBACK. Không đặtTRUNCATEvào giữa một transaction quan trọng.
3.5. Phục hồi dữ liệu sau DELETE
Một khi DELETE đã COMMIT, dữ liệu không tự quay lại. Các lớp bảo vệ, từ nhẹ đến nặng:
| Mức | Cách | Điều kiện |
|---|---|---|
| 1 | Chưa COMMIT: ROLLBACK (mục 4) |
cứu hộ tức thì; lý do nên chạy thao tác nguy hiểm trong transaction |
| 2 | Chụp lại dòng bị xóa trước khi xóa: INSERT ... SELECT sang bảng lưu trữ (như 6.1c); SQL Server có OUTPUT deleted.* để ghi dòng vừa xóa vào bảng khác trong cùng câu lệnh |
chuẩn bị trước khi xóa |
| 3 | Xóa mềm (soft delete): đặt status = 'DELETED' hoặc is_deleted = 1 và lọc khi truy vấn |
thiết kế bảng từ đầu; thường dùng với dữ liệu tài chính vì luật yêu cầu lưu vết |
| 4 | Khôi phục từ backup: SQL Server RESTORE DATABASE ... WITH STOPAT = 'thời điểm' (bản full + log); MySQL khôi phục mysqldump rồi áp lại binary log đến trước thời điểm sai |
việc của DBA, cần chuẩn bị từ trước (mục 5) |
4. Transaction (giao dịch)
4.1. Vấn đề cần giải quyết
Chuyển 10.000.000 từ tài khoản A sang B gồm hai UPDATE. Nếu máy chủ mất điện sau lệnh trừ mà chưa kịp lệnh cộng, khách hàng mất tiền. Ta muốn hai việc hoặc cùng thành công, hoặc cùng không xảy ra. Transaction gom nhiều lệnh thành một đơn vị công việc không thể chia nhỏ.
4.2. BEGIN, COMMIT, ROLLBACK
| Thao tác | SQL Server | MySQL |
|---|---|---|
| Bắt đầu | BEGIN TRANSACTION |
START TRANSACTION (hoặc BEGIN) |
| Chốt (lưu vĩnh viễn) | COMMIT TRANSACTION |
COMMIT |
| Hủy (quay về trước khi bắt đầu) | ROLLBACK TRANSACTION |
ROLLBACK |
Hình dưới đây mô tả vòng đời của một transaction: mở bằng BEGIN, chạy các lệnh, rồi kết thúc bằng COMMIT (chốt tất cả) hoặc ROLLBACK (hủy tất cả); có thể đặt SAVEPOINT để hủy một phần.
Các lệnh trong transaction hoặc cùng thành công (COMMIT) hoặc cùng bị hủy (ROLLBACK) — đó là tính nguyên tử (Atomicity) của ACID. SAVEPOINT cho phép hủy một phần.
Cách đọc hình: đi từ trên xuống. Mọi thay đổi giữa BEGIN và COMMIT chỉ là tạm thời với các phiên khác. Nếu mọi lệnh thành công và kết quả đúng kỳ vọng, nhánh COMMIT biến chúng thành vĩnh viễn. Nếu có lỗi hoặc kết quả sai, nhánh ROLLBACK đưa dữ liệu về đúng trạng thái trước BEGIN. Nếu có điểm lưu, ROLLBACK đến điểm lưu chỉ hủy phần sau điểm đó.
Chuyển tiền thành công — bài 6.4a. Sau khi COMMIT, số dư mới được giữ lại:
SQL Server:
BEGIN TRANSACTION;
UPDATE Accounts SET balance = balance - 10000000 WHERE account_id = 1;
UPDATE Accounts SET balance = balance + 10000000 WHERE account_id = 2;
COMMIT TRANSACTION;
SELECT account_id, balance FROM Accounts WHERE account_id IN (1, 2) ORDER BY account_id;MySQL:
START TRANSACTION;
UPDATE Accounts SET balance = balance - 10000000 WHERE account_id = 1;
UPDATE Accounts SET balance = balance + 10000000 WHERE account_id = 2;
COMMIT;
SELECT account_id, balance FROM Accounts WHERE account_id IN (1, 2) ORDER BY account_id;Tài khoản 1: 67.000.000 → 57.000.000; tài khoản 2: 8.000.000 → 18.000.000; tổng số dư không đổi (tính bảo toàn).
Hủy giữa chừng bằng ROLLBACK — số dư trở về như cũ:
BEGIN TRANSACTION;
UPDATE Accounts SET balance = 0 WHERE account_id = 1;
ROLLBACK TRANSACTION;
SELECT balance FROM Accounts WHERE account_id = 1;START TRANSACTION;
UPDATE Accounts SET balance = 0 WHERE account_id = 1;
ROLLBACK;
SELECT balance FROM Accounts WHERE account_id = 1;Kết quả 67000000.00 (không đổi).
Lưu ý: cả hai hệ chạy ở chế độ autocommit — mỗi câu lệnh riêng lẻ tự COMMIT ngay. Bạn chỉ có khả năng
ROLLBACKkhi đã mở transaction tường minh. Trên Codelab, runner cũng chạy ở chế độ autocommit, vì vậyBEGIN/COMMIT/ROLLBACKtrong script của bạn có tác dụng thật.
Bài 6.4b cho bạn thử cả hai kết cục trên bảng Orders (kiểm tra cơ chế xóa lệnh): transaction thử nghiệm xóa toàn bộ lệnh PENDING rồi ROLLBACK (17 lệnh vẫn còn), sau đó transaction thật chỉ xóa lệnh PENDING trước 2026-09-01 rồi COMMIT (8 lệnh bị xóa, còn 9).
4.3. Điểm lưu (savepoint) và xử lý lỗi
Savepoint cho phép hủy một phần: chuyển 10.000.000 nhưng "gõ nhầm" bước cộng — hủy riêng bước sai mà vẫn giữ bước trước.
SQL Server — SAVE TRANSACTION / ROLLBACK TRANSACTION tên:
BEGIN TRANSACTION;
UPDATE Accounts SET balance = balance - 10000000 WHERE account_id = 1;
SAVE TRANSACTION diem1;
UPDATE Accounts SET balance = balance + 999 WHERE account_id = 2; -- bước sai
ROLLBACK TRANSACTION diem1; -- chỉ hủy phần sau diem1
COMMIT TRANSACTION;
SELECT account_id, balance FROM Accounts WHERE account_id IN (1, 2) ORDER BY account_id;MySQL — SAVEPOINT / ROLLBACK TO SAVEPOINT:
START TRANSACTION;
UPDATE Accounts SET balance = balance - 10000000 WHERE account_id = 1;
SAVEPOINT diem1;
UPDATE Accounts SET balance = balance + 999 WHERE account_id = 2; -- bước sai
ROLLBACK TO SAVEPOINT diem1; -- chỉ hủy phần sau diem1
COMMIT;
SELECT account_id, balance FROM Accounts WHERE account_id IN (1, 2) ORDER BY account_id;Kết quả: tài khoản 1 còn 57.000.000 (bước trừ được giữ), tài khoản 2 vẫn 8.000.000 (bước sai bị hủy).
- SQL Server thường viết trong
BEGIN TRY ... END TRY BEGIN CATCH ROLLBACK ... END CATCHvà bậtSET XACT_ABORT ONđể bất kỳ lỗi nào cũng tự hủy toàn bộ transaction. MySQL xử lý lỗi bằngDECLARE EXIT HANDLER FOR SQLEXCEPTIONtrong stored procedure (Buổi sau). - Không để transaction mở quá lâu: nó giữ khóa (lock) trên các dòng đã sửa, người khác phải chờ. Giữ transaction ngắn, chỉ chứa các lệnh cần thiết.
Mẹo: mô phỏng hai phiên chờ nhau/deadlock không chấm tự động được trên Codelab. Bạn có thể tự thử trong SSMS bằng hai cửa sổ Query: cửa sổ 1 chạy
BEGIN TRANSACTION; UPDATE Accounts SET balance = balance + 1 WHERE account_id = 1;(chưa COMMIT); cửa sổ 2 chạySELECT balance FROM Accounts WHERE account_id = 1;— truy vấn sẽ treo chờ cho đến khi cửa sổ 1COMMIThoặcROLLBACK. Nhớ đóng transaction dở dang sau khi thử.
4.4. ACID — bốn tính chất của transaction
| Tính chất | Ý nghĩa | Ví dụ ngành chứng khoán |
|---|---|---|
| Atomicity (nguyên tử) | tất cả hoặc không gì cả | khớp lệnh: cập nhật lệnh + ghi giao dịch + trừ tiền + cộng cổ phiếu — thiếu một bước là sai |
| Consistency (nhất quán) | dữ liệu luôn thỏa ràng buộc và quy tắc nghiệp vụ | tổng số dư bảo toàn; không ghi giao dịch cho lệnh không tồn tại |
| Isolation (cô lập) | transaction đồng thời không "nhìn thấy" trạng thái dở dang của nhau | hai nhân viên cùng sửa số dư một tài khoản không làm mất cập nhật của nhau |
| Durability (bền vững) | đã COMMIT thì không mất, kể cả khi sập nguồn |
lệnh đã khớp phải còn sau khi khởi động lại máy chủ |
SQL Server vs MySQL: mức cô lập mặc định của SQL Server là
READ COMMITTED; của MySQL (InnoDB) làREPEATABLE READ. Bạn sẽ gặp lại khi học về hiệu năng và khóa.
4.5. Ứng dụng: khớp lệnh nguyên tử
Bài 6.4c gói bốn thao tác của việc khớp lệnh mua 27 (tài khoản 62 mua 200 VCS giá 60.000) vào một transaction: đổi trạng thái lệnh, ghi giao dịch, trừ 12.000.000, tăng danh mục từ 100 lên 300 cổ phiếu. Mọi số liệu lấy bằng subquery từ chính lệnh 27. Ví dụ SQL Server (MySQL: đổi BEGIN TRANSACTION thành START TRANSACTION, COMMIT TRANSACTION thành COMMIT):
BEGIN TRANSACTION;
UPDATE Orders SET status = 'MATCHED' WHERE order_id = 27;
INSERT INTO Trades (trade_id, order_id, symbol, quantity, price, trade_date)
SELECT 87, order_id, symbol, quantity, price, '2026-09-25' FROM Orders WHERE order_id = 27;
UPDATE Accounts
SET balance = balance - (SELECT quantity * price FROM Orders WHERE order_id = 27)
WHERE account_id = (SELECT account_id FROM Orders WHERE order_id = 27);
UPDATE Portfolio
SET quantity = quantity + (SELECT quantity FROM Orders WHERE order_id = 27)
WHERE account_id = (SELECT account_id FROM Orders WHERE order_id = 27)
AND symbol = (SELECT symbol FROM Orders WHERE order_id = 27);
COMMIT TRANSACTION;
SELECT (SELECT status FROM Orders WHERE order_id = 27) AS order_status,
(SELECT balance FROM Accounts WHERE account_id = 62) AS balance,
(SELECT quantity FROM Portfolio WHERE account_id = 62 AND symbol = 'VCS') AS vcs_quantity;Cùng ví dụ trên MySQL:
START TRANSACTION;
UPDATE Orders SET status = 'MATCHED' WHERE order_id = 27;
INSERT INTO Trades (trade_id, order_id, symbol, quantity, price, trade_date)
SELECT 87, order_id, symbol, quantity, price, '2026-09-25' FROM Orders WHERE order_id = 27;
UPDATE Accounts
SET balance = balance - (SELECT quantity * price FROM Orders WHERE order_id = 27)
WHERE account_id = (SELECT account_id FROM Orders WHERE order_id = 27);
UPDATE Portfolio
SET quantity = quantity + (SELECT quantity FROM Orders WHERE order_id = 27)
WHERE account_id = (SELECT account_id FROM Orders WHERE order_id = 27)
AND symbol = (SELECT symbol FROM Orders WHERE order_id = 27);
COMMIT;
SELECT (SELECT status FROM Orders WHERE order_id = 27) AS order_status,
(SELECT balance FROM Accounts WHERE account_id = 62) AS balance,
(SELECT quantity FROM Portfolio WHERE account_id = 62 AND symbol = 'VCS') AS vcs_quantity;Lưu ý: hãy để ý một điểm tinh tế: bước 1 đổi trạng thái lệnh trước, nhưng các bước sau vẫn đọc được
quantity,price,account_idtừ cùng dòng đó (chỉstatusbị đổi). Nếu thay vìUPDATE Ordersbạn đổi giá hay số lượng ở bước đầu thì các bước sau sẽ đọc phải giá trị mới — luôn đọc dữ liệu nguồn trước khi sửa nó.
Vừa học xong transaction và ACID, hãy làm 3 bài 6.4a → 6.4c: chuyển tiền giữa hai tài khoản, thử ROLLBACK/COMMIT khi xóa lệnh chờ khớp, và khớp lệnh mua trong một transaction nguyên tử.
5. Best practices và bảo mật
5.1. Backup trước khi thao tác
- Trước đợt thay đổi lớn, có bản sao lưu đã kiểm chứng phục hồi được.
- Cho các thay đổi nhỏ: sao lưu tại chỗ phần dữ liệu sắp sửa bằng
INSERT ... SELECTvào bảng lưu trữ (bài 6.1c), hoặcSELECT ... INTO backup_20260925 FROM .... - Backup chỉ có giá trị khi đã thử restore. Hệ thống thật của công ty (xem tài liệu vận hành) có lịch backup hằng ngày; DBA chịu trách nhiệm, còn bạn chịu trách nhiệm không làm sai trước tiên.
| SQL Server | MySQL | |
|---|---|---|
| Sao lưu toàn bộ | BACKUP DATABASE hnx_securities TO DISK = 'D:\backup\hnx.bak' |
mysqldump hnx_securities > hnx.sql |
| Sao lưu thay đổi | differential + log backup | binary log |
| Phục hồi | RESTORE DATABASE ... (WITH STOPAT để về đúng thời điểm) |
nạp lại file dump, áp binary log đến thời điểm cần |
5.2. SELECT trước UPDATE/DELETE
Quy trình 5 bước cho mọi thay đổi dữ liệu trên môi trường thật:
- Viết
SELECTvới đúngWHERE→ xem các dòng sẽ bị tác động, đếm số dòng. - Mở transaction (
BEGIN TRANSACTION/START TRANSACTION). - Đổi
SELECTthànhUPDATE/DELETE(giữ nguyênWHERE). - Kiểm tra số dòng bị ảnh hưởng, rồi
SELECTlại để xem kết quả. - Đúng như kỳ vọng →
COMMIT; khác kỳ vọng →ROLLBACK.
Bước 4 dùng hàm khác nhau ở hai hệ. Ví dụ cộng 1 đồng cho hai tài khoản và đọc số dòng bị ảnh hưởng (kết quả 2):
UPDATE Accounts SET balance = balance + 1 WHERE account_id IN (1, 2);
SELECT @@ROWCOUNT AS so_dong_bi_anh_huong;UPDATE Accounts SET balance = balance + 1 WHERE account_id IN (1, 2);
SELECT ROW_COUNT() AS so_dong_bi_anh_huong;Mẹo: nếu con số này khác số bạn đếm ở bước 1 (ví dụ kỳ vọng 4 dòng nhưng ra 4.000), hãy
ROLLBACKngay. Thêm: chạy lần đầu trên bản sao/bảng thử nghiệm; không bao giờ thử trên production; luôn liệt kê cột trongINSERT; đọc kỹ lại toàn bộ câu lệnh (đúng bảng, đúngWHERE, đúng cơ sở dữ liệu) trước khi bấm chạy.
5.3. Quản lý quyền truy cập
Nguyên tắc đặc quyền tối thiểu (least privilege): mỗi người chỉ có quyền đủ để làm việc của mình.
| Vai trò | Quyền điển hình |
|---|---|
| Nhà phân tích / báo cáo | chỉ SELECT |
| Nhân viên vận hành back-office | SELECT, INSERT, UPDATE trên một số bảng; không DELETE/TRUNCATE |
| DBA | toàn quyền, thao tác có ghi nhận |
SQL Server:
CREATE ROLE role_backoffice;
GRANT SELECT, INSERT, UPDATE ON dbo.Orders TO role_backoffice;
DENY DELETE ON dbo.Orders TO role_backoffice;MySQL:
CREATE USER 'ops'@'%' IDENTIFIED BY '...';
GRANT SELECT, INSERT, UPDATE ON hnx_securities.Orders TO 'ops'@'%';Bổ sung: tài khoản dùng chung cho ứng dụng không nên là sa/root; ghi nhật ký kiểm toán (như bảng AuditLog) cho mọi thay đổi trạng thái quan trọng.
Cảnh báo: ứng dụng gọi SQL phải dùng câu lệnh tham số hóa (parameterized query), không ghép chuỗi từ dữ liệu người dùng — nếu không sẽ bị SQL injection, kẻ tấn công có thể chèn thêm
DELETE/DROPvào câu lệnh của bạn.
Sai lầm thường gặp
| Sai lầm | Hậu quả | Cách đúng |
|---|---|---|
UPDATE/DELETE không có WHERE (hoặc WHERE sai) |
tác động toàn bảng | viết SELECT cùng WHERE trước; dùng transaction |
INSERT không liệt kê cột; số giá trị khác số cột |
chèn nhầm cột hoặc hỏng khi bảng đổi cấu trúc | luôn INSERT INTO t (cột...) VALUES (...) |
| Thêm bảng con trước bảng cha; xóa bảng cha trước bảng con | vi phạm khóa ngoại | thêm cha → con; xóa con → cha |
| Xóa hàng loạt rồi mới nhận ra tiêu chí lọc phụ thuộc dữ liệu vừa xóa (bài 6.3c) | không tính lại được điều kiện | chốt danh sách vào bảng tạm trước |
Gán = NULL khi so sánh |
không dòng nào khớp | IS NULL để so sánh, SET cột = NULL để gán |
Dùng TRUNCATE khi còn cần WHERE, hoặc bảng có khóa ngoại tham chiếu |
mất hết dữ liệu / lỗi | dùng DELETE ... WHERE |
Quên COMMIT hoặc để transaction mở |
giữ khóa, chặn người khác | giữ transaction ngắn, luôn kết thúc bằng COMMIT/ROLLBACK |
Tin rằng ROLLBACK cứu được mọi thứ |
MySQL không hoàn tác DDL; số tự tăng đã cấp không trả lại | không đặt DDL giữa transaction; đừng dùng số tự tăng làm số liên tục |
Dùng GETDATE()/NOW() cho "hôm nay" |
kết quả đổi theo ngày, chấm sai | dùng ngày cố định '2026-09-25' |
Tóm tắt
| Chủ đề | Ghi nhớ |
|---|---|
| INSERT | luôn liệt kê cột; nhiều dòng bằng VALUES (...), (...); INSERT ... SELECT để nhân bản; cha trước con |
| IDENTITY / AUTO_INCREMENT | tự cấp số, không tái sử dụng; DELETE không đặt lại, TRUNCATE đặt lại |
| UPDATE | SET a = ..., b = ...; luôn WHERE; cộng dồn balance = balance + x; JOIN khác cú pháp giữa hai hệ |
| DELETE | từ con lên cha; chốt danh sách trước khi xóa nhiều tầng |
| DELETE vs TRUNCATE | TRUNCATE: nhanh, không WHERE, reset tự tăng; MySQL không rollback được |
| Transaction | BEGIN/START TRANSACTION → COMMIT/ROLLBACK; mặc định là autocommit |
| ACID | Atomicity, Consistency, Isolation, Durability |
| Best practices | backup, SELECT trước, transaction, @@ROWCOUNT/ROW_COUNT(), quyền tối thiểu |
Gợi ý mở rộng
- Viết
TRY...CATCH(SQL Server) bọc bài 6.4a để tựROLLBACKkhi số dư sau khi trừ bị âm. - Tạo trigger tự ghi
AuditLogmỗi khiCustomers.statusđổi (Buổi về trigger). - Thử
SET IDENTITY_INSERTvà quan sát cách cột tự tăng tiếp nối. - Mở hai cửa sổ Query trong SSMS để quan sát khóa và mức cô lập.
- Thiết kế chính sách xóa mềm cho
Customers(cộtis_deleted) và cập nhật các truy vấn báo cáo tương ứng.