Chapter 18 · Change Tracking, CDC, Service Broker, and Integration Patterns
Change Tracking for Lightweight Synchronization and Version-Based Reads
Choose deliberately among AGs, replication, log shipping, CDC/ETL and application replication from the information and failover contract actually required.
Learning outcomes
A field-service tablet that reconnects after an hour does not need a full history of every intermediate value. It needs to know which rows changed since the last successful synchronization, retrieve the current row state, and detect when its checkpoint is too old to trust. SQL Server Change Tracking (CT) is designed for that lightweight contract. The crucial discipline is to use its version metadata as a synchronization protocol rather than pretending it is an audit log.
Explain database-level and table-level Change Tracking, versions, side metadata, retention, and automatic cleanup.
Use CHANGE_TRACKING_CURRENT_VERSION, CHANGETABLE and CHANGE_TRACKING_MIN_VALID_VERSION as a safe synchronization handshake.
Distinguish CT change metadata from current base-table values and from CDC before/after history.
Design recovery when a client checkpoint falls behind the retention window instead of returning an incomplete delta silently.
Account for SQL Server 2025 adaptive shallow cleanup when diagnosing large Change Tracking side tables.
1. The information contract: “what changed?”, not “what were all the values?”
Change Tracking records lightweight metadata keyed by the primary key of a tracked table. It can tell a synchronizer that a row was inserted, updated, or deleted after a version checkpoint, and it can optionally record which tracked columns participated in an update. It does not preserve a durable sequence of all intermediate row images. For an insert or update, the normal synchronization pattern joins the change metadata back to the base table to fetch the row’s current values. A delete has no current base row, so the change row’s primary-key metadata is what lets a consumer remove its local copy.
The version number is database-scoped.
CHANGE_TRACKING_CURRENT_VERSION() returns the
current version boundary.
CHANGETABLE(CHANGES table, last_sync_version) asks
which rows changed after a prior checkpoint. Before using that
checkpoint, the client must compare it with
CHANGE_TRACKING_MIN_VALID_VERSION(OBJECT_ID(...)).
If the saved version is older than the minimum valid version,
cleanup may already have removed required metadata and the delta
is no longer complete.
USE master;GOALTER DATABASE ServiceHubLab SET CHANGE_TRACKING = ON (CHANGE_RETENTION = 2 DAYS, AUTO_CLEANUP = ON);GOUSE ServiceHubLab;GOIF SCHEMA_ID(N'lab18') IS NULL EXEC(N'CREATE SCHEMA lab18 AUTHORIZATION dbo;');GODROP TABLE IF EXISTS lab18.SyncItem;GOCREATE TABLE lab18.SyncItem( item_id int NOT NULL PRIMARY KEY, work_order_id bigint NOT NULL, status varchar(20) NOT NULL, note nvarchar(200) NULL, modified_at datetime2(3) NOT NULL DEFAULT SYSUTCDATETIME());GOALTER TABLE lab18.SyncItem ENABLE CHANGE_TRACKING WITH (TRACK_COLUMNS_UPDATED = ON);GO
The database option establishes retention and cleanup policy;
the table option opts this particular table into tracking. The
primary key is fundamental because the synchronization consumer
must identify the row whose state changed.
TRACK_COLUMNS_UPDATED adds a bit-mask that can help
a client decide whether a particular column participated in an
update, but it does not turn CT into a column-value history.
2. Capture a version boundary, change rows, then read a delta
USE ServiceHubLab;GODECLARE @last_sync_version bigint = CHANGE_TRACKING_CURRENT_VERSION();INSERT lab18.SyncItem(item_id, work_order_id, status, note)VALUES (101, 1001, 'ASSIGNED', N'Initial dispatch'), (102, 1002, 'NEW', N'Awaiting technician');UPDATE lab18.SyncItemSET status = 'ONSITE', modified_at = SYSUTCDATETIME()WHERE item_id = 101;DELETE lab18.SyncItem WHERE item_id = 102;DECLARE @next_sync_version bigint = CHANGE_TRACKING_CURRENT_VERSION();SELECT CT.SYS_CHANGE_VERSION, CT.SYS_CHANGE_CREATION_VERSION, CT.SYS_CHANGE_OPERATION, CT.SYS_CHANGE_COLUMNS, CT.item_id, B.work_order_id, B.status, B.note, B.modified_atFROM CHANGETABLE(CHANGES lab18.SyncItem, @last_sync_version) AS CTLEFT JOIN lab18.SyncItem AS B ON B.item_id = CT.item_idORDER BY CT.SYS_CHANGE_VERSION, CT.item_id;SELECT @last_sync_version AS last_version, @next_sync_version AS checkpoint_to_persist;GO
The expected result is one logical change row per key
representing the net tracked change since the requested version.
Item 101 should be present with its current base-table status
ONSITE. Item 102 should appear as a delete; the
left join yields no base-row payload because the row is gone.
The synchronization transaction should persist
@next_sync_version only after the client has
durably applied every row in the window.
3. The failure mode that matters: a checkpoint older than retained metadata
A common implementation bug is to store a checkpoint forever and
blindly call CHANGETABLE. A client that was offline
longer than the configured retention interval can then receive a
partial set and incorrectly conclude that it is synchronized.
The safe algorithm validates the checkpoint first. When it is
stale, perform a full re-snapshot of current data and establish
a new version boundary.
USE ServiceHubLab;GODECLARE @client_version bigint = -1; -- deliberate stale-client simulationDECLARE @min_valid bigint = CHANGE_TRACKING_MIN_VALID_VERSION(OBJECT_ID(N'lab18.SyncItem'));DECLARE @current bigint = CHANGE_TRACKING_CURRENT_VERSION();SELECT @client_version AS client_version, @min_valid AS minimum_valid_version, @current AS current_version;IF @min_valid IS NULL THROW 51000, 'Change Tracking is not available for the tracked table.', 1;IF @client_version < @min_validBEGIN SELECT 'FULL_RESYNC_REQUIRED' AS decision, @current AS new_baseline_version; -- Application path: read the authoritative table snapshot, -- commit it at the client, then store @current as the new checkpoint.ENDELSEBEGIN SELECT * FROM CHANGETABLE(CHANGES lab18.SyncItem, @client_version) AS CT;END;GO
This intentionally “fails closed”: it refuses to represent an incomplete delta as success. Retention should be chosen from the maximum expected client outage plus operational margin, observed side-table growth, and cleanup performance. Do not extend retention without capacity analysis merely to hide synchronization design defects.
4. SQL Server 2025 cleanup changes and operational evidence
SQL Server 2025 changes Change Tracking automatic cleanup for large side tables. Earlier versions used a deeper periodic cleanup approach; 17.x introduces adaptive shallow cleanup below a safe cleanup point, informed by retention and cleanup depth. The architectural lesson is not to depend on undocumented cleanup timing. Observe configured retention, minimum valid versions, synchronization age, and side-table pressure; design clients around validity checks.
SELECT name, is_auto_cleanup_on, retention_period, retention_period_units_descFROM sys.change_tracking_databasesWHERE database_id = DB_ID(N'ServiceHubLab');USE ServiceHubLab;SELECT OBJECT_SCHEMA_NAME(object_id) AS schema_name, OBJECT_NAME(object_id) AS table_name, is_track_columns_updated_on, begin_version, min_valid_versionFROM sys.change_tracking_tablesWHERE object_id = OBJECT_ID(N'lab18.SyncItem');GO
These catalog views establish configuration and table enrollment. They do not prove every client is healthy. Production monitoring needs client checkpoint age, full-resync counts, synchronization duration, row volume, and errors alongside the server metadata.
5. Production judgment and bridge
Change Tracking is a strong fit for occasionally connected clients, cache synchronization, and incremental refresh where the source table remains authoritative. Protect its contract with primary keys, bounded retention, checkpoint durability, minimum-valid-version checks, and a tested full-resync path. Do not sell it as audit evidence or an exactly-once event stream. Lesson 2 moves to CDC, which retains relational change rows and LSN-based windows for ETL-style consumption—but brings more storage, retention, Agent and edition responsibilities.
Check your understanding
- Why must a CT client call CHANGE_TRACKING_MIN_VALID_VERSION before requesting changes?
- Does SYS_CHANGE_COLUMNS contain old and new values?
- Why is a delete handled differently from an insert/update?
- What changed operationally in SQL Server 2025 CT cleanup?
- When is CT preferable to CDC?
Review the answers
1. Because cleanup can remove metadata older than the retention window. A checkpoint below the minimum can yield an incomplete delta and must trigger a full re-synchronization.
2. No. It is metadata indicating columns that participated in an update when TRACK_COLUMNS_UPDATED is enabled; current row values come from the base table.
3. The base row no longer exists, so the CT row’s key and delete operation tell the consumer what local row to remove.
4. Large side tables can use adaptive shallow cleanup rather than relying only on the older deep-cleanup pattern; clients must still use the validity protocol instead of timing assumptions.
5. When a consumer mainly needs lightweight “which rows changed?” synchronization and does not require every intermediate change or before/after history.