Change Tracking và CDC: 302 lần sửa một sản phẩm thành 1 dòng hay 604 dòng
Change Tracking cho biết khóa nào đã đổi, CDC giữ mọi lần đổi kèm giá trị cũ và mới. Đo trên SQL Server 2019: số dòng, chi phí ghi, độ trễ, rủi ro vận hành.
Mục lục
- 1. Hai câu hỏi về cùng một dòng
- 2. Change Tracking: khóa nào đã đổi từ mốc X
- 3. CDC: đọc lại từng lần đổi từ transaction log
- 4. 302 lần sửa SP-00777: mỗi cách trả mấy dòng
- 5. Chi phí khi ghi, độ trễ, và khi phía đọc dừng
- 6. Consumer: lưu mốc cùng giao dịch với dữ liệu đích
- 7. Chọn cách nào
- Những chỗ hay hiểu sai
- Đọc tiếp
- Nguồn
20:00 thứ Sáu 2026-09-18, BanHang hạ giá SP-00777 từ 8.990.000 xuống 4.990.000 cho đợt flash sale 300 máy, rồi trả giá cũ lúc 20:40. Trong 40 phút đó, dòng SanPhamId = 777 của dbo.SanPham bị sửa 302 lần: hai lần đổi giá và 300 lần trừ kho. Trang sản phẩm và bộ tìm kiếm chỉ cần giá và tồn hiện tại của sản phẩm vừa đổi, nhưng đang quét lại cả 5.000 sản phẩm mỗi phút. Kho dữ liệu kế toán cần cả 40 phút giá 4.990.000, vì 300 đơn bán ở giá đó, trong khi ảnh chụp cuối ngày chỉ thấy 8.990.000. SQL Server có hai tính năng cho hai câu hỏi này: Change Tracking (CT) và Change Data Capture (CDC). Bài này đo cả hai trên cùng một máy: mỗi cách trả mấy dòng, tốn thêm bao nhiêu khi ghi, trễ bao lâu, hỏng thế nào khi phía đọc dừng.
Đọc nhanh
- CT ghi khóa của dòng đã đổi ngay trong giao dịch DML. 302 lần sửa
SP-00777ra đúng 1 dòng; giá trị đọc từ bảng nguồn. Có trên mọi edition, kể cả Express. - CDC dùng capture job của SQL Server Agent đọc lại transaction log, giữ ảnh trước và sau: 302 lần sửa thành 604 dòng. Cần Standard hoặc Enterprise. Với
pollingintervalmặc định 5 giây, thay đổi hiện ra sau trung vị 2,4 giây. - 10.000 đơn: CT thêm 2% log ghi xuống đĩa, CDC thêm 39% vì capture job chép lại từng thay đổi. Thông lượng gần như không đổi vì commit chờ đĩa log.
- Capture dừng thì log không cắt được: 30.000 đơn giữ thêm 126 MiB qua hai lần log backup,
log_reuse_wait_desc = REPLICATION. Consumer CT ngừng quá retention thì phải tải lại toàn bộ. Consumer nào cũng lưu mốc cùng giao dịch với dữ liệu đích.
1. Hai câu hỏi về cùng một dòng
| Trang sản phẩm, bộ tìm kiếm | Kho kế toán, đối soát | |
|---|---|---|
| Cần biết | Sản phẩm nào đã đổi, giá và tồn hiện tại | Mọi lần đổi giá, mọi trạng thái đơn đi qua, giá trị trước và sau |
| Bỏ bước giữa được không | Được, chỉ giá trị cuối có nghĩa | Không, 40 phút giá 4.990.000 phải còn |
| Cơ chế hợp | Change Tracking | CDC |
Hai cơ chế khác nhau ở thời điểm ghi nhận. CT ghi trong chính giao dịch UPDATE: commit xong là CHANGETABLE thấy ngay, giao dịch rollback không để lại gì. CDC không đụng vào giao dịch ghi: capture job đọc transaction log về sau, theo từng vòng quét, bằng cùng bộ đọc log (sp_replcmds) với transactional replication, rồi chép dòng đổi vào change table riêng.
2. Change Tracking: khóa nào đã đổi từ mốc X
Bật ở mức database rồi từng bảng. Từ đó, mỗi dòng bị INSERT, UPDATE, DELETE thêm một dòng vào bảng nội bộ change_tracking_<object_id> của bảng đó: khóa chính cộng một số phiên bản, và với TRACK_COLUMNS_UPDATED = ON thêm 4 byte cho mỗi cột bị sửa. Mỗi giao dịch commit thêm một dòng vào sys.syscommittab. Không có giá trị cột nào được chép. Microsoft so chi phí này với việc giữ thêm một chỉ mục trên bảng, đúng loại chi phí ở mục 8 của chương chỉ mục. Script dưới chạy trên SQL Server 2019 Developer trong container Linux (môi trường ở mục 5). Bảng dbo.DonHang (cùng pf_DonHang_Ngay, ps_DonHang_Ngay) và dbo.SanPham giữ nguyên định nghĩa của BanHang ở chương lưu trữ và bài flash sale; đường dẫn file là của container.
-- sqlcmd -S localhost,14330 -U sa -P "<mật khẩu>" -C -f 65001 -i 0-tao.sql
CREATE DATABASE Kumeo_cdcct COLLATE Vietnamese_100_CI_AS;
GO
ALTER DATABASE Kumeo_cdcct ADD FILEGROUP FG_DATA;
ALTER DATABASE Kumeo_cdcct ADD FILEGROUP FG_ARCHIVE;
ALTER DATABASE Kumeo_cdcct ADD FILE (NAME = cdcct_data, FILENAME = '/var/opt/mssql/data/Kumeo_cdcct_data.ndf') TO FILEGROUP FG_DATA;
ALTER DATABASE Kumeo_cdcct ADD FILE (NAME = cdcct_arch, FILENAME = '/var/opt/mssql/data/Kumeo_cdcct_arch.ndf') TO FILEGROUP FG_ARCHIVE;
ALTER DATABASE Kumeo_cdcct MODIFY FILEGROUP FG_DATA DEFAULT;
ALTER DATABASE Kumeo_cdcct SET RECOVERY FULL;
ALTER DATABASE Kumeo_cdcct SET ALLOW_SNAPSHOT_ISOLATION ON;
GO
USE Kumeo_cdcct;
GO
CREATE PARTITION FUNCTION pf_DonHang_Ngay (datetime2(0))
AS RANGE RIGHT FOR VALUES (CAST('20250101' AS datetime2(0)), CAST('20260101' AS datetime2(0)), CAST('20270101' AS datetime2(0)));
CREATE PARTITION SCHEME ps_DonHang_Ngay AS PARTITION pf_DonHang_Ngay TO (FG_ARCHIVE, FG_ARCHIVE, FG_DATA, FG_DATA);
CREATE TABLE dbo.DonHang (
DonHangId bigint NOT NULL,
NgayTao datetime2(0) NOT NULL,
KhachHangId int NOT NULL,
TrangThai tinyint NOT NULL,
TongTien decimal(18, 2) NOT NULL,
CONSTRAINT PK_DonHang PRIMARY KEY CLUSTERED (NgayTao, DonHangId)
) ON ps_DonHang_Ngay (NgayTao);
CREATE INDEX IX_DonHang_KhachHang ON dbo.DonHang (KhachHangId, NgayTao) ON ps_DonHang_Ngay (NgayTao);
CREATE TABLE dbo.SanPham (
SanPhamId int NOT NULL CONSTRAINT PK_SanPham PRIMARY KEY CLUSTERED,
MaSanPham varchar(20) NOT NULL CONSTRAINT UQ_SanPham_Ma UNIQUE,
Ten nvarchar(200) NOT NULL,
DonGia decimal(18, 2) NOT NULL,
TonKho int NOT NULL CONSTRAINT CK_SanPham_TonKho CHECK (TonKho >= 0)
);
INSERT INTO dbo.SanPham (SanPhamId, MaSanPham, Ten, DonGia, TonKho) -- 5.000 sản phẩm
SELECT n, CONCAT('SP-', RIGHT(CONCAT('0000', n), 5)), CONCAT(N'Sản phẩm ', n), 1000000 + n % 50 * 100000, 1000000
FROM (SELECT TOP (5000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM sys.all_objects a CROSS JOIN sys.all_objects b) AS s(n);
UPDATE dbo.SanPham SET Ten = N'Điện thoại 128 GB', DonGia = 8990000, TonKho = 300 WHERE SanPhamId = 777;
GO
ALTER DATABASE Kumeo_cdcct SET CHANGE_TRACKING = ON (CHANGE_RETENTION = 2 DAYS, AUTO_CLEANUP = ON);
ALTER TABLE dbo.SanPham ENABLE CHANGE_TRACKING WITH (TRACK_COLUMNS_UPDATED = ON);
ALTER TABLE dbo.DonHang ENABLE CHANGE_TRACKING WITH (TRACK_COLUMNS_UPDATED = ON);
Ba hàm làm hết việc đọc:
| Hàm | Trả về |
|---|---|
CHANGE_TRACKING_CURRENT_VERSION() |
Phiên bản của giao dịch commit gần nhất. Consumer lưu nó làm mốc cho lần sau |
CHANGETABLE(CHANGES t, @moc) |
Một dòng cho mỗi khóa đổi sau @moc: khóa, SYS_CHANGE_VERSION, SYS_CHANGE_OPERATION (I, U, D), SYS_CHANGE_COLUMNS |
CHANGE_TRACKING_MIN_VALID_VERSION(id) |
Mốc nhỏ nhất còn đọc đúng cho bảng có object_id đó. Mốc cũ hơn thì phải tải lại toàn bộ |
Điều kiện và giới hạn:
- Bảng phải có khóa chính; thiếu thì nhận Msg 4997.
- Thông tin đổi giữ theo
CHANGE_RETENTION: mặc định 2 ngày, tối thiểu 1 phút, dọn tự động bật sẵn. Đến SQL Server 2022, luồng dọn chạy 30 phút một lần; SQL Server 2025 dọn bảng phụ lớn theo từng bước. - Microsoft "khuyến nghị mạnh" đọc trong một giao dịch
SNAPSHOT, để mốc,MIN_VALID_VERSIONvàCHANGETABLEcùng thấy một thời điểm dù luồng dọn chạy giữa chừng. Cái giá của snapshot ở mục 8 bài transaction. - Bảng có CT không
SWITCHđược (Msg 4900).dbo.DonHangđẩy năm cũ ra bằng SWITCH, nênBanHangthật chỉ bật CT chodbo.SanPham; database thử bật cả hai để so ở mục 4.TRUNCATE TABLEđẩy mốc tối thiểu lên, mọi consumer phải tải lại. - Có trên mọi edition. 302 lần sửa
SP-00777ở mục 4 chạy trên LocalDB 2019 CU27 (Express) cũng cho đúng 1 dòng CT, cònsys.sp_cdc_enable_dbở đó báo Msg 22988.
3. CDC: đọc lại từng lần đổi từ transaction log
sys.sp_cdc_enable_db (cần sysadmin) tạo schema cdc. sys.sp_cdc_enable_table tạo một capture instance: change table cdc.dbo_SanPham_CT, hàm cdc.fn_cdc_get_all_changes_dbo_SanPham, và với @supports_net_changes = 1 thêm hàm net changes cùng một chỉ mục trên change table. Bảng đầu tiên được bật kéo theo hai job của SQL Server Agent.
USE Kumeo_cdcct;
EXEC sys.sp_cdc_enable_db;
EXEC sys.sp_cdc_enable_table @source_schema = N'dbo', @source_name = N'SanPham', @role_name = NULL, @supports_net_changes = 1;
EXEC sys.sp_cdc_enable_table @source_schema = N'dbo', @source_name = N'DonHang', @role_name = NULL, @supports_net_changes = 1;
EXEC sys.sp_cdc_help_jobs;
| Job | Tham số, đọc từ sys.sp_cdc_help_jobs |
Việc làm |
|---|---|---|
| Capture | maxtrans 500, maxscans 10, continuous 1, pollinginterval 5 |
Quét log tới 10 lần × 500 giao dịch, nghỉ 5 giây, lặp lại. Mỗi vòng commit một giao dịch riêng |
| Cleanup | retention 4320, threshold 5000 |
2:00 mỗi ngày xóa dòng cũ hơn 4.320 phút (3 ngày), mỗi câu DELETE tối đa 5.000 dòng |
Năm cột đầu của change table là metadata: __$start_lsn (LSN lúc commit, xếp thứ tự giao dịch), __$seqval (thứ tự trong một giao dịch), __$operation (1 xóa, 2 thêm, 3 ảnh trước của UPDATE, 4 ảnh sau), __$update_mask (cột bị sửa). Các cột còn lại chép cột của bảng nguồn. cdc.lsn_time_mapping nối LSN với giờ commit, nên sys.fn_cdc_map_time_to_lsn đổi "từ 20:00 đến 20:40" thành khoảng LSN cho hai hàm đọc.
Điều kiện và giới hạn:
- Agent phải chạy. Agent dừng thì capture dừng, và log dồn lại như mục 5. Trên SQL Server 2019, CDC có ở Enterprise và Standard (Developer có đủ tính năng Enterprise), không có ở Web và Express. Bản Linux có CDC từ SQL Server 2017 CU18.
- Mỗi bảng tối đa hai capture instance. Cột thêm vào bảng nguồn sau khi bật không được chép; cột bị xóa thành
NULL. Muốn có cột mới thì tạo instance thứ hai, cho consumer chuyển sang, rồi bỏ instance cũ. SWITCHkhông bị chặn (@allow_partition_switchmặc định 1, lúc bật có cảnh báo) nhưng không được ghi lại.SWITCHpartition 2024 có 3 đơn ra bảng staging:_CTkhông có dòng xóa nào. Với kho kế toán, đẩy năm cũ ra lưu trữ không phải xóa nghiệp vụ nên thường đúng ý; consumer coi_CTlà đủ mọiDELETEthì phải biết lịchSWITCH.- Azure SQL Managed Instance chạy CDC bằng Agent như bản tự cài. Azure SQL Database thay Agent bằng bộ lập lịch: capture mỗi 20 giây, cleanup mỗi giờ, không chỉnh được
pollinginterval, cần vCore hoặc DTU từ S3, không có SLA cho độ trễ. Bật CDC còn tắt việc cắt log sớm của ADR, như chương Azure SQL đã nhắc.
4. 302 lần sửa SP-00777: mỗi cách trả mấy dòng
Script dưới chạy lại đợt sale, mỗi câu một giao dịch như endpoint thật, rồi đi hết vòng đời của đơn 10042: tạo 11:58 với 1.500.000, sửa thành 1.750.000, qua trạng thái 2, 3, 4.
USE Kumeo_cdcct;
SET NOCOUNT ON;
DECLARE @v0 bigint = CHANGE_TRACKING_CURRENT_VERSION(), @i int = 0;
UPDATE dbo.SanPham SET DonGia = 4990000 WHERE SanPhamId = 777; -- 20:00
WHILE @i < 300
BEGIN
UPDATE dbo.SanPham SET TonKho = TonKho - 1 WHERE SanPhamId = 777 AND TonKho >= 1;
SET @i += 1;
END;
UPDATE dbo.SanPham SET DonGia = 8990000 WHERE SanPhamId = 777; -- 20:40
INSERT INTO dbo.DonHang VALUES (10042, '2026-10-02T11:58:00', 7, 1, 1500000);
UPDATE dbo.DonHang SET TongTien = 1750000 WHERE NgayTao = '2026-10-02T11:58:00' AND DonHangId = 10042;
UPDATE dbo.DonHang SET TrangThai = 2 WHERE NgayTao = '2026-10-02T11:58:00' AND DonHangId = 10042;
UPDATE dbo.DonHang SET TrangThai = 3 WHERE NgayTao = '2026-10-02T11:58:00' AND DonHangId = 10042;
UPDATE dbo.DonHang SET TrangThai = 4 WHERE NgayTao = '2026-10-02T11:58:00' AND DonHangId = 10042;
WAITFOR DELAY '00:00:15'; -- ba vòng quét của capture job
DECLARE @den binary(10) = sys.fn_cdc_get_max_lsn(), @sp binary(10) = sys.fn_cdc_get_min_lsn('dbo_SanPham'), @dh binary(10) = sys.fn_cdc_get_min_lsn('dbo_DonHang');
SELECT 'SanPham' AS bang,
(SELECT COUNT(*) FROM CHANGETABLE(CHANGES dbo.SanPham, @v0) AS c) AS ct,
(SELECT COUNT(*) FROM cdc.fn_cdc_get_net_changes_dbo_SanPham(@sp, @den, N'all')) AS cdc_net,
(SELECT COUNT(*) FROM cdc.fn_cdc_get_all_changes_dbo_SanPham(@sp, @den, N'all')) AS cdc_all,
(SELECT COUNT(*) FROM cdc.fn_cdc_get_all_changes_dbo_SanPham(@sp, @den, N'all update old')) AS cdc_all_old
UNION ALL
SELECT 'DonHang',
(SELECT COUNT(*) FROM CHANGETABLE(CHANGES dbo.DonHang, @v0) AS c),
(SELECT COUNT(*) FROM cdc.fn_cdc_get_net_changes_dbo_DonHang(@dh, @den, N'all')),
(SELECT COUNT(*) FROM cdc.fn_cdc_get_all_changes_dbo_DonHang(@dh, @den, N'all')),
(SELECT COUNT(*) FROM cdc.fn_cdc_get_all_changes_dbo_DonHang(@dh, @den, N'all update old'));
| Cách đọc | SP-00777, 302 lần sửa |
Đơn 10042, thêm rồi sửa 4 lần |
|---|---|---|
CT, CHANGETABLE(CHANGES ...) |
1 dòng, U, version 302 |
1 dòng, I |
| CDC net changes | 1 | 1, __$operation 2 với giá trị cuối |
CDC all changes, all |
302, chỉ ảnh sau | 5 |
CDC all changes, all update old |
604 | 9 |
Dòng trong bảng phụ: bảng nội bộ CT, _CT của CDC |
302, 604 | 5, 9 |
Dòng CT của SP-00777 có SYS_CHANGE_COLUMNS đánh dấu cả DonGia lẫn TonKho, nhưng không còn dấu vết của giá 4.990.000: JOIN sang dbo.SanPham chỉ thấy 8.990.000 và tồn 0. Đơn 10042 hiện là I vì được tạo sau mốc; trạng thái 2 và 3 biến mất. Với trang sản phẩm, đó đúng là thứ cần: một lần làm mới thay cho 302. Với kế toán, chỉ all update old giữ được 1.500.000 → 1.750.000 và 1 → 2 → 3 → 4. Hàng cuối hay bị bỏ qua: CT trả 1 dòng nhưng bảng nội bộ vẫn giữ 302 dòng tới khi bị dọn sau retention; net changes chỉ là cách đọc, _CT vẫn lưu đủ 604 dòng trong 3 ngày.
5. Chi phí khi ghi, độ trễ, và khi phía đọc dừng
Môi trường đo: SQL Server 2019 Developer, bản RTM-CU32-GDR (15.0.4490.9), trong container Linux giới hạn 4 GB RAM, trên laptop Intel Core Ultra 5 125U chạy Windows 11 đang làm việc khác. Thời gian dao động giữa các lần chạy; số đếm và số byte ổn định hơn.
Chi phí khi ghi
Mỗi lần đo tạo một database mới cùng filegroup và bảng như mục 2 nhưng không bật snapshot isolation, rồi bật một trong ba chế độ. Một phiên ghi 10.000 đơn, mỗi đơn một giao dịch: INSERT vào dbo.DonHang và trừ kho một trong 5.000 sản phẩm chọn bằng Random(42). Harness là một file .NET 10 dùng Microsoft.Data.SqlClient 7.1.1. Log của một giao dịch đọc từ sys.dm_tran_database_transactions trước khi commit, trung bình 50 đơn. Log ghi xuống đĩa đọc từ bộ đếm Log Bytes Flushed/sec của database, gồm cả phần capture job ghi vào _CT. Mỗi chế độ 3 lần, lấy trung vị.
| 10.000 đơn, trung vị 3 lần | Không theo dõi | CT | CDC |
|---|---|---|---|
| Đơn mỗi giây | 75 | 75 | 77 |
| Log của một giao dịch ghi đơn | 522 B | 848 B | 641 B |
| Log ghi xuống đĩa, gồm cả capture job | 39,3 MiB | 40,0 MiB | 54,5 MiB |
| Bảng phụ sau khi ghi | 0 | 2,1 MiB | 6,0 MiB |
Thông lượng gần như bằng nhau vì mỗi commit chờ log xuống đĩa. 39,3 MiB cho 10.000 đơn là khoảng 4 KB mỗi commit dù giao dịch chỉ sinh 522 B log. Cách giải thích hợp với số đo, nhưng bài chưa kiểm bằng tài liệu: mỗi commit ghi trọn một khối log, làm tròn theo kích thước sector của đĩa. CT ghi thông tin đổi ngay trong giao dịch, thêm 326 B (+62%), nhưng phần thêm nằm gọn trong khối đã phải ghi nên tổng log chỉ thêm 2%. CDC thêm 119 B vào giao dịch ghi; chi phí chính đến sau, khi capture job chép 30.000 dòng (10.000 ảnh thêm, 20.000 ảnh trước và sau) vào _CT: tổng log +39%, bảng phụ gần gấp ba CT. Capture đọc kịp trung vị 4,9 giây sau đơn cuối.
Độ trễ của CDC
CT không có độ trễ: thông tin đổi commit cùng dòng dữ liệu. Với CDC, một phiên sửa DonGia của sản phẩm 42 bốn mươi lần, cách nhau 0,3 đến 1,7 giây (Random(42)); phiên thứ hai đọc liên tục cdc.dbo_SanPham_CT ở mức SNAPSHOT. Độ trễ tính từ lúc UPDATE trả về tới lúc phiên đọc thấy giá mới. Job chỉ đọc cấu hình lúc khởi động, nên đổi tham số xong phải dừng rồi chạy lại:
USE Kumeo_cdcct;
EXEC sys.sp_cdc_change_job @job_type = N'capture', @pollinginterval = 1;
EXEC sys.sp_cdc_stop_job @job_type = N'capture';
WAITFOR DELAY '00:00:05';
EXEC sys.sp_cdc_start_job @job_type = N'capture';
pollinginterval |
Trung vị | p95 | Lâu nhất | CPU của job khi rảnh |
|---|---|---|---|---|
| 5, mặc định | 2,4 s | 5,0 s | 5,2 s | khoảng 2 ms mỗi giây |
| 1 | 0,53 s | 1,0 s | 1,06 s | 9 ms mỗi giây |
| 0 | 16 ms | 34 ms | 59 ms | 840 đến 845 ms mỗi giây |
Độ trễ CDC đi theo pollinginterval: 5 giây cho trung vị 2,4 s, 0 giây cho 16 ms
Bảng số liệu
| Trung vị | p95 | |
|---|---|---|
| pollinginterval 5 | 2.417 ms | 4.991 ms |
| pollinginterval 1 | 529 ms | 998 ms |
| pollinginterval 0 | 16 ms | 34 ms |
Độ trễ rải gần đều từ 0 tới đúng pollinginterval: thay đổi chờ vòng quét kế tiếp. Mức 0 cho vài chục mili giây nhưng job quét liên tục, ăn khoảng 0,84 giây CPU mỗi giây kể cả khi không ai ghi. Cột CPU là cpu_time của request job trong 20 giây rảnh, đo 1 lần ở mức 5, 2 lần ở mức 1 và 0. Trang sản phẩm chịu được 5 giây; kho kế toán nạp theo giờ thì không cần nghĩ tới. Trên production, sys.dm_cdc_log_scan_sessions ghi từng vòng quét, cột latency là số giây từ commit cuối được chép tới lúc vòng quét xong.
Khi capture job dừng
Giả sử Agent bị dừng để vá máy chiều thứ Sáu và quên bật lại tới sáng thứ Hai. Thử trên Kumeo_cdcct, recovery model FULL, log cấp sẵn 1 GB: dừng capture job, ghi 30.000 đơn bằng vòng lặp T-SQL, mỗi đơn một giao dịch như trên (khoảng 2,3 ngày đơn của BanHang, chỉ tính đơn và trừ kho), chạy log backup, rồi bật lại job. Log được giải phóng theo từng VLF, ở đây khoảng 127 MiB, nên con số trước khi dừng chưa về 0.
| Bước | Log đang dùng | log_reuse_wait_desc ngay sau log backup |
|---|---|---|
| Trước khi dừng capture | 79,8 MiB | NOTHING |
| Capture dừng, 30.000 đơn, log backup | 206,2 MiB | REPLICATION |
| Log backup thêm một lần | 209,5 MiB | REPLICATION |
| Bật lại job, đọc kịp sau 31,4 giây, log backup | 4,4 MiB | NOTHING |
Log backup vẫn chạy, vẫn chép log ra file, nhưng không giải phóng được phần capture chưa đọc. Lý do là REPLICATION vì CDC dùng chung bộ đọc log với transactional replication. Theo tài liệu, ở recovery model SIMPLE cũng vậy: CHECKPOINT không cắt được log khi capture chưa đọc. Trong BanHang, log lớn dần tới khi ổ L: đầy dù lịch log backup 15 phút của chương lưu trữ vẫn báo thành công. CT không có rủi ro này vì thông tin đổi nằm trong chính giao dịch. Cột log_reuse_wait_desc chỉ ghi lý do ở lần kiểm gần nhất, nên một lần đọc có thể ra LOG_BACKUP hay NOTHING; giám sát cần đọc lặp lại, kèm số log đang dùng:
USE Kumeo_cdcct;
SELECT d.log_reuse_wait_desc, u.used_log_space_in_bytes / 1048576 AS log_dung_mib
FROM sys.databases AS d CROSS JOIN sys.dm_db_log_space_usage AS u
WHERE d.database_id = DB_ID();
Khi consumer CT ngừng quá retention
Consumer CT ở mục 6 đang có mốc 63.675. Để khỏi chờ 2 ngày, hạ CHANGE_RETENTION xuống 1 phút, sửa vài dòng, chờ hơn 1 phút, rồi gọi sys.sp_flush_CT_internal_table_on_demand thay cho luồng dọn tự động. Thủ tục báo watermark 63.676 và xóa hơn 60.000 dòng cũ ở mỗi bảng nội bộ. CHANGE_TRACKING_MIN_VALID_VERSION của dbo.SanPham thành 63.676, lớn hơn mốc, nên lần chạy kế tiếp của consumer đi vào nhánh tải lại: 5.000 dòng.
Tải lại 5.000 sản phẩm là chuyện nhỏ, tải lại 10 triệu đơn thì không. Retention phải dài hơn lần ngừng lâu nhất của consumer, kể cả một cuối tuần, kèm cảnh báo khi mốc gần chạm MIN_VALID_VERSION. CT không giữ log khi consumer ngừng; thứ mất là thông tin đổi, bị dọn theo lịch dù consumer đã đọc hay chưa. CDC có giới hạn tương tự: theo tài liệu, mốc nhỏ hơn sys.fn_cdc_get_min_lsn nghĩa là cleanup đã xóa phần chưa đọc, và consumer ở mục 6 dừng với lỗi 50002 thay vì lặng lẽ bỏ qua.
6. Consumer: lưu mốc cùng giao dịch với dữ liệu đích
Consumer kéo một lô, ghi vào đích, rồi lưu mốc, cả ba trong một giao dịch. Lưu mốc trước rồi sập thì mất lô đó; ghi đích trước, lưu mốc sau rồi sập thì lần sau ghi lại cả lô. Phía ghi đích phải chịu được việc chạy lại, đúng lập luận của consumer idempotent trong bài hai vị tướng. Ví dụ đặt đích ở schema dich cùng database cho chạy được; đích ở hệ khác thì lưu mốc ngay trong hệ đó.
USE Kumeo_cdcct;
GO
CREATE SCHEMA dich;
GO
CREATE TABLE dich.MocDongBo (Nguon varchar(30) NOT NULL PRIMARY KEY, PhienBanCT bigint NULL, LsnCDC binary(10) NULL);
CREATE TABLE dich.SanPhamTimKiem (SanPhamId int NOT NULL PRIMARY KEY, DonGia decimal(18, 2) NOT NULL, TonKho int NOT NULL);
CREATE TABLE dich.LichSuGia (Lsn binary(10) NOT NULL, SeqVal binary(10) NOT NULL, SanPhamId int NOT NULL,
GiaCu decimal(18, 2) NOT NULL, GiaMoi decimal(18, 2) NOT NULL, LucDoi datetime NOT NULL, PRIMARY KEY (Lsn, SeqVal, SanPhamId));
INSERT INTO dich.MocDongBo (Nguon) VALUES ('SanPham');
GO
CREATE OR ALTER PROCEDURE dich.usp_KeoCT -- bản sao giá và tồn cho trang sản phẩm
AS
BEGIN
SET NOCOUNT, XACT_ABORT ON;
SET TRANSACTION ISOLATION LEVEL SNAPSHOT;
BEGIN TRANSACTION;
DECLARE @tu bigint = (SELECT PhienBanCT FROM dich.MocDongBo WHERE Nguon = 'SanPham');
DECLARE @den bigint = CHANGE_TRACKING_CURRENT_VERSION();
IF @tu IS NULL OR @tu < CHANGE_TRACKING_MIN_VALID_VERSION(OBJECT_ID('dbo.SanPham'))
BEGIN -- lần đầu, hoặc đã ngừng quá retention
DELETE FROM dich.SanPhamTimKiem;
INSERT INTO dich.SanPhamTimKiem SELECT SanPhamId, DonGia, TonKho FROM dbo.SanPham;
SELECT N'tải lại toàn bộ' AS KetQua, @@ROWCOUNT AS SoDong;
END
ELSE
BEGIN
MERGE dich.SanPhamTimKiem AS d
USING (SELECT c.SanPhamId, c.SYS_CHANGE_OPERATION AS Op, s.DonGia, s.TonKho
FROM CHANGETABLE(CHANGES dbo.SanPham, @tu) AS c
LEFT JOIN dbo.SanPham AS s ON s.SanPhamId = c.SanPhamId) AS n
ON d.SanPhamId = n.SanPhamId
WHEN MATCHED AND n.Op = 'D' THEN DELETE
WHEN MATCHED THEN UPDATE SET DonGia = n.DonGia, TonKho = n.TonKho
WHEN NOT MATCHED AND n.Op <> 'D' THEN INSERT VALUES (n.SanPhamId, n.DonGia, n.TonKho);
SELECT N'kéo thay đổi' AS KetQua, @@ROWCOUNT AS SoDong;
END;
UPDATE dich.MocDongBo SET PhienBanCT = @den WHERE Nguon = 'SanPham';
COMMIT;
END;
GO
CREATE OR ALTER PROCEDURE dich.usp_KeoCDC -- lịch sử giá cho kho kế toán
AS
BEGIN
SET NOCOUNT, XACT_ABORT ON;
DECLARE @moc binary(10) = (SELECT LsnCDC FROM dich.MocDongBo WHERE Nguon = 'SanPham');
DECLARE @min binary(10) = sys.fn_cdc_get_min_lsn('dbo_SanPham');
DECLARE @tu binary(10) = IIF(@moc IS NULL, @min, sys.fn_cdc_increment_lsn(@moc));
DECLARE @den binary(10) = sys.fn_cdc_get_max_lsn();
IF @tu < @min THROW 50002, N'Cleanup đã xóa phần chưa đọc: phải nạp lại từ nguồn.', 1;
IF @tu > @den BEGIN SELECT N'chưa có gì mới' AS KetQua, 0 AS SoDong; RETURN; END;
BEGIN TRANSACTION;
INSERT INTO dich.LichSuGia (Lsn, SeqVal, SanPhamId, GiaCu, GiaMoi, LucDoi)
SELECT c.__$start_lsn, c.__$seqval, c.SanPhamId, c.GiaCu, c.DonGia, sys.fn_cdc_map_lsn_to_time(c.__$start_lsn)
FROM (SELECT *, LAG(DonGia) OVER (PARTITION BY __$start_lsn, __$seqval, SanPhamId
ORDER BY __$operation) AS GiaCu -- ảnh trước (3) đứng trước ảnh sau (4)
FROM cdc.fn_cdc_get_all_changes_dbo_SanPham(@tu, @den, N'all update old')) AS c
WHERE c.__$operation = 4 AND c.GiaCu <> c.DonGia
AND NOT EXISTS (SELECT 1 FROM dich.LichSuGia AS l -- khử trùng khi chạy lại
WHERE l.Lsn = c.__$start_lsn AND l.SeqVal = c.__$seqval AND l.SanPhamId = c.SanPhamId);
SELECT N'ghi lịch sử giá' AS KetQua, @@ROWCOUNT AS SoDong;
UPDATE dich.MocDongBo SET LsnCDC = @den WHERE Nguon = 'SanPham';
COMMIT;
END;
GO
Chạy trên database của mục 4, theo thứ tự:
| Bước | EXEC dich.usp_KeoCT |
EXEC dich.usp_KeoCDC |
|---|---|---|
| Lần đầu, chưa có mốc | Tải lại 5.000 sản phẩm | 2 dòng: 8.990.000 → 4.990.000 → 8.990.000 |
Giá SP-00777 thành 7.990.000, nhập 3 máy, bán 2 |
Kéo 1 dòng | 1 dòng; 3 lần đổi tồn bị bỏ qua |
| Chạy lại khi không có gì mới | Kéo 0 dòng | Chưa có gì mới |
Xóa LsnCDC, chạy lại từ đầu |
0 dòng mới, dich.LichSuGia vẫn 3 dòng |
Bước cuối giả lập consumer mất mốc, như khi đích ở hệ khác và mốc chưa kịp lưu. CT không cần khử trùng vì MERGE ghi giá trị hiện tại: chạy hai lần cho cùng một bản sao. CDC chèn lịch sử, nên khóa (Lsn, SeqVal, SanPhamId) chặn lần giao lại thành dòng thứ hai. Debezium có connector SQL Server dựng trên chính CDC (SQL Server 2016 SP1 trở lên, Standard hoặc Enterprise), lưu commit LSN và change LSN làm offset; tài liệu của họ ghi offset được commit định kỳ nên sau sự cố có thể có sự kiện trùng. Phía nhận vẫn phải khử trùng như trên.
7. Chọn cách nào
| Hệ nhận cần | Dùng | Vì sao |
|---|---|---|
| Giá, tồn hiện tại cho trang sản phẩm, cache, bộ tìm kiếm | CT trên dbo.SanPham |
1 dòng mỗi khóa, thấy ngay lúc commit, chạy cả trên Express |
| Đơn mới và đơn đổi trạng thái cho ERP của đại lý 42, thay cho gọi lại endpoint mỗi 15 phút | CDC net changes trên dbo.DonHang |
dbo.DonHang cần SWITCH, CT chặn nó |
| Mọi lần đổi giá, mọi trạng thái đơn, giá trị trước và sau, cho kho kế toán | CDC, all update old |
Chỉ CDC giữ ảnh trước |
"Lúc 12:00 khách 42 tên gì", hỏi ngay trong BanHang |
Temporal table | Lịch sử nằm cạnh bảng, hỏi bằng FOR SYSTEM_TIME AS OF |
| Dòng có bị sửa từ lúc mở màn hình kiểm kho không | rowversion |
So một cột, không cần cơ chế nào chạy nền |
| Báo cho hệ khác "đơn đã thanh toán" | Outbox | CT và CDC cho biết dòng nào đổi, không cho biết vì sao |
Cột rowversion cũng trả lời được "dòng nào đổi từ mốc X", nhưng không thấy dòng đã xóa, và giá trị gán lúc sửa chứ không phải lúc commit: giao dịch dài commit muộn có thể mang giá trị nhỏ hơn mốc đã lưu và bị bỏ sót. Microsoft nêu đúng điểm này khi so CT, vốn xếp theo thời điểm commit, với cách tự làm bằng cột timestamp.
Những chỗ hay hiểu sai
- "CT cho biết dữ liệu đổi thế nào." Nó chỉ cho biết khóa nào đổi, cột nào bị đụng. 302 lần sửa ra 1 dòng, giá 4.990.000 không còn ở đâu.
- "Net changes nhẹ hơn all changes." Đó chỉ là cách đọc.
_CTvẫn lưu 604 dòng cho 302 lần sửa, đủ 3 ngày. - "CDC đọc log nên không tốn gì cho phía ghi." Giao dịch ghi đơn chỉ thêm 119 B log, nhưng capture job chép lại từng thay đổi: tổng log ghi xuống đĩa tăng 39%.
- "Agent dừng một lúc thì CDC chỉ trễ." Không mất thay đổi, nhưng log giữ nguyên cho tới khi capture đọc kịp; log backup không giải phóng được nó.
- "CT nhẹ nên bật cho mọi bảng." Bảng cần
SWITCHpartition nhưdbo.DonHangbị chặn với Msg 4900.
Đọc tiếp
- Flash sale bán quá tồn kho: 300 lần trừ kho trên
SP-00777mà bài này đếm lại. - Bài toán hai vị tướng: vì sao consumer nào cũng phải chịu được việc giao lại, và outbox cho sự kiện nghiệp vụ.
- Transaction, khóa và isolation:
SNAPSHOTmà consumer CT dùng, và cái giá của phiên bản dòng. - Kiến trúc lưu trữ: log được cắt khi nào, vì sao log backup không cắt được phần capture chưa đọc.
- Azure SQL: trần ghi log, và vì sao CDC làm log giữ lâu hơn trên Azure SQL Database.
Nguồn
- Microsoft Learn, Track data changes: CT đồng bộ trong DML, CDC bất đồng bộ từ log, CT xếp theo thời điểm commit.
- Microsoft Learn, About change tracking, Enable and disable change tracking, ALTER DATABASE SET options: chỉ ghi khóa chính, retention, luồng dọn, khuyến nghị snapshot isolation.
- Microsoft Learn, Work with change tracking và Manage change tracking: mốc đồng bộ,
MIN_VALID_VERSION, giao dịchSNAPSHOT, chi phí như một chỉ mục,SWITCH,TRUNCATE. - Microsoft Learn, What is change data capture và CDC and other features: change table, validity interval,
sp_replcmds, log không cắt khi capture chưa đọc, hai capture instance, Linux. - Microsoft Learn, sys.sp_cdc_enable_table, sys.sp_cdc_add_job, sys.sp_cdc_change_job, cdc.fn_cdc_get_net_changes, sys.sp_flush_CT_internal_table_on_demand.
- Microsoft Learn, CDC with Azure SQL Database: bộ lập lịch 20 giây, tầng dịch vụ, ADR.
- Microsoft Learn, Editions and supported features of SQL Server 2019 và sys.databases: edition có CDC, CT;
log_reuse_wait_desc. - Debezium 3.7, Debezium connector for SQL Server, xem ngày 2026-10-02.