---
title: "PostgreSQL to SQL Server data types"
description: "Every PostgreSQL data type that SQL Server stores differently, what the converter writes for it, when, and the report line."
canonical: https://intosql.com/docs/types/postgresql-to-sql-server
last_updated: 2026-10-15
---

# PostgreSQL to SQL Server data types

Each row is a type from PostgreSQL that SQL Server stores differently: what the SQL Server script gets, why, and the line the report writes about it. The rows come from the converter's own rules, so this page matches what it does.

**Convert in the browser:** [PostgreSQL to SQL Server](/postgresql-to-sql-server) opens the converter set up for this pair. The [PostgreSQL to SQL Server migration guide](/docs/migrations/postgresql-to-sql-server) covers dumping, loading and the rules for keys, indexes and names.

A report line is a fix when the converter changed something and lists it, or a problem when a value or object is worth a look.

## Numbers

| In PostgreSQL | In SQL Server | Why | Report line |
| --- | --- | --- | --- |
| numeric(p,s) past 38 digits; numeric without a precision | nvarchar; decimal(p,s) fitted to the values when they fit 38 digits | SQL Server's decimal holds 38 digits. A numeric without a precision gets the narrowest decimal its values fit; wider numbers are kept as text so every digit stays exact. | `decimal_as_text` (fix) |
| money | decimal(19,2) | SQL Server's money is smaller than PostgreSQL's and has four decimals; decimal(19,2) holds every amount, without the currency symbol the server's locale added. | `type_replaced` (fix) |
| oid, xid, cid | bigint with a CHECK that it isn't negative | bigint holds every value. | None |

## Booleans and bits

| In PostgreSQL | In SQL Server | Why | Report line |
| --- | --- | --- | --- |
| boolean | bit | bit holds true as 1 and false as 0. | None |
| bit(n), bit varying | bit for one bit; bigint up to 63 bits; text digits beyond | A single bit is a yes or no; longer bit strings are stored as the number their bits make. | None |

## Text

| In PostgreSQL | In SQL Server | Why | Report line |
| --- | --- | --- | --- |
| varchar(n), char(n), text | nvarchar(n); text as nvarchar(max), or nvarchar(450) when a key uses it and every value fits | nvarchar holds every character. SQL Server can't index nvarchar(max), so text in a key is sized to fit a key. | None |
| text that isn't valid in its encoding | the bad bytes as the replacement character | nvarchar holds Unicode text. | `invalid_utf8_replaced` (problem) |

## Enums and sets

| In PostgreSQL | In SQL Server | Why | Report line |
| --- | --- | --- | --- |
| enum types | nvarchar with a CHECK of its labels | Text with a check keeps the same values. | None |
| enum columns holding values outside their labels | nvarchar without a check | Values the labels don't name would fail the check, so the check isn't created. | `enum_check_skipped` (problem) |

## Dates and times

| In PostgreSQL | In SQL Server | Why | Report line |
| --- | --- | --- | --- |
| interval | nvarchar(100) with PostgreSQL's text | SQL Server has no interval type; the text keeps every part. | `type_replaced` (fix) |
| timestamp | datetime2 | A timestamp without time zone is a wall-clock reading, like datetime2. | None |
| timestamp with time zone | datetimeoffset in UTC | datetimeoffset keeps the moment; each value is written in UTC, the same instant whatever offset the dump wrote. | None |
| time with 24:00:00 in it | nvarchar(17) | SQL Server's time ends at 23:59:59.9999999, so a column holding 24:00:00 keeps its text. | `time_as_interval` (problem) |
| infinity, -infinity and BC dates | NULL | SQL Server stores years 1 to 9999 without infinity. Those values become NULL and the report counts them. | `date_out_of_range` (problem) |

## Binary

| In PostgreSQL | In SQL Server | Why | Report line |
| --- | --- | --- | --- |
| bytea | varbinary(max) | Bytes stay bytes. | None |

## JSON, spatial and other types

| In PostgreSQL | In SQL Server | Why | Report line |
| --- | --- | --- | --- |
| arrays (integer[], text[]...) | nvarchar(max) with a JSON array | SQL Server has no arrays. A JSON array keeps every item and NULL, and OPENJSON reads it. | `type_replaced` (fix) |
| ranges, geometric types, tsvector, xml, hstore, PostGIS, composite and extension types | nvarchar(max) with PostgreSQL's text | SQL Server has no equivalent type, so each value keeps the text PostgreSQL wrote. | `type_replaced` (fix) |
| json, jsonb | nvarchar(max) | SQL Server's JSON functions read JSON from text. | None |
| NaN and Infinity in numeric, real and double precision | NULL | SQL Server's numbers have no NaN or Infinity, so those values become NULL and the report counts them. | `invalid_value_null` (problem) |
| uuid, inet, cidr, macaddr | uniqueidentifier; addresses as nvarchar | uniqueidentifier is SQL Server's UUID; addresses are kept as text. | None |
| types IntoSQL doesn't know | nvarchar(max) | Text holds any value; change the type after loading. | `unknown_type` (problem) |

## Counters and identities

| In PostgreSQL | In SQL Server | Why | Report line |
| --- | --- | --- | --- |
| serial, identity | IDENTITY, continuing after the highest id | The copied ids load with IDENTITY_INSERT, and the counter continues after the highest one or the sequence's next value, whichever is higher. | None |
| a second serial or identity column in a table | a plain integer | SQL Server counts one IDENTITY column per table; any other keeps its values without a counter. | `counter_dropped` (problem) |

## Other pairs

- PostgreSQL into [PostgreSQL](/docs/types/postgresql-to-postgresql), [MySQL](/docs/types/postgresql-to-mysql), [MariaDB](/docs/types/postgresql-to-mariadb), [SQLite](/docs/types/postgresql-to-sqlite), [Oracle](/docs/types/postgresql-to-oracle)
- Into SQL Server from [MySQL](/docs/types/mysql-to-sql-server), [MariaDB](/docs/types/mariadb-to-sql-server), [SQL Server](/docs/types/sql-server-to-sql-server), [SQLite](/docs/types/sqlite-to-sql-server), [Oracle](/docs/types/oracle-to-sql-server)
- [Every pair](/docs/types#from-one-database-to-another)
