Migration guides
PostgreSQL to SQL Server migration guide
Convert a PostgreSQL dump into a SQL Server script with the same tables, keys, indexes and data: how to dump, convert, load, and every rule the converter applies.
IntoSQL reads a plain PostgreSQL dump and writes one SQL Server script with the same tables, columns, keys, indexes, foreign keys, defaults, identities, comments and rows. It reads the dump itself, without a database server, and lists in the report every value SQL Server can't hold as it is and every object it doesn't carry. The script loads on SQL Server 2017 to 2025.
Convert in the browser: PostgreSQL to SQL Server opens the converter set up for this pair.
Dump for the best result
Any plain dump from pg_dump or pg_dumpall from PostgreSQL 10 on works, as .sql or .sql.gz, with COPY rows (the default) or --inserts:
pg_dump --no-owner --no-privileges shop | gzip > shop.sql.gz--no-owner --no-privilegesleave out roles and grants, which the converter skips anyway.- A custom or directory archive (
-Fc,-Fd) is binary:pg_restore -f shop.sql shop.backupturns it into plain SQL without a server. The converter says so when it gets an archive (dump_archive). pg_dumpallwrites every database in one file; each database's tables are named after it, so nothing collides.
A dump made with --data-only still converts: the types are read from the values, and the report says the schema was missing (no_schema). Partitioned tables arrive as one table with every partition's rows (partitions_dropped).
Convert
In the browser, open PostgreSQL to SQL Server or the converter on the home page, drop the dump and choose SQL Server. It reads the file from your disk in pieces, twice, and writes the script as it goes, so the dump is never uploaded and its size isn't limited by memory. The desktop app does the same with the file on disk.
With the command line:
intosql inspect shop.sql.gz
intosql convert shop.sql.gz --to sql-server --output shop.sql_server.sql --report shop.report.jsoninspect lists the tables, their rows and what won't be converted, without writing anything. convert writes the script and, with --report, the report as JSON.
Through the API, a dump converts like any other file: open a conversion with its name ending in .sql or .sql.gz and "output": {"dialect": "sql_server"}.
Load into SQL Server
sqlcmd -S localhost -Q "CREATE DATABASE shop"
sqlcmd -b -f 65001 -S localhost -d shop -i shop.sql_server.sql-b stops at the first error and -f 65001 reads the script as UTF-8. Each schema stays a schema, and public is dbo. Text columns compare exactly, as in PostgreSQL (Latin1_General_100_BIN2), and citext columns ignore case (Latin1_General_100_CI_AS_SC), so unique keys keep the same rows apart. The script creates the tables, copies the rows in batches of up to 1000 with IDENTITY_INSERT, then adds indexes, foreign keys, identities and comments. Foreign keys are added WITH NOCHECK and then checked: a row that points to a missing row is named by its constraint instead of failing the load half way.
What isn't converted
Views, materialized views, functions, procedures, triggers, rules, row security policies, extensions and types are written in PostgreSQL's own language. They are listed in the report by kind and name, never dropped silently, so you can recreate them in SQL Server.
Data types
PostgreSQL to SQL Server data types lists each type that changes in the script: what it becomes, why, and the report line.
Every other rule
Generated from the engine, where each rule is decided.
| In PostgreSQL | In SQL Server | Why | Report line |
|---|---|---|---|
| text comparison | exact (Latin1_General_100_BIN2); citext ignoring case (Latin1_General_100_CI_AS_SC) | SQL Server's usual collation ignores case, and PostgreSQL's text doesn't, so each column says how it compares and unique keys keep the same rows apart. | None |
| NOT NULL columns with values that become NULL | NOT NULL dropped | A column whose values had to become NULL can't keep NOT NULL, or the load would stop. | not_null_relaxed |
| sequences not owned by a column | CREATE SEQUENCE ... AS bigint, at its next value | SQL Server has sequences with the same options; each one continues where the dump left it. | None |
| generated columns | computed columns, PERSISTED when the source stores them | SQL Server computes them the same two ways. | None |
| generated columns with functions SQL Server lacks | a plain column with the values | A formula that can't be written for SQL Server leaves a plain column with the dump's values and a report line. | generated_not_translated |
| defaults with functions SQL Server lacks, and defaults from a sequence | no default | A default that can't be written for SQL Server is left out and reported. | default_not_translated |
| CHECK constraints | translated, inline in CREATE TABLE | Inline checks are tested as each row loads. | None |
| CHECK constraints with functions SQL Server lacks | not created | A check that can't be written for SQL Server is left out and reported. | check_skipped |
| CHECK ... NOT VALID | not created | PostgreSQL never checked the rows that were there before, so they may break it. | check_not_valid |
| gin, gist, brin and hash indexes, partial and expression indexes, and indexes on text that doesn't fit a key | not created | SQL Server indexes columns, not expressions, and not nvarchar(max). | index_skipped |
| foreign keys | after the data WITH NOCHECK, then checked with a warning on orphans | The key is created after the rows and checked last; rows without a parent leave it unchecked with a warning instead of stopping the load. | None |
| ON DELETE or ON UPDATE that cascades in a cycle or along a second path | NO ACTION | SQL Server refuses cascades that could reach a table twice; PostgreSQL allows them. | foreign_key_action_dropped |
| foreign keys to tables the dump doesn't create | not created | A key can't name a table that isn't there. | foreign_key_skipped |
| partitioned tables | one table with every partition's rows | The rows are the same; partition the table again in SQL Server if it needs it. | partitions_dropped |
| views, functions, procedures, triggers, rules, policies, extensions and types | listed in the report | Their code is written in PostgreSQL's language, so they are listed for you to recreate. | not_converted_objects |
| dumps without CREATE TABLE (--data-only) | types read from the values | A data-only dump has only rows, so the types come from the values. | no_schema |
| expressions in INSERT values | NULL | A converter that doesn't run a database can't evaluate them. | expression_value |
| text decoded twice (ñ) | copied as it is | The dump holds exactly these characters, so they are copied; the report points them out. | mojibake_suspected |