Tooling will convert a Db2 schema to Postgres quickly and mostly correctly, which is why these projects are consistently underestimated. Tables and indexes are the easy part. The cost sits in procedural code, in behaviour your application depends on without anyone having written it down, and in the reports that only surface at quarter end - long after you thought you were finished.
01Do not migrate what nobody uses
Long-lived Db2 estates accumulate. Tables from decommissioned products, procedures called by a job that was disabled years ago, indexes supporting a report nobody runs. Migrating all of it means paying to convert, test and then maintain code with no consumer.
Establish actual usage before scoping. Query the catalogue and package cache for what has genuinely executed over a full business cycle - a quarter at minimum, ideally a year so annual processes appear. Dead objects should be documented and dropped, not ported.
02Stored procedures are the real project
Db2 SQL PL and Postgres PL/pgSQL are similar enough to lull you and different enough to hurt. Exception handling, cursor behaviour, result set returns and temporary table semantics all differ in ways that are individually small and collectively substantial.
Automated converters get most of the syntax and almost none of the semantics. The output compiles and then behaves differently under concurrency or at the edges, which is the worst possible failure mode because it passes a smoke test. Budget for reading and re-testing every procedure that carries business logic, and treat converter output as a first draft rather than a result.
03The differences that bite
A handful of behavioural differences account for most production incidents after a Db2 to Postgres cutover. None are difficult once known; all are expensive when discovered late.
| Area | Db2 | Postgres | Consequence |
|---|---|---|---|
| Default isolation | Cursor stability | Read committed | Different concurrency behaviour under load |
| Empty string | Distinct from NULL | Distinct, but casts differ | Subtle predicate changes |
| Identity / sequences | Db2 semantics | Sequence objects | Gaps and reset behaviour differ |
| Case folding | Upper by default | Lower by default | Unquoted identifiers resolve differently |
| MERGE / upsert | MERGE | INSERT ... ON CONFLICT | Rewrite, with different locking |
04Identifier case folding will catch you
Db2 folds unquoted identifiers to upper case; Postgres folds them to lower. Any application or tool that quotes identifiers inconsistently - and in a large estate something always does - will find tables it cannot see, with an error message that points nowhere useful.
Decide on a convention early: unquoted lower-case everywhere is the least painful in Postgres. Retrofitting this after the application layer has been ported is materially harder than deciding it at the start.
05Vacuum is the operational change
Postgres uses multiversion concurrency control, so updates and deletes leave dead tuples that autovacuum reclaims later. This is fine until a high-churn table outpaces autovacuum's default settings, at which point the table bloats, plans degrade and performance falls away gradually rather than failing outright.
A team coming from Db2 has no operational instinct for this because Db2 does not work this way. Tune autovacuum for your churn, monitor table bloat and transaction ID age, and understand long-running transactions - they hold back the vacuum horizon and are the usual root cause when bloat appears suddenly.
06Run both in parallel and compare output
The only migration validation that reliably works is running both systems against the same inputs and comparing results, particularly for reporting and batch. Unit tests confirm the code runs; parallel running confirms it produces the same numbers, which is the question the business actually cares about.
Run it across a full business cycle. Month-end and quarter-end processes exercise code paths that daily traffic never touches, and those paths are exactly where the interesting differences hide. A migration validated on two weeks of ordinary traffic has not been validated.
07Sequencing that works
Move reporting and read-only workloads first. They are lower risk, they surface a large share of the SQL dialect differences, and they let the team build operational familiarity with Postgres before anything transactional depends on it.
Keep Db2 authoritative until parallel running is clean, then cut writes over, then decommission. Resist the temptation to decommission early to save licence cost - the licence is far cheaper than discovering an unmigrated dependency with no system left to fall back to.
Topics
Marcus Hale
Lead Architect · SyncTrix
Writes about the engineering decisions behind production systems - architecture, delivery and the trade-offs that only show up at scale.
Building something like this?
SyncTrix engineers AI, SaaS, platform and cloud systems for enterprises and high-growth teams. Tell us what you're shipping and we'll scope it with you.