Migrating Legacy ETL
When to use
- Moving legacy ETL (stored procs, SSIS, Informatica, hand-rolled jobs) or an
on-prem warehouse to a modern stack (dbt, Spark, cloud warehouse).
- Rewriting procedural transformations into set-based SQL/models.
- Validating that the new system matches the old before cutover.
- Do NOT use for greenfield pipelines (use the building/authoring skills).
Workflow
- [ ] Inventory jobs, dependencies, and consumers; find what is actually used
- [ ] Migrate incrementally (strangler fig), not big-bang
- [ ] Rewrite procedural logic into set-based, idempotent transformations
- [ ] Run old and new in parallel; reconcile outputs
- [ ] Cut over per domain once reconciliation passes; decommission the old path
- Inventory and triage. Map every job, its dependencies, and its downstream
consumers. Many legacy jobs are dead or duplicated — don't migrate what nobody
uses.
- Strangler-fig phasing. Migrate one domain/table at a time, redirecting
consumers as each piece is proven. Big-bang cutovers fail.
- Rewrite, don't transliterate. Convert row-by-row cursors/procedures into
set-based, idempotent SQL/models — a literal port keeps the old bottlenecks.
- Parallel run + reconcile. Run old and new side by side and compare row
counts, key sets, and aggregate sums until they match within tolerance.
- Cut over gradually and decommission the legacy path only after
reconciliation holds.
Patterns
Reconciliation harness — compare old vs new for the same window:
SELECT 'row_count' metric, (SELECT COUNT(*) FROM legacy.fct_orders) legacy,
(SELECT COUNT(*) FROM new.fct_orders) new_
UNION ALL
SELECT 'sum_amount', (SELECT SUM(amount) FROM legacy.fct_orders),
(SELECT SUM(amount) FROM new.fct_orders);
Investigate every non-matching metric; differences are usually NULL handling,
timezone, rounding, or dedup logic that the legacy system did implicitly.
Capture business rules buried in procedures as tested dbt models with
descriptions, so tribal logic becomes documented and version-controlled.
Common pitfalls
- Big-bang cutover — high risk, no rollback; migrate domain by domain.
- Literal transliteration — porting cursors/temp-table hops keeps legacy
inefficiency; re-express as set-based logic.
- No parallel-run reconciliation — subtle logic differences ship as data bugs;
compare counts/sums before trusting the new path.
- Migrating dead jobs — wasted effort; verify each job has real consumers.
- Losing implicit rules — legacy handling of NULLs/timezones/dedup is often
undocumented; reconcile to surface and encode it.
- No decommission step — old and new both run forever, doubling cost and
confusion.
1---2name: migrating-legacy-etl3description: Plan and execute migrations of legacy ETL and data warehouses — stored procedures, SSIS/Informatica, or on-prem warehouses to modern stacks (dbt, Spark, cloud warehouses) — using strangler-fig phasing, parallel runs, and row/aggregate reconciliation. Use when migrating legacy pipelines or warehouses, rewriting stored procedures into dbt/Spark, or validating a migration against the source system.4---56# Migrating Legacy ETL78## When to use910- Moving legacy ETL (stored procs, SSIS, Informatica, hand-rolled jobs) or an11 on-prem warehouse to a modern stack (dbt, Spark, cloud warehouse).12- Rewriting procedural transformations into set-based SQL/models.13- Validating that the new system matches the old before cutover.14- Do NOT use for greenfield pipelines (use the building/authoring skills).1516## Workflow1718```19- [ ] Inventory jobs, dependencies, and consumers; find what is actually used20- [ ] Migrate incrementally (strangler fig), not big-bang21- [ ] Rewrite procedural logic into set-based, idempotent transformations22- [ ] Run old and new in parallel; reconcile outputs23- [ ] Cut over per domain once reconciliation passes; decommission the old path24```25261. **Inventory and triage.** Map every job, its dependencies, and its downstream27 consumers. Many legacy jobs are dead or duplicated — don't migrate what nobody28 uses.292. **Strangler-fig phasing.** Migrate one domain/table at a time, redirecting30 consumers as each piece is proven. Big-bang cutovers fail.313. **Rewrite, don't transliterate.** Convert row-by-row cursors/procedures into32 set-based, idempotent SQL/models — a literal port keeps the old bottlenecks.334. **Parallel run + reconcile.** Run old and new side by side and compare row34 counts, key sets, and aggregate sums until they match within tolerance.355. **Cut over gradually** and decommission the legacy path only after36 reconciliation holds.3738## Patterns3940**Reconciliation harness** — compare old vs new for the same window:4142```sql43SELECT 'row_count' metric, (SELECT COUNT(*) FROM legacy.fct_orders) legacy,44 (SELECT COUNT(*) FROM new.fct_orders) new_45UNION ALL46SELECT 'sum_amount', (SELECT SUM(amount) FROM legacy.fct_orders),47 (SELECT SUM(amount) FROM new.fct_orders);48```4950Investigate every non-matching metric; differences are usually NULL handling,51timezone, rounding, or dedup logic that the legacy system did implicitly.5253**Capture business rules** buried in procedures as tested dbt models with54descriptions, so tribal logic becomes documented and version-controlled.5556## Common pitfalls5758- **Big-bang cutover** — high risk, no rollback; migrate domain by domain.59- **Literal transliteration** — porting cursors/temp-table hops keeps legacy60 inefficiency; re-express as set-based logic.61- **No parallel-run reconciliation** — subtle logic differences ship as data bugs;62 compare counts/sums before trusting the new path.63- **Migrating dead jobs** — wasted effort; verify each job has real consumers.64- **Losing implicit rules** — legacy handling of NULLs/timezones/dedup is often65 undocumented; reconcile to surface and encode it.66- **No decommission step** — old and new both run forever, doubling cost and67 confusion.