On this page
The migration in one paragraph
The migration in one paragraph
Inventory objects, code, jobs and linked servers, then run Babelfish Compass and the AWS Schema Conversion Tool assessment to classify the work. Choose between Babelfish, which lets PostgreSQL accept SQL Server clients and T-SQL for a staged move, and full conversion to native PostgreSQL. Load the data with pgloader for a one-time move, or with AWS DMS or Debezium when SQL Server keeps taking writes. Validate with counts, aggregates, sequences and replayed queries, and cut over in a short write freeze with a rollback agreed in advance. Microsoft's lifecycle page shows SQL Server 2016 extended support ended on 14 July 2026, which is a common trigger, but paid Extended Security Updates run to July 2029 if the deadline is the only problem.
Decide whether to migrate, and why now
Microsoft's lifecycle page lists SQL Server 2016 with mainstream support ended on 13 July 2021 and extended support ended on 14 July 2026. Paid Extended Security Updates run in three yearly steps, from 15 July 2026 to 17 July 2029 (checked 31 October 2026). If a supported version is the only goal, upgrading SQL Server or buying Extended Security Updates is a much smaller change than changing engine.
Migrate when there is a reason beyond preference: a license cost you cannot justify at renewal, a platform standard, extensions such as PostGIS or pgvector, or a move to a managed PostgreSQL service. The rest of this guide assumes you have decided to move.
Inventory the objects, the code and what sits outside the engine
Start with a count of what is actually there. An object census shows how much procedural code you have:
SELECT type_desc, COUNT(*) AS n
FROM sys.objects
WHERE is_ms_shipped = 0
GROUP BY type_desc
ORDER BY n DESC;A column-type census shows how many of the hard type cases you have:
SELECT t.name AS data_type, COUNT(*) AS n
FROM sys.columns c
JOIN sys.types t ON c.user_type_id = t.user_type_id
JOIN sys.objects o ON o.object_id = c.object_id
WHERE o.is_ms_shipped = 0 AND o.type = 'U'
GROUP BY t.name
ORDER BY n DESC;Then list what a schema tool will not see: SQL Agent jobs (msdb.dbo.sysjobs), linked servers (sys.servers where is_linked = 1), SSIS packages, SSRS reports, CLR assemblies, Service Broker queues and full-text catalogs. None of these is a database object that converts. Each needs a named replacement in the plan, such as pg_cron, foreign data wrappers or an external scheduler. Finally, record the database collation, the recovery model, the largest tables and the sustained write rate, because they decide how the data move and cutover work.
Assess with Babelfish Compass and the AWS tool
Babelfish Compass is a compatibility assessment tool for Babelfish for PostgreSQL. You point it at your T-SQL DDL and SQL scripts and it reports which constructs Babelfish supports and which it does not. Run it over the DDL and over the application SQL you can capture, such as ORM output and the top statements from query statistics. The report tells you whether Babelfish is a viable bridge, and which constructs would need a rewrite either way.
For the full-conversion route, the AWS Schema Conversion Tool or DMS Schema Conversion produces an assessment of how much of the schema and code converts automatically and puts action items into the converted code where it could not finish. Treat either report as an input. Classify every object as converts, needs review or needs rewrite, and keep the list: it becomes the project plan and the regression checklist.
Choose the path: Babelfish or native PostgreSQL
Babelfish for PostgreSQL is an open source project, licensed under Apache 2.0 and the PostgreSQL license, that lets PostgreSQL understand the SQL Server wire protocol (TDS 7.1 and higher) and T-SQL. Its documentation lists OLEDB, ODBC, JDBC and ADO.NET Entity Framework clients as working, and says the jTDS driver is not supported. Its docs also have pages on single versus multiple database setups, collations and language support, and migration workarounds.
| Babelfish | Full conversion | Babelfish first, native later | |
|---|---|---|---|
| What changes in the application | Connection string and any unsupported constructs | Drivers, SQL and every call to a converted procedure | Little at cutover, then one application at a time |
| Where the T-SQL lives | Stays T-SQL, running on PostgreSQL | Becomes PL/pgSQL and standard SQL | T-SQL first, converted later |
| PostgreSQL version | Set by the Babelfish release, which needs a PostgreSQL build with its hooks | Any supported PostgreSQL release | Babelfish's, until the last application moves |
| Best when | Many applications or vendor packages speak T-SQL and cannot be rewritten together | The application is yours and is being changed anyway | A deadline now and a longer runway for the rewrite |
| Risk | Features Babelfish does not cover, found late if you skip Compass | The largest code change | Two states to support, and a plan that has to say when Babelfish goes |
Map the types that differ
The table lists the type decisions that most often matter. Tools apply defaults, and the point is to know where you are overriding them.
| SQL Server | PostgreSQL | What to check |
|---|---|---|
| INT or BIGINT IDENTITY | integer or bigint GENERATED ... AS IDENTITY | AWS DMS does not migrate the identity property. Recreate it and reset every sequence past the loaded maximum. |
| BIT | boolean | pgloader's default mapping. Confirm no code compares the column to numbers other than 0 and 1. |
| DATETIME, DATETIME2, SMALLDATETIME | timestamp or timestamptz | pgloader maps datetime and datetime2 to timestamptz. Choose per column from what the application assumes about time zones. |
| DATETIMEOFFSET | timestamptz | PostgreSQL stores an instant and does not keep the original offset. |
| UNIQUEIDENTIFIER | uuid | pgloader's default mapping. Check any code that treats the value as a string. |
| MONEY, SMALLMONEY | numeric(19,4) | Avoid PostgreSQL's money type: its formatting depends on locale. |
| VARCHAR(n), NVARCHAR(n) | varchar(n) or text | pgloader maps them to text and drops the length. Keep the length where a constraint matters. |
| VARCHAR(MAX), NVARCHAR(MAX) | text | No special handling in PostgreSQL. LOB settings matter to DMS, not to the target. |
| VARBINARY, IMAGE | bytea | Large values: test load time and memory. |
| ROWVERSION or TIMESTAMP | No equivalent type | Optimistic-concurrency columns need a replacement, such as an explicit version column or the xmin system column. |
| HIERARCHYID, GEOGRAPHY, GEOMETRY | ltree or PostGIS types | pgloader maps hierarchyid and geography to bytea, which is not usable. Plan these columns by hand. |
| SQL_VARIANT, CURSOR, TABLE | No equivalent | AWS DMS does not support tables with columns of these types. |
Convert the T-SQL
On the full-conversion route, simple procedures convert quickly. The table covers the constructs that cause rewrites and surprises:
| T-SQL | PostgreSQL | What to check |
|---|---|---|
| SELECT TOP (n) | LIMIT n, or FETCH FIRST n ROWS ONLY | TOP without ORDER BY returns arbitrary rows in both. WITH TIES needs FETCH FIRST ... WITH TIES. |
| ISNULL(a, b) | COALESCE(a, b) | Result type rules differ. ISNULL takes the type of the first argument. |
| GETDATE(), SYSDATETIME() | now() or clock_timestamp() | now() returns the transaction start time and does not change inside a transaction. |
| a + b on strings | a || b, or concat(a, b) | || returns NULL if either side is NULL. concat() ignores NULL. |
| LEN() | length() | LEN ignores trailing spaces. length() does not, for varchar. |
| DATEADD, DATEDIFF | Interval arithmetic, date_trunc(), extract() | DATEDIFF counts boundaries crossed, not elapsed time. Rewrite, do not translate word for word. |
| IIF() | CASE WHEN | A mechanical rewrite. |
| OUTPUT inserted.* | RETURNING | Different placement in the statement. |
| MERGE | MERGE (PostgreSQL 15 and later), or INSERT ... ON CONFLICT | Check concurrency behavior under load for either. |
| SELECT ... INTO #t | CREATE TEMP TABLE t AS SELECT ... | Temp tables are session-scoped in both, but statistics and plans behave differently. Time the reporting procedures. |
| @@ROWCOUNT, SCOPE_IDENTITY() | GET DIAGNOSTICS n = ROW_COUNT, RETURNING id | Every use needs a decision about where the value is captured. |
| TRY ... CATCH | BEGIN ... EXCEPTION WHEN ... END | Each EXCEPTION block opens a subtransaction. In hot loops this is costly. |
| sp_executesql | EXECUTE ... USING | Parameter handling differs. Review for injection risk while you are there. |
| WITH (NOLOCK) | Remove it | PostgreSQL readers do not block writers and never see uncommitted data. Reports that relied on dirty reads will return different numbers. |
| Procedure returning a result set | Function RETURNS TABLE, SETOF or a refcursor | PostgreSQL procedures do not return result sets, so every caller changes. |
Choose the tool for the data move
Three tools cover most data moves. They differ mainly in whether they can keep PostgreSQL in sync while SQL Server still takes writes.
| pgloader | AWS DMS | Debezium | |
|---|---|---|---|
| Best for | One-time load with a write freeze | Full load plus ongoing replication, on or off AWS | Change events through Kafka when Kafka exists or several consumers need the changes |
| Schema conversion | Discovers the schema and builds indexes, primary and foreign keys | Creates tables and primary keys only. Use DMS Schema Conversion or the Schema Conversion Tool for the rest | Limited. The JDBC sink can create tables and add columns, not change types or keys |
| Change data capture | Not described in its documentation. Treat as a bulk loader | Yes, through MS-Replication or MS-CDC | Yes, through SQL Server's CDC tables |
| Watch for | Triggers and stored procedures are yours to convert | Identity not migrated. Temporal tables, sparse columns, columnstore indexes and memory-optimized tables unsupported. Computed-column changes not replicated | Needs Kafka and Kafka Connect, and the capture job must keep up or the transaction log grows |
Prepare change capture on the source
AWS lists the SQL Server prerequisites for DMS ongoing replication: the database must be set up for full backups with a backup taken before replication starts, and the recovery model must be Full or Bulk-logged. Transaction log entries are not marked for reuse until the capture job has processed them, which means a stalled capture job fills the log:
ALTER DATABASE appdb SET RECOVERY FULL;
BACKUP DATABASE appdb TO DISK = N'D:\backup\appdb.bak';
-- for MS-CDC based capture (DMS or Debezium)
EXEC sys.sp_cdc_enable_db;
EXEC sys.sp_cdc_enable_table
@source_schema = N'dbo', @source_name = N'orders', @role_name = NULL;CDC capture and cleanup run as SQL Server Agent jobs, so the Agent must be running, and you need to monitor log size and capture latency for the whole migration. AWS also documents that DMS can lose events if SQL Server truncates the log before DMS reads it, that tables without a primary key capture only inserts and deletes unless MS-Replication or MS-CDC is configured, and that all columns of a unique index must be NOT NULL when CDC is used. Fix these on the source before the load starts.
Debezium. The SQL Server connector takes a snapshot and then reads the CDC tables, streaming change events into Kafka or, through Debezium Server, into other sinks. The same source prerequisites apply.
Load the data
pgloader reads SQL Server directly, discovers the schema, and loads tables, indexes, primary keys and foreign keys. A load file makes the choices explicit and repeatable. Check the options against pgloader's MS SQL reference for the version you run:
LOAD DATABASE
FROM mssql://migrator@mssql-host/appdb
INTO postgresql://migrator@pg-host/appdb
WITH include drop, create tables, create indexes, foreign keys, reset sequences;Rehearse on a copy of production and time each phase. Where a write freeze of a few hours is acceptable this may be the whole data move. Otherwise load a snapshot, then let AWS DMS or Debezium carry the changes that arrive during and after the load. If you are using Babelfish, test the actual bulk-load path you intend to use against your volumes, because that is the part of the toolchain most specific to your estate.
Validate before anyone switches
Run validation continuously while the target syncs, not once at the end:
- Row counts and aggregates per table, plus hashes or sums over key ranges, because equal counts hide type coercion and timestamp shifts.
- Sequences: every identity or serial value must sit above the table's maximum. After a load with explicit IDs, advance each one:
SELECT setval(pg_get_serial_sequence('orders', 'id'),
COALESCE(MAX(id), 1), MAX(id) IS NOT NULL)
FROM orders;- Case-only duplicates: common SQL Server collations are case-insensitive, so unique columns can hold values that collide under PostgreSQL's case-sensitive default. Find them before the application does:
SELECT lower(email) AS e, count(*)
FROM users
GROUP BY 1
HAVING count(*) > 1;- Real queries: replay the top statements against both databases, compare results, then compare plans on the PostgreSQL side with
EXPLAIN ANALYZE. Indexes that suited SQL Server's optimizer may not suit PostgreSQL's. - Jobs and reports: run the month-end procedures and scheduled reports, not just the screens, and compare the numbers.
- Behavior that changed on purpose: NOLOCK removal, time zone handling and NULL ordering. SQL Server sorts NULLs first in ascending order and PostgreSQL sorts them last.
Run the cutover and keep a way back
- Days before: freeze schema changes, lower DNS time to live on the database hostname, and rehearse the full runbook at least once on a copy.
- Stop writes: put the application in maintenance mode or revoke write permissions on SQL Server. From here the write freeze clock is running.
- Drain: wait until the capture lag is nil and the last changes have applied.
- Compare: run the final counts, aggregates and sequence checks. Do not skip this to save minutes.
- Switch: repoint the application, start it, and smoke-test the critical paths.
- Rollback: until PostgreSQL takes its first write, SQL Server is untouched and rolling back means pointing back. After that you need reverse replication or a replay of the writes, so agree the point of no return in advance and write it into the change request.
Never combine the migration with a SQL Server upgrade or a major application release in the same window. If the source has to be upgraded first, that is its own change.
Getting help
AceMQ plans and runs migrations to PostgreSQL from SQL Server, Oracle and MySQL: assessment, a proof of value on one database, conversion, validation and a rehearsed cutover. See database migration to PostgreSQL for the method and deliverables, and PostgreSQL support or managed PostgreSQL services for the platform afterwards.
Sources, all read on 31 October 2026: Microsoft lifecycle page for SQL Server 2016; AWS documentation for SQL Server as a DMS source; pgloader's MS SQL reference; and the Babelfish for PostgreSQL site, FAQ and Babelfish Compass README.
Frequently asked questions
Can I migrate SQL Server to PostgreSQL without rewriting my application?
Partly. Babelfish for PostgreSQL lets PostgreSQL accept SQL Server client connections and T-SQL, so many applications need little change at first. It does not cover every SQL Server feature, so run Babelfish Compass over your DDL and application SQL before committing. Most estates still convert to native PostgreSQL over time.
What is Babelfish Compass?
A compatibility assessment tool for Babelfish for PostgreSQL. It analyzes T-SQL DDL and SQL scripts and reports which constructs Babelfish supports and which it does not.
Is pgloader enough to migrate SQL Server to PostgreSQL?
For a database that can tolerate a write freeze, it can move the schema, indexes, keys and data in one pass. Its documentation does not describe change capture, and stored procedures and triggers are not converted, so use AWS DMS or Debezium when SQL Server keeps taking writes.
Which SQL Server features does AWS DMS not replicate?
AWS documents that the identity property is not migrated, that temporal tables, sparse columns, columnstore indexes and memory-optimized tables are not supported, and that changes to computed columns are not replicated. Tables without a primary key capture only inserts and deletes unless MS-Replication or MS-CDC is configured.
When did SQL Server 2016 support end?
Microsoft's lifecycle page shows extended support ended on 14 July 2026. Paid Extended Security Updates run from 15 July 2026 to 17 July 2029. Upgrading SQL Server is a smaller change than changing engine if a supported version is the only goal.