Several databases with one agent
One SQL Server Agent installation syncing several databases on the same instance with mixed sync modes, a table naming structure that keeps them apart, and sizing guidance for thousands of tables.
One agent installation handles every database on a SQL Server instance that the agent login can reach. This scenario shows an ERP database on Change Tracking, a legacy database with no primary keys on full extracts, a warehouse staging database on CDC, and how to lay the output out so the three never collide.
When to use it
- Several databases live on one instance (or one Availability Group listener) and you want one host, one schedule, one state database and one set of alerts.
- The databases differ in what they support: some have primary keys and Change Tracking, some do not.
- Total object count is large (thousands of tables), so refresh and memory settings matter.
When two instances are involved, install two agents (one per instance, each with its own target_schema_prefix and state database) rather than pointing one agent at a linked server.
Configuration
agent_version: 1.9.4
state_db_path: C:/dlh/state/dlh_agent_state.duckdb
connection_information:
connection_type: sql_server
server_name_or_ip: sql01.corp.local
server_port: 1433
database_names: [ErpDb, LegacyDb, StagingDb]
data_retrieval:
historical_load: false
output_format: delta
direct_cloud_write: true
storage_base_path: /data/delta_iceberg_tables
output_path: C:/dlh/output
table_name_structure: db_separated
table_name_casing: lowercase
parquet_compression: zstd
on_schema_drift_action: migrate_target
on_schema_drift_action_handling: fail_once
fetch_batch_size: 50000
max_memory_percent: 80
databases:
# ERP: primary keys everywhere, Change Tracking enabled on the database
- name: ErpDb
sync_mode: ct
schema_refresh_mode: 1D
tables:
- name: dbo.*
all_columns: true
no_pk_strategy: full
- name: dbo.EventLog
all_columns: true
write_strategy: append
views:
- name: rpt.*
all_columns: true
# Legacy: mostly heaps without primary keys, no CT available
- name: LegacyDb
sync_mode: full
schema_refresh_mode: 1W
tables:
- name: dbo.Customers
all_columns: true
write_strategy: replace
- name: dbo.Products
all_columns: false
columns: [ProductId, Name, Category, UnitPrice, Discontinued]
write_strategy: replace
- name: dbo.SalesHistory # has a primary key but CT is not enabled on LegacyDb
all_columns: true
write_strategy: delete+insert
incremental_key: SaleDate
# Staging: CDC is already enabled for other consumers
- name: StagingDb
sync_mode: cdc
schema_refresh_mode: table_changed
tables:
- name: stg.*
all_columns: true
no_pk_strategy: full
dlh_ref:
org_guid: '<YOUR_ORG_GUID>'
prj_guid: '<YOUR_PROJECT_GUID>'
connection_guid: '<YOUR_CONNECTION_GUID>'
target_schema_prefix: Site003
api_key: '<YOUR_DLH_API_KEY>'
dlh_notifications:
enabled: true
platform_run_report: true
webhooks:
- url_env_var: DLH_OPS_WEBHOOK_URL
format: teams| Setting | Why |
|---|---|
database_names: [ErpDb, LegacyDb, StagingDb] | The connection is opened once per database; each databases entry must match a name here. The agent login needs access to all three. |
sync_mode per database | The database-level mode is the default for its tables: ct for ERP, full for the keyless legacy database, cdc for staging. Individual tables can still override. |
table_name_structure: db_separated | Output becomes delta/erpdb/dbo.customer/, delta/legacydb/dbo.customers/: one folder per database, schema.table inside. Two databases with a dbo.Customers table no longer share a folder. See the table below for the alternatives. |
write_strategy: replace on the legacy tables | A full extract with replace rewrites the table each run, the cheapest correct option when there is no key to merge on. |
delete+insert with incremental_key: SaleDate | For the one large legacy table that does have a primary key, the agent deletes the keys present in the batch, bounded by the SaleDate window, and reinserts them instead of rewriting the whole table. Without a primary key this strategy behaves like append, so it is not used on the heaps. |
columns: on dbo.Products | Column selection works on specific entries only (wildcard entries always take all columns). |
schema_refresh_mode per database | Daily for ERP, weekly for the legacy database that never changes, and only when SQL Server reports a modification for staging. At thousands of tables, always would spend most of a five-minute run on metadata. |
fetch_batch_size: 50000, max_memory_percent: 80 | More conservative than the defaults because three databases' initial loads run back to back on the same host. |
Naming structures compared
table_name_structure decides how database, schema and table are arranged in the output. For Delta tables under storage_base_path:
| Value | Delta folder | Iceberg namespace and table |
|---|---|---|
default or flat | delta/ErpDb.dbo.Customer/ | default, ErpDb__dbo__Customer |
nested | delta/ErpDb/dbo/Customer/ | default.ErpDb.dbo, Customer |
db_combined | delta/ErpDb__dbo/Customer/ | default.ErpDb__dbo, Customer |
db_separated | delta/ErpDb/dbo.Customer/ | default.ErpDb, dbo__Customer |
Pick one before the first run and keep it. Changing it later makes the agent create new tables at the new paths while the old ones stay behind; you would then reload and repoint the downstream engine. flat is fine for a single database; db_separated or nested read better once several databases share a prefix. CSV and Parquet output ignores this setting and always writes <db>.<schema>.<table> file names in dated folders.
Run behaviour
The run summary groups tables by database, and each database's tables are processed in turn:
ErpDb dbo.Customer table ct 42 OK
ErpDb dbo.EventLog table ct 1210 OK
ErpDb rpt.OpenOrders view full 3308 OK
LegacyDb dbo.Customers table full 18455 OK
LegacyDb dbo.SalesHistory table full 4021 OK
StagingDb stg.Shipment table cdc 17 OK
StagingDb stg.ShipmentLine table cdc 0 OK No CT/CDC changes - skipped writeViews are always extracted in full (they have no CT or CDC) and are typically written with replace. The state database records one sync state per database, schema and table, so a failure in one database never blocks the others, and --diagnose reports the CT and CDC status of each database separately.
Sizing for thousands of tables
- Wildcards keep the file short:
dbo.*on a 3,000-table schema is one entry. Add specific entries only for exceptions. - Schema refresh is the main per-run cost at scale. Use
1D,1Wortable_changedrather thanalwaysin production. - Initial load of thousands of tables can take hours. Run it interactively or from a one-off scheduled task outside business hours, then enable the five-minute schedule once the first run finishes. Keep the scheduled task's "Do not start a new instance" setting so a long run is never overlapped by the next trigger; see Operations.
- Memory: watch the
STATE STOREblock and the memory pause messages in the log during the first week. If pauses recur, lowerfetch_batch_sizefurther. See Performance and retention. - Alerts at volume: a single failing table out of 3,000 still produces an error notification every run. Use
error_severity: criticalon a noisy channel anderroron the channel your operators watch.
Variations
- Different targets per database (for example Iceberg for staging and Delta for ERP) are not supported in one configuration;
output_formatis global. Install a second agent with its owntarget_schema_prefix. - Same database name on two instances: give each agent a distinct
target_schema_prefix; storage paths always include it, so nothing collides. - Availability Groups: point
server_name_or_ipat the listener. Change Tracking and CDC state fails over with the database, and the agent's own state is in its DuckDB file on the host.