Guide · Database

SQL Server to PostgreSQL Migration: The Technical Guide

Moving SQL Server to PostgreSQL is a code migration more than a data migration. Rows copy cleanly. The work is in T-SQL stored procedures, types with no counterpart, collation behavior, and everything around the engine that SQL Server quietly provided: Agent jobs, linked servers, replication and reporting. This guide takes the steps in order: decide, inventory, assess with Babelfish Compass, map types and T-SQL, choose the data-move tool, prepare change capture, validate, and cut over with a rollback.

Scott Sternloff

By Scott Sternloff, Senior Enterprise Architect

LinkedIn · Updated · 12 min read · 12 sections

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 and a staged move
BabelfishFull conversionBabelfish first, native later
What changes in the applicationConnection string and any unsupported constructsDrivers, SQL and every call to a converted procedureLittle at cutover, then one application at a time
Where the T-SQL livesStays T-SQL, running on PostgreSQLBecomes PL/pgSQL and standard SQLT-SQL first, converted later
PostgreSQL versionSet by the Babelfish release, which needs a PostgreSQL build with its hooksAny supported PostgreSQL releaseBabelfish's, until the last application moves
Best whenMany applications or vendor packages speak T-SQL and cannot be rewritten togetherThe application is yours and is being changed anywayA deadline now and a longer runway for the rewrite
RiskFeatures Babelfish does not cover, found late if you skip CompassThe largest code changeTwo states to support, and a plan that has to say when Babelfish goes
Table · Babelfish, full conversion and a staged move. Scroll sideways on small screens.

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 types and their PostgreSQL equivalents
SQL ServerPostgreSQLWhat to check
INT or BIGINT IDENTITYinteger or bigint GENERATED ... AS IDENTITYAWS DMS does not migrate the identity property. Recreate it and reset every sequence past the loaded maximum.
BITbooleanpgloader's default mapping. Confirm no code compares the column to numbers other than 0 and 1.
DATETIME, DATETIME2, SMALLDATETIMEtimestamp or timestamptzpgloader maps datetime and datetime2 to timestamptz. Choose per column from what the application assumes about time zones.
DATETIMEOFFSETtimestamptzPostgreSQL stores an instant and does not keep the original offset.
UNIQUEIDENTIFIERuuidpgloader's default mapping. Check any code that treats the value as a string.
MONEY, SMALLMONEYnumeric(19,4)Avoid PostgreSQL's money type: its formatting depends on locale.
VARCHAR(n), NVARCHAR(n)varchar(n) or textpgloader maps them to text and drops the length. Keep the length where a constraint matters.
VARCHAR(MAX), NVARCHAR(MAX)textNo special handling in PostgreSQL. LOB settings matter to DMS, not to the target.
VARBINARY, IMAGEbyteaLarge values: test load time and memory.
ROWVERSION or TIMESTAMPNo equivalent typeOptimistic-concurrency columns need a replacement, such as an explicit version column or the xmin system column.
HIERARCHYID, GEOGRAPHY, GEOMETRYltree or PostGIS typespgloader maps hierarchyid and geography to bytea, which is not usable. Plan these columns by hand.
SQL_VARIANT, CURSOR, TABLENo equivalentAWS DMS does not support tables with columns of these types.
Table · SQL Server types and their PostgreSQL equivalents. Scroll sideways on small screens.

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 constructs and their PostgreSQL equivalents
T-SQLPostgreSQLWhat to check
SELECT TOP (n)LIMIT n, or FETCH FIRST n ROWS ONLYTOP 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 stringsa || 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, DATEDIFFInterval arithmetic, date_trunc(), extract()DATEDIFF counts boundaries crossed, not elapsed time. Rewrite, do not translate word for word.
IIF()CASE WHENA mechanical rewrite.
OUTPUT inserted.*RETURNINGDifferent placement in the statement.
MERGEMERGE (PostgreSQL 15 and later), or INSERT ... ON CONFLICTCheck concurrency behavior under load for either.
SELECT ... INTO #tCREATE 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 idEvery use needs a decision about where the value is captured.
TRY ... CATCHBEGIN ... EXCEPTION WHEN ... ENDEach EXCEPTION block opens a subtransaction. In hot loops this is costly.
sp_executesqlEXECUTE ... USINGParameter handling differs. Review for injection risk while you are there.
WITH (NOLOCK)Remove itPostgreSQL readers do not block writers and never see uncommitted data. Reports that relied on dirty reads will return different numbers.
Procedure returning a result setFunction RETURNS TABLE, SETOF or a refcursorPostgreSQL procedures do not return result sets, so every caller changes.
Table · T-SQL constructs and their PostgreSQL equivalents. Scroll sideways on small screens.

15-minute emergency SLA. Named senior engineers.

Contact Us

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 and Debezium for SQL Server to PostgreSQL
pgloaderAWS DMSDebezium
Best forOne-time load with a write freezeFull load plus ongoing replication, on or off AWSChange events through Kafka when Kafka exists or several consumers need the changes
Schema conversionDiscovers the schema and builds indexes, primary and foreign keysCreates tables and primary keys only. Use DMS Schema Conversion or the Schema Conversion Tool for the restLimited. The JDBC sink can create tables and add columns, not change types or keys
Change data captureNot described in its documentation. Treat as a bulk loaderYes, through MS-Replication or MS-CDCYes, through SQL Server's CDC tables
Watch forTriggers and stored procedures are yours to convertIdentity not migrated. Temporal tables, sparse columns, columnstore indexes and memory-optimized tables unsupported. Computed-column changes not replicatedNeeds Kafka and Kafka Connect, and the capture job must keep up or the transaction log grows
Table · pgloader, AWS DMS and Debezium for SQL Server to PostgreSQL. Scroll sideways on small screens.

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

  1. 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.
  2. Stop writes: put the application in maintenance mode or revoke write permissions on SQL Server. From here the write freeze clock is running.
  3. Drain: wait until the capture lag is nil and the last changes have applied.
  4. Compare: run the final counts, aggregates and sequence checks. Do not skip this to save minutes.
  5. Switch: repoint the application, start it, and smoke-test the critical paths.
  6. 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.

Free Consultation

Get Expert Eyes on Your Database Environment

Whether you're troubleshooting a production incident, planning a migration, or want a second opinion on your architecture — our team is ready. No pitch, just answers.

Email Us