---
title: "SQL Server to PostgreSQL migration guide"
description: "Convert a SQL Server script from SSMS into a PostgreSQL script with the same tables, keys and data: how to script, convert, load, and every rule applied."
canonical: https://intosql.com/docs/migrations/sql-server-to-postgresql
last_updated: 2026-10-15
---

# SQL Server to PostgreSQL migration guide

IntoSQL reads the script SSMS writes with Generate Scripts and writes one PostgreSQL script with the same tables, columns, keys, indexes, foreign keys, defaults, counters, comments and rows. It reads the script itself, without a database server, and lists in the report every value PostgreSQL can't hold as it is and every object it doesn't carry. The script loads on PostgreSQL 13 and later.

**Convert in the browser:** [SQL Server to PostgreSQL](/sql-server-to-postgresql) opens the converter set up for this pair.

## Script for the best result

In SQL Server Management Studio, right-click the database, choose **Tasks**, **Generate Scripts**, pick the tables, and under **Advanced** set **Types of data to script** to **Schema and data**. Save it as Unicode (SSMS's default) or UTF-8, as `.sql` or `.sql.gz`. dbatools (`Export-DbaScript`), mssql-scripter and other tools built on SQL Server Management Objects write the same script, from SQL Server 2017 on.

- A script of the data alone still converts: the types are read from the values, and the report says the schema was missing (`no_schema`).
- Tables on a partition scheme arrive as one table with every partition's rows (`partitions_dropped`), and a temporal table and its history table arrive as two plain tables.
- SSMS doesn't script the values of computed and `rowversion` columns; computed columns are written again, and `rowversion` columns are reported (`rowversion_empty`).

## Convert

In the browser, open [SQL Server to PostgreSQL](/sql-server-to-postgresql) or the converter on the home page, drop the script and choose PostgreSQL. It reads the file from your disk in pieces, twice, and writes the new script as it goes, so the file is never uploaded and its size isn't limited by memory. The [desktop app](/docs/desktop) does the same with the file on disk.

With the [command line](/docs/cli):

```shell
intosql inspect shop.sql
intosql convert shop.sql --to postgresql --output shop.postgresql.sql --report shop.report.json
```

`inspect` 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](/docs/api), a script converts like any other file: open a conversion with its name ending in `.sql` or `.sql.gz` and `"output": {"dialect": "postgresql"}`.

## Load into PostgreSQL

```shell
createdb shop
psql -v ON_ERROR_STOP=1 -d shop -f shop.postgresql.sql
```

The script creates the schemas and tables without keys, copies the rows with `COPY`, then adds keys, indexes, foreign keys, identities and sequences, so it loads fast and stops at the first error. Each schema stays a schema, and `dbo` is `public`. `bit` becomes `boolean`, `money` `numeric(19,4)`, `uniqueidentifier` `uuid`, `xml` `xml`, and `datetimeoffset` `timestamptz`. `datetime2` keeps six digits of a second, and the seventh is cut, never rounded (`fraction_cut`). Columns with a collation that ignores case compare exactly in PostgreSQL, and the report says how many there are (`case_insensitive_collation`).

### Or load it straight in

`intosql load` and the [desktop app](/docs/desktop) load the dump into PostgreSQL without a script, with the same tables, keys and indexes the script creates:

```shell
intosql load shop.sql --into postgres://ana@localhost/shop
```

It stops before writing anything when one of the dump's tables already exists. It loads in one transaction, so a failed load leaves nothing. Loading into a database server is part of IntoSQL Pro, with a 14-day trial. See [Loading](/docs/loading#database-dumps).

## What isn't converted

Views, functions, procedures, triggers, synonyms, rules and security policies are written in T-SQL. They are listed in the report by kind and name, never dropped silently, so you can recreate them in PostgreSQL.


## Data types

[SQL Server to PostgreSQL data types](/docs/types/sql-server-to-postgresql) 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 SQL Server | In PostgreSQL | Why | Report line |
| --- | --- | --- | --- |
| collations that ignore case (_CI_) | exact comparison | PostgreSQL compares text exactly, so columns that ignored case in SQL Server now tell Ana and ana apart. | `case_insensitive_collation` |
| CREATE SEQUENCE | CREATE SEQUENCE with the same options | PostgreSQL sequences work the same way. SSMS doesn't script where a sequence has reached, so it starts where it was created. | None |
| computed columns | generated columns, STORED | PostgreSQL 13 to 17 only store generated columns; the formula is the same, with CONCAT written so NULL counts as empty as in SQL Server. | `virtual_as_stored` |
| computed columns with functions PostgreSQL lacks | a plain column | A formula that can't be written for PostgreSQL leaves a plain column and a report line; SSMS scripts no values for computed columns. | `generated_not_translated` |
| defaults with functions PostgreSQL lacks, and NEXT VALUE FOR | no default | A default that can't be written for PostgreSQL is left out and reported. | `default_not_translated` |
| CHECK constraints | translated | The same checks, tested as each row loads. | None |
| CHECK constraints with functions PostgreSQL lacks | not created | A check that can't be written for PostgreSQL is left out and reported. | `check_skipped` |
| CHECK constraints added WITH NOCHECK or disabled | not created | SQL Server doesn't apply these checks, so the rows may break them. | `check_not_enforced` |
| filtered and columnstore indexes | not created | IntoSQL writes b-tree indexes on columns; create the others again after loading. | `index_skipped` |
| foreign keys | foreign keys, after the rows | The rows load in any order, and the keys check them afterwards. | None |
| 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` |
| tables on a partition scheme | one table with every partition's rows | The rows are the same; partition the table again in PostgreSQL if it needs it. | `partitions_dropped` |
| temporal tables (SYSTEM_VERSIONING) | two tables: the current rows and the history | The table and its history table are copied as plain tables; PostgreSQL doesn't keep the history by itself. | `system_versioning_dropped` |
| 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` |
| views, functions, procedures, triggers, synonyms, types and partition functions | listed in the report | Their code is written in T-SQL, so they are listed for you to recreate. | `not_converted_objects` |
| scripts without CREATE TABLE (data only) | types read from the values | A data-only script 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 script holds exactly these characters, so they are copied; the report points them out. | `mojibake_suspected` |
