Skip to content
Guide · Migrate between databases

SQL Server to PostgreSQL Migration Guide

Moving from SQL Server to PostgreSQL means converting three things: data types, T-SQL code and the data itself. This guide maps the common types, shows the T-SQL constructs that need rewriting for PostgreSQL, compares pgloader, AWS DMS with Schema Conversion and Babelfish for Aurora PostgreSQL, and works through one table and one stored procedure from source to validated target.

Steps checked 8 October 2026 against the official documentation for each product. Versions covered: SQL Server 2016 to 2022 as source, PostgreSQL 18 (18.6) as target, pgloader 3.6.9, AWS DMS 3.5.x/3.6.x with DMS Schema Conversion, Babelfish for Aurora PostgreSQL. Next review due April 2027. Installers, versions and download pages change; follow the official page if a step differs.
Short answer
  • Most tables convert mechanically: INT and BIGINT stay, BIT becomes boolean, NVARCHAR becomes varchar or text, DATETIME2 becomes timestamp, UNIQUEIDENTIFIER becomes uuid and VARBINARY becomes bytea.
  • The real work is T-SQL: TOP, ISNULL, GETDATE, + for strings, #temp tables, TRY...CATCH and procedures that return result sets all need PostgreSQL equivalents.
  • pgloader converts and loads a whole SQL Server database into PostgreSQL in one command, but it does not translate procedures or keep the target in sync.
  • AWS DMS with DMS Schema Conversion converts schema and code and can replicate changes (CDC) for a low-downtime cutover into RDS or Aurora PostgreSQL.
  • Babelfish for Aurora PostgreSQL lets SQL Server applications keep using T-SQL and the TDS protocol on PostgreSQL, with documented differences; assess your code with Babelfish Compass first.
How we know: Research-based: type behaviour, syntax and tool options were checked against the PostgreSQL 18 documentation, Microsoft's Transact-SQL reference, the pgloader documentation, the AWS DMS User Guide and the Aurora User Guide on 8 October 2026. The SQL examples follow the documented syntax but were not run against a live server for this guide; test them on a copy of your own schema.

Planning a SQL Server to PostgreSQL migration

A SQL Server to PostgreSQL migration is heterogeneous: the engines share standard SQL but differ in types, procedural language, identifier rules and system functions. Before choosing a tool, list the schemas and table sizes, count the stored procedures, functions, triggers and views, find SQL Server features with no direct PostgreSQL equivalent (SQL Agent jobs, linked servers, CLR objects, full-text catalogues), and record every application that connects. The SQL Server vs PostgreSQL comparison summarises the engine differences, and the database migration guide covers the general process.

There are three realistic routes:

  • Convert to native PostgreSQL with pgloader (schema and data) or DMS Schema Conversion plus AWS DMS (schema, code and data, with optional CDC). Applications are then changed to PostgreSQL drivers and SQL.
  • Babelfish for Aurora PostgreSQL, which accepts SQL Server clients over TDS on port 1433 and runs T-SQL with documented differences, so applications change less. It is specific to Amazon Aurora.
  • Manual: script the schema yourself, export data with bcp and load it with PostgreSQL's COPY. Practical only for small schemas.

SQL Server to PostgreSQL data type mapping

The middle column shows what pgloader does by default; the right column is the usual hand-written choice. pgloader turns every character type into text and every datetime into timestamptz; PostgreSQL's text has no length limit, so if you rely on lengths for validation, keep varchar(n) instead.

Common SQL Server types and their PostgreSQL equivalents
SQL Serverpgloader defaultTypical PostgreSQL choice and notes
INT IDENTITY(1,1)integer with a sequenceinteger GENERATED BY DEFAULT AS IDENTITY; reset it after loading
TINYINTsmallintsmallint, the smallest PostgreSQL integer type
BITbooleanboolean; code comparing with 1 and 0 must change to true and false
DECIMAL(p,s), NUMERIC, MONEYnumericnumeric(p,s); give MONEY an explicit scale such as numeric(19,4)
CHAR, VARCHAR, NCHAR, NVARCHARtextvarchar(n) or text; create the database with UTF8 encoding so these columns hold Unicode text
NVARCHAR(MAX), XMLtexttext, or PostgreSQL's xml type
DATETIME, DATETIME2timestamptztimestamp (without time zone) matches SQL Server semantics; timestamptz if you store UTC
DATETIMEOFFSETnot listedtimestamptz
UNIQUEIDENTIFIERuuiduuid
BINARY, VARBINARY(MAX)byteabytea
FLOAT, REALfloat, realdouble precision, real

T-SQL to PostgreSQL: what to rewrite

Procedural code is where most effort goes. The table lists the constructs that appear in almost every SQL Server code base and their PostgreSQL forms. PostgreSQL procedures (CREATE PROCEDURE) do not return result sets the way T-SQL procedures do, so procedures that end in a SELECT usually become functions that RETURNS TABLE; DMS Schema Conversion has a setting for exactly this choice.

T-SQL constructs and PostgreSQL equivalents
T-SQL (SQL Server)PostgreSQLNote
SELECT TOP (10) ...... LIMIT 10 (or FETCH FIRST 10 ROWS ONLY)LIMIT goes at the end of the query
ISNULL(a, b)COALESCE(a, b)COALESCE also works in SQL Server
GETDATE()now() or current_timestampReturns the transaction start time, with time zone
'a' + 'b''a' || 'b'|| is the string concatenation operator
[Order Details]"Order Details"Unquoted names fold to lower case; quoted names are case-sensitive
SCOPE_IDENTITY() after INSERTINSERT ... RETURNING idRETURNING works on INSERT, UPDATE, DELETE and MERGE
#temp tablesCREATE TEMP TABLEDropped automatically at the end of the session
BEGIN TRY ... END TRY BEGIN CATCH ... END CATCHBEGIN ... EXCEPTION WHEN ... THEN ... ENDPL/pgSQL traps errors with an EXCEPTION clause
MERGEMERGE or INSERT ... ON CONFLICTPostgreSQL supports MERGE; ON CONFLICT handles simple upserts
Procedure ending in SELECTFunction with RETURNS TABLECalled with SELECT * FROM fn(...)

Tools: pgloader, AWS DMS and Babelfish compared

pgloader's stable release is 3.6.9; its documentation now also describes a Java-based v4 published as a development build. For the AWS route step by step, including CDC prerequisites on SQL Server and pricing, see the AWS DMS guide. For Babelfish, AWS recommends running Babelfish Compass on your generated DDL to measure how much T-SQL is supported before committing.

SQL Server to PostgreSQL migration tools
pgloaderAWS DMS + DMS Schema ConversionBabelfish for Aurora PostgreSQL
What it doesReads the SQL Server catalogue, creates tables, indexes and keys, and loads data with COPYConverts schema and code (assessment report, then convert), then migrates data with full load and optional CDCAdds a T-SQL/TDS endpoint to Aurora PostgreSQL so SQL Server clients connect unchanged
Stored proceduresNot convertedConverted where the rules or generative AI can; the rest flagged for manual workRun as T-SQL, within Babelfish's documented limits
Ongoing changesNoYes (needs Enterprise, Standard 2016+ or Developer source)Data moved with DMS or export and import
TargetAny PostgreSQLAmazon RDS or Aurora PostgreSQLAmazon Aurora PostgreSQL only
CostFree, open sourceHourly DMS capacity; Schema Conversion free apart from S3 storageAurora pricing

Worked example: one table and one procedure

The source is a SQL Server table with an identity key and a procedure that returns a customer's most recent orders. The PostgreSQL version uses an identity column, boolean, timestamp and a function returning a table. The load is done with a pgloader command file that copies the dbo schema into public; pgloader documents its default options for database sources as creating tables, indexes and foreign keys and resetting sequences.

Source (T-SQL, SQL Server)
CREATE TABLE dbo.orders (
    order_id     INT IDENTITY(1,1) PRIMARY KEY,
    customer_id  INT           NOT NULL,
    status       NVARCHAR(20)  NULL,
    is_paid      BIT           NOT NULL DEFAULT 0,
    total_amount DECIMAL(12,2) NOT NULL,
    created_at   DATETIME2     NOT NULL DEFAULT GETDATE()
);
GO
CREATE PROCEDURE dbo.get_recent_orders @customer_id INT, @top_n INT
AS
BEGIN
    SET NOCOUNT ON;
    SELECT TOP (@top_n) order_id, ISNULL(status, N'new') AS status, created_at
    FROM dbo.orders
    WHERE customer_id = @customer_id
    ORDER BY created_at DESC;
END;
Target (PostgreSQL)
CREATE TABLE orders (
    order_id     integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    customer_id  integer       NOT NULL,
    status       varchar(20),
    is_paid      boolean       NOT NULL DEFAULT false,
    total_amount numeric(12,2) NOT NULL,
    created_at   timestamp     NOT NULL DEFAULT now()
);

CREATE OR REPLACE FUNCTION get_recent_orders(p_customer_id integer, p_top_n integer)
RETURNS TABLE (order_id integer, status text, created_at timestamp)
LANGUAGE plpgsql
AS $$
BEGIN
    RETURN QUERY
    SELECT o.order_id, COALESCE(o.status, 'new')::text, o.created_at
    FROM orders AS o
    WHERE o.customer_id = p_customer_id
    ORDER BY o.created_at DESC
    LIMIT p_top_n;
END;
$$;

-- T-SQL: EXEC dbo.get_recent_orders 42, 10;   PostgreSQL:
SELECT * FROM get_recent_orders(42, 10);
pgloader command file (mydb.load), run with: pgloader mydb.load
LOAD DATABASE
     FROM mssql://<user>:<your-password>@<sqlserver-host>/mydb
     INTO postgresql://<pg-user>:<your-password>@<pg-host>/mydb

ALTER SCHEMA 'dbo' RENAME TO 'public';
Validation and sequence check (PostgreSQL); run the same COUNT/SUM on SQL Server
SELECT COUNT(*) AS row_count, SUM(total_amount) AS total, MAX(order_id) AS max_id
FROM orders;

-- if you created the table yourself and loaded explicit ids,
-- move the identity past the highest copied value
SELECT setval(pg_get_serial_sequence('orders', 'order_id'),
              (SELECT MAX(order_id) FROM orders));

Testing, cutover and rollback

  • Compare data table by table with counts, sums and key ranges, then compare sample rows column by column. Watch for strings that change length because of collation or trailing spaces, and timestamps shifted by a time zone if you chose timestamptz.
  • Check case sensitivity. If your SQL Server collation is case-insensitive (its name contains _CI_), queries such as WHERE email = @email matched regardless of case. On PostgreSQL, the documented options are comparing lower() on both sides, the citext extension (a case-insensitive text type that calls lower internally), or a nondeterministic collation.
  • Run the application test suite against PostgreSQL, including reports and scheduled jobs that used SQL Agent.
  • Cut over with writes stopped, sequences reset, and connection strings switched; keep the SQL Server database untouched until the agreed rollback window closes.

For backups of the new database, see the related pg_dump guide; to manage it day to day, see pgAdmin or DBeaver. If you are learning PostgreSQL SQL after years of T-SQL, the SQL beginner course covers the shared basics.

Frequently asked questions

Can I migrate SQL Server to PostgreSQL for free?

Yes. pgloader is free and open source and converts tables and data in one command; you then rewrite stored procedures by hand. Paid routes such as AWS DMS add code conversion assistance and change replication.

How do I convert T-SQL stored procedures to PostgreSQL?

Rewrite them in PL/pgSQL: replace TOP with LIMIT, ISNULL with COALESCE, GETDATE with now(), + with ||, TRY...CATCH with an EXCEPTION block, and turn procedures that return rows into functions with RETURNS TABLE. DMS Schema Conversion can convert much of this automatically and flags what it cannot.

What is Babelfish for PostgreSQL?

A feature of Amazon Aurora PostgreSQL that understands the SQL Server wire protocol (TDS) and T-SQL, so applications built for SQL Server can connect with their existing drivers and run most queries with few changes. AWS documents the differences from SQL Server.

Does pgloader migrate SQL Server views and procedures?

pgloader can migrate views as if they were tables (materialising their data), but it does not convert procedures, functions or triggers. Convert those separately.

What does SQL Server NVARCHAR map to in PostgreSQL?

To varchar(n) or text. In a UTF8-encoded PostgreSQL database these types hold Unicode text, so a separate national character type is not needed. pgloader maps NVARCHAR to text by default.

Sources

Checked 8 October 2026.

How we research guides: our editorial method. We link only to official downloads and never host installers.

Database installed?

Write your first queries with the free beginner course, then practise on real problems.