Overview Phase 1 — Assessment Phase 2 — Schema Conversion Phase 3 — Data Migration Phase 4 — Validation Phase 5 — Performance Phase 6 — Documentation
Phase 03 — Data Migration

Migrating 20.5 Million Rows

I built a custom Python ETL pipeline to move every row from PostgreSQL into the new SQL Server schema — and had to solve a real problem first: the audit triggers I'd just built would have fired on every one of the 10.8 million rows in the two audited tables.

Python ETL psycopg2 pyodbc fast_executemany Trigger Disable IDENTITY Reseed

What I Did in Phase 3

The Problem I Solved First

Before writing any load logic, I realized the audit triggers from Phase 2 would fire on every row inserted into cms.providers and cms.provider_services — roughly 10.8 million rows combined. Left unhandled, that would have flooded audit.data_access_log with migration noise and slowed the load dramatically from row-by-row trigger overhead.

-- Disable before bulk load, re-enable in a finally block -- so triggers come back even if migration fails partway through ALTER TABLE cms.providers DISABLE TRIGGER trg_audit_providers; ALTER TABLE cms.provider_services DISABLE TRIGGER trg_audit_provider_services; -- ... bulk load happens here ... ALTER TABLE cms.providers ENABLE TRIGGER trg_audit_providers; ALTER TABLE cms.provider_services ENABLE TRIGGER trg_audit_provider_services;
~20.5M
Rows Migrated
4
Tables Loaded
~15 min
Total Duration
0
Audit Rows From Migration

What Actually Went Wrong

Getting the ETL script to connect took several real fixes — documented here rather than smoothed over, since this is realistic DBA troubleshooting, not just script bugs.

ProblemCauseFix
Login timeout / error 1326TCP/IP disabled by default in SQL Server Configuration ManagerEnabled TCP/IP, restarted the SQL Server service
Still couldn't connectNo Windows Firewall rule for port 1433Created an inbound rule for TCP 1433
Login failed for etl_servicePassword typo at the interactive promptConfirmed credentials worked via SSMS, re-ran carefully
Cannot find object "providers"db_datawriter/db_datareader lack ALTER permission needed to disable triggersGranted ALTER on just the two tables that needed it
DBCC CHECKIDENT permission deniedIdentity reseeding needs elevated permission beyond routine ETL accessRan the one-time reseed manually under my own admin account

Every Table, Migrated and Verified

TableRowsTimeThroughput
cms.providers1,175,28150.8s~23,141 rows/sec
cms.provider_services9,660,647482.4s~20,026 rows/sec
cms.user_state_access2<0.1s
cms.staging_raw9,660,647357.4s~27,031 rows/sec
Screenshot — ETL Migration Output
Terminal output showing all four tables migrated with row counts and throughput
The custom Python ETL pipeline migrating all four tables end to end

I reseeded the IDENTITY counter on cms.provider_services after loading with IDENTITY_INSERT on, confirming the current identity value matched the last row loaded (9,660,647) so future inserts continue correctly.