Server switch

PostgreSQL for a SQL Server professional

What carries over, what quietly does not, and the design decisions that cost the most when you learn them late.

SQL Server PostgreSQL Instance Cluster Database A Database B one connection joins both Database A Database B one connection sees one Where you draw the database line is the costliest decision to reverse.
SQL Server Instance Database A Database B one connection joins both PostgreSQL Cluster Database A Database B one connection sees one
$0license, every feature, any size
1/yra new major release, each supported five years
~100default max_connections, one OS process each
32 bitthe transaction ID that has to be frozen

The commercial picture

Free is the smallest difference

The licence is the part you adjust to fastest. The release cadence is the part that changes how you plan your year.

SQL ServerPostgreSQL
LicencePer core, plus CALs. The edition you buy sets the feature list.Free, all features. One edition: no Express 10 GB cap, no Enterprise-only index.
SupportAbout ten years per release, from a vendor you can escalate to.Five years per major, from the community. A paid contract is optional: EDB, Crunchy Data, others.
CadenceA new major every two or three years. You patch; you rarely re-platform.A new major every autumn. Minors every quarter. You upgrade majors every two or three years regardless.
UpgradeIn place with setup, or an availability group to shorten the cutover.Minor: swap binaries, restart. Major: pg_upgrade or dump and restore. The data directory format changes.
Put the upgrade on the calendar.

PostgreSQL 14 reaches end of life on 12 November 2026, five years after it shipped. Anything you stand up today carries a known expiry, and the clock starts on day one, not when you notice. That is the habit to build: a major upgrade is routine maintenance you schedule, not a project you fund.

The shape of a server

One cluster, many databases, no bridges

This is the difference that breaks the most migrations. In SQL Server a database is a namespace. In PostgreSQL it is a separate catalog with its own connections.

No sa, and no fixed server roles.

The bootstrap superuser is postgres and it is an ordinary account. There is no db_owner; you grant usage on the schema, select on the tables, and alter default privileges so tomorrow's tables behave the same. Built-in roles such as pg_read_all_data and pg_monitor cover the broad cases. And since 15, the public schema grants nobody create by default: the first "permission denied for schema public" is almost always that.

Storage

There is no clustered index

In SQL Server the primary key often is the table, and its order is the physical order. Here a table is an unordered heap and every index is a separate structure pointing into it.

Concurrency

Readers never block writers

SQL Server defaults to locking: a reader waits for an uncommitted writer. PostgreSQL defaults to snapshotting: a reader sees the last committed version and never waits. What you get for free, something has to clean up.

The dialect

Same SQL, different words

About a dozen constructs account for most of the pain. Everything else is close enough that you will not notice it until something returns the wrong type.

SQL ServerPostgreSQLWhy it bites
select top 10limit 10A parse error, not a warning.
getdate()now()Returns timestamptz, not a naive timestamp.
isnull(a, b)coalesce(a, b)No isnull function exists.
a + ba || b+ is arithmetic only; concatenation errors.
[brackets]"quotes"Brackets never parse. Single quotes are always strings.
go—Client-side batching. psql uses ; and backslash commands.
identity(1,1)generated always as identityserial is the older way and leaves an orphan sequence.
mergeinsert ... on conflictmerge exists; on conflict is the idiom.
scope_identity()returning idThe whole inserted row comes back, not just a number.
select * into #tcreate temp table asTemp tables are session-scoped, not block-scoped.
sp_who2pg_stat_activityThis is where the DMVs went. \d table is sp_help.
psql replaces SSMS.

\d describes an object, \l lists databases, \dn lists schemas, \x flips to expanded output for wide rows. There is no live plan diagram and no plan cache to click into: explain (analyze, buffers) is the plan, and reading it is the skill you will actually be paid for.

Data types

The types map, until they do not

Most of the mapping is obvious. The dangerous part is that two types collide on one word, and the shorter name is the one with the trap.

SQL ServerUse insteadWatch out
nvarchar(n)textNo speed difference. varchar(n) only adds a length check.
nvarchar(max)textNo separate LOB type; large values move out of line on their own.
datetimetimestampNo time zone. This is the trap that keeps taking money.
datetimeoffsettimestamptzStored as UTC, shown in the session TimeZone.
bitbooleanReal true and false. No 0/1 arithmetic.
moneynumeric(19,4)PostgreSQL's money is locale-dependent. Do not use it.
uniqueidentifieruuid16 bytes, and not SQL Server's sort order.
int identityinteger generated always as identityOr bigint. Sequence gaps are normal.
varbinary(max)byteaLarge objects exist, but bytea is the default answer.
xmljsonbIndexable with GIN, and usually what you actually wanted.
sql_variantjsonbNothing replaces it; jsonb is the nearest thing.
hierarchyidltreeAn extension, not core.
Two NULL rules that will differ.

Pick timestamptz by default and convert to a local timestamp only at the edge of the application. And a unique index here accepts any number of NULLs, where SQL Server allows exactly one; if you need that back, declare unique nulls not distinct on 15 or later.

Running it

Backups, failover and the missing Agent

Almost none of the SQL Server tooling around the engine exists. Each replacement is smaller, separate, and better behaved once it is wired up.

You are used toIn PostgreSQL
SQL Server Agentpg_cron, systemd timers, or cron. No scheduler ships with the server.
Full, differential and log backupspg_dump for one database, or a base backup plus continuous WAL for point-in-time restore. There is no differential.
Database MailNothing built in. Send from the application or a script.
Linked Serverspostgres_fdw, dblink, or a foreign data wrapper.
Always On availability groupsStreaming replication plus Patroni or repmgr. The server does not fail over by itself.
Transactional replicationLogical replication: create publication, create subscription.
Query Storepg_stat_statements for the aggregate, auto_explain for the slow ones. No plan forcing.
SSIS, SSRS, SSASNot in the box. PostgreSQL is an engine, not a platform.
sys.dm_os_wait_statspg_stat_activity.wait_event, sampled per session.

Your first week

  1. Install a supported major, not the newest. 18 is current; 19 is a release candidate with general availability planned for 19 November 2026, so it is not a day-one choice.
  2. Size it before you tune it. shared_buffers around a quarter of RAM, max_connections near 100, and PgBouncer in front if the application pools badly.
  3. Turn on pg_stat_statements and auto_explain. Without them you are blind in a way Query Store never let you be.
  4. Decide the database boundary once. One database with many schemas, or many databases bridged by postgres_fdw or the application. This is the decision you cannot cheaply undo.
  5. Port the schema by hand. Transliterate the syntax, then re-decide every type, starting with every datetime that should have been timestamptz.
  6. Prove the backup. Base backup plus WAL archiving to object storage, then a real restore onto a scratch server before you trust the schedule.
  7. Rehearse the upgrade. Run pg_upgrade against a copy now, so the first attempt is not the one that matters.
What you pick up in the trade.

Extensions are the real prize: postgis, pgvector, pg_partman, pg_trgm, pg_cron, timescaledb. create extension installs a capability that in SQL Server would have been a separate product with its own licence. jsonb with GIN indexes, arrays, ranges and copy for bulk load come with the server.