Limited Time Offer: 40% off
Back to Blog

PostgreSQL vs SQL Server: Which Should You Choose?

JayJay

PostgreSQL vs SQL Server is closer than most database comparisons. Both are full-featured relational databases with strict typing, transactional schema changes, strong SQL support, and decades of production use. The biggest differences are cost, concurrency behaviour, and the ecosystem each one belongs to.

The short verdict: choose PostgreSQL for new applications, especially on Linux, containers, or any cloud other than Azure. It costs nothing to license at any scale, and its extension ecosystem covers vector search, geospatial data, and time series. Choose SQL Server when your organisation already runs on Microsoft, when your team has years of T-SQL, or when you rely on SSIS, Analysis Services, or Power BI integration.

All examples below were run in Docker against postgres:18 (PostgreSQL 18.6) and mcr.microsoft.com/mssql/server:2025-latest (SQL Server 2025 CU9), with default settings.

PostgreSQLSQL Server
LicencePostgreSQL License (permissive, free)Commercial; free Express and Developer editions
VendorCommunity projectMicrosoft
PlatformsLinux, Windows, macOS, BSDWindows and Linux
Procedural languagePL/pgSQL, plus others via extensionsT-SQL
Default read behaviourMVCC, readers never wait for writersShared locks, readers can wait for writers
Default string comparisonCase and accent sensitiveCase insensitive, accent sensitive
Identifier caseFolds unquoted names to lower caseFollows the collation (case insensitive by default)
Transactional DDLYesYes
UpsertON CONFLICT, MERGEMERGE
Return changed rowsRETURNINGOUTPUT
Table storageHeapClustered index on the primary key by default
JSONjsonb with GIN indexesNative json type (2025)
Vector searchpgvector extensionNative vector type (2025), vector index in preview
Temporal tablesNot built inSystem-versioned tables built in
Managed cloudEvery major cloud, plus Neon, Supabase, and othersAzure SQL, Amazon RDS, Google Cloud SQL

PostgreSQL vs SQL Server on cost

PostgreSQL is free to use, modify, and distribute under the PostgreSQL License, on any number of servers and cores. Paid support is available from several companies, but no licence is required.

SQL Server is free in two forms: Express, which is limited to the lesser of one socket or four cores, 1,410 MB of buffer pool memory, and 50 GB per database, and the Developer editions, which have every feature but are licensed for development and test only. Production beyond Express means Standard or Enterprise, licensed per core in two-core packs with a minimum of four core licences per processor. Standard edition in SQL Server 2025 is capped at the lesser of four sockets or 32 cores and 256 GB of buffer pool memory. The MySQL vs SQL Server post has the full edition table.

Many SQL Server features that matter in production, such as full availability groups, online index rebuilds, and several intelligent query processing features, are Enterprise only. The price difference grows as you scale, and PostgreSQL has no edition boundary to cross.

Concurrency: MVCC vs locking

This is the most important technical difference.

PostgreSQL uses multiversion concurrency control. An update writes a new row version, and readers keep seeing the last committed version until the writer commits. SQL Server's default read committed isolation uses shared locks, so a reader waits for a writer that holds a lock on the same row.

I opened a transaction that updated a row and held it for ten seconds, then read the row from a second session with a three second timeout:

SQL
-- SQL Server, session 1
BEGIN TRAN;
UPDATE stock SET qty = 0 WHERE sku = 'KB-01';
WAITFOR DELAY '00:00:10';
ROLLBACK;

-- SQL Server, session 2
SET LOCK_TIMEOUT 3000;
SELECT sku, qty FROM stock WHERE sku = 'KB-01';
Msg 1222, Level 16, State 51
Lock request time out period exceeded.

The same test in PostgreSQL, with pg_sleep(8) in session 1 and lock_timeout = '3s' in session 2, returned the committed row at once:

  sku  | qty
-------+-----
 KB-01 |  10

SQL Server can behave the same way. Enable read committed snapshot isolation on the database:

SQL
ALTER DATABASE shop SET READ_COMMITTED_SNAPSHOT ON WITH ROLLBACK IMMEDIATE;

After that, the reader in session 2 returned immediately. Microsoft's documentation says this option is off by default in SQL Server and on by default in Azure SQL Database. If you build on SQL Server, turning it on is usually the right choice.

Each approach has a maintenance cost. PostgreSQL leaves old row versions in the table and cleans them up with VACUUM, which update-heavy tables need tuned. SQL Server keeps row versions in tempdb when snapshot isolation is on, so tempdb sizing matters.

Collations, case, and Unicode

SQL Server's default collation in the Docker image is SQL_Latin1_General_CP1_CI_AS: case insensitive and accent sensitive. PostgreSQL compares strings exactly. In SQL Server, 'Alice' = 'alice' is true. In PostgreSQL, it is false.

The same rule applies to identifiers. SQL Server resolved both STOCK and Stock to the stock table and matched sku = 'kb-01' against KB-01. PostgreSQL folds unquoted identifiers to lower case, so STOCK works, but a table created as "Stock" with quotes can only be reached with quotes.

If you need case-insensitive columns in PostgreSQL, use a nondeterministic ICU collation:

SQL
CREATE COLLATION case_insensitive (provider = icu, locale = 'und-u-ks-level2', deterministic = false);
CREATE TABLE accounts (email text COLLATE case_insensitive UNIQUE);
INSERT INTO accounts VALUES ('zoe@example.com');
INSERT INTO accounts VALUES ('ZOE@example.com');
ERROR:  duplicate key value violates unique constraint "accounts_email_key"
DETAIL:  Key (email)=(ZOE@example.com) already exists.

Unicode handling differs more. PostgreSQL databases are normally UTF-8, and text or varchar store any character. In SQL Server, varchar under a legacy collation stores single-byte code page data, and anything outside it is replaced without an error:

1> CREATE TABLE notes (plain varchar(20), wide nvarchar(20));
2> INSERT INTO notes VALUES (N'Zoë 🎉', N'Zoë 🎉');
3> SELECT plain, wide FROM notes;
plain wide
----- ----
Zoë ?? Zoë 🎉

PostgreSQL stored Zoë 🎉 in a plain varchar(20). In SQL Server, use nvarchar or a _UTF8 collation for user-entered text.

T-SQL vs PostgreSQL SQL

Both are strict. SELECT '12abc' + 1 fails in both, as does SELECT 1/0, and both return 2 for SELECT 5/2. Both run DDL inside transactions, so a failed migration rolls back cleanly in either.

The dialects diverge on syntax. Each of these T-SQL habits fails in PostgreSQL:

postgres=# SELECT TOP 5 * FROM stock;
ERROR:  syntax error at or near "5"

postgres=# SELECT 'DB' + ' Pro';
ERROR:  operator is not unique: unknown + unknown
HINT:  Could not choose a best candidate operator. You might need to add explicit type casts.

postgres=# SELECT ISNULL(NULL, 0);
ERROR:  function isnull(unknown, integer) does not exist
TaskPostgreSQLSQL Server
Limit rowsLIMIT 5 or FETCH FIRST 5 ROWS ONLYTOP (5) or OFFSET 0 ROWS FETCH NEXT 5 ROWS ONLY
Auto-increment keyid int GENERATED ALWAYS AS IDENTITYid int IDENTITY(1,1)
Null fallbackCOALESCE(a, 0)ISNULL(a, 0) or COALESCE(a, 0)
Concatenateconcat(a, b) or the double pipe operatora + b, CONCAT(a, b), or the double pipe operator (2025)
Quote an identifier"order"[order] or "order"
Current timenow()GETDATE() or SYSDATETIME()

Procedural code is the bigger gap. T-SQL runs variables and control flow in any batch. PostgreSQL needs a function, a procedure, or an anonymous DO block:

SQL
-- SQL Server
DECLARE @n int = 3;
IF @n > 2 PRINT 'more than two';

-- PostgreSQL
DO $$
DECLARE n int := 3;
BEGIN
  IF n > 2 THEN RAISE NOTICE 'more than two'; END IF;
END $$;

Both printed more than two. Codebases with hundreds of T-SQL stored procedures are the main reason SQL Server migrations stall.

Upserts and returning rows

PostgreSQL has INSERT ... ON CONFLICT, which is atomic under concurrency, and MERGE. SQL Server has MERGE:

SQL
-- PostgreSQL
INSERT INTO stock (sku, qty) VALUES ('KB-01', 3)
ON CONFLICT (sku) DO UPDATE SET qty = stock.qty + EXCLUDED.qty
RETURNING sku, qty;

-- SQL Server
MERGE stock AS s
USING (VALUES ('KB-01', 3), ('MS-02', 7)) AS v (sku, qty) ON s.sku = v.sku
WHEN MATCHED THEN UPDATE SET qty = s.qty + v.qty
WHEN NOT MATCHED THEN INSERT (sku, qty) VALUES (v.sku, v.qty)
OUTPUT $action, inserted.sku, inserted.qty;
$action sku qty
------- --- ---
UPDATE KB-01 8
INSERT MS-02 7

PostgreSQL's MERGE ... RETURNING merge_action() produces the same kind of output. Microsoft's MERGE documentation notes that concurrent upserts on unique keys may need a HOLDLOCK hint to avoid key violations. For single-row upserts, PostgreSQL's ON CONFLICT is simpler to get right. See our ON CONFLICT guide.

DB Pro

Work With Your Databases Like A Pro

Query, explore, and manage your databases with a beautiful desktop app and built-in AI.

Download Now
DB Pro Dashboard

JSON, vectors, and data types

SQL Server 2025 added a native json type, a vector type, and regular expression functions. All three worked in the container:

SQL
CREATE TABLE events (
  id bigint IDENTITY(1,1) PRIMARY KEY,
  payload json NOT NULL,
  embedding vector(3)
);
SELECT id, JSON_VALUE(payload, '$.plan') AS [plan] FROM events;
SELECT CASE WHEN REGEXP_LIKE('KB-01', '^[A-Z]{2}-[0-9]+$') THEN 1 ELSE 0 END;

Vector indexes and VECTOR_SEARCH still require the PREVIEW_FEATURES database scoped configuration in SQL Server 2025.

PostgreSQL has had jsonb with GIN indexing for years, plus JSON path queries. Vector search comes from the pgvector extension, which offers HNSW and IVFFlat indexes and is available on most managed PostgreSQL services. We cover it in pgvector.

PostgreSQL also has types SQL Server lacks: arrays, range types, inet, enums, and user-defined composite types. PostgreSQL 18 added uuidv7() for time-ordered UUIDs and temporal PRIMARY KEY and UNIQUE constraints with WITHOUT OVERLAPS.

SQL Server has built-in features PostgreSQL needs extensions or application code for:

  • System-versioned temporal tables. SQL Server keeps row history automatically. After an insert at 49.00 and an update to 59.00, SELECT sku, price FROM prices FOR SYSTEM_TIME ALL returned both rows.
  • Columnstore indexes for analytical queries on the same tables as transactional data.
  • Query Store, on by default for new databases, which records plans and runtime statistics and supports plan forcing. PostgreSQL's closest equivalent is the pg_stat_statements extension, which tracks statement statistics but does not force plans.
  • SQL Server Agent for scheduled jobs. PostgreSQL uses the pg_cron extension or an external scheduler.

Storage and indexing

SQL Server stores a table in its clustered index, which a PRIMARY KEY creates by default. Rows are physically ordered by the key, which makes range scans on the key efficient and makes a random primary key such as a UUIDv4 expensive on inserts.

PostgreSQL stores rows in a heap, and every index, including the primary key, points into it. The CLUSTER command reorders a table once but does not maintain the order. In exchange, PostgreSQL offers more index types: B-tree, hash, GIN, GiST, SP-GiST, and BRIN, plus partial and expression indexes.

High availability and replication

PostgreSQL includes streaming replication and logical replication. Automatic failover is handled by external tools such as Patroni or by your managed service. Every feature is available without a licence tier.

SQL Server's Always On availability groups provide automatic failover and readable secondaries, but full availability groups need Enterprise edition. Standard edition supports basic availability groups with two replicas and one database per group, plus two-node failover cluster instances.

Tools and ecosystem

SQL Server's tooling is extensive and consistent: SQL Server Management Studio (Windows only), SQL Server Agent, Integration Services, Analysis Services, Power BI Report Server, and integration with Active Directory, Microsoft Entra ID, Visual Studio, and .NET.

PostgreSQL's tooling is spread across projects: psql, pgAdmin, many third-party clients, and an extension ecosystem that includes PostGIS, pgvector, TimescaleDB, and Citus. Almost every ORM, framework, and hosting platform supports it first. Best PostgreSQL GUI clients compares the desktop options.

DB Pro connects to both from macOS, Windows, and Linux. See the PostgreSQL client and SQL Server client pages.

Cloud options

SQL Server runs best on Azure: Azure SQL Database, Azure SQL Managed Instance, and SQL Server on Azure virtual machines. Amazon RDS and Google Cloud SQL also offer it, usually with the licence included in the hourly rate.

PostgreSQL is available on every major cloud, including Azure Database for PostgreSQL, and on developer-focused platforms such as Neon and Supabase. On AWS, Aurora PostgreSQL is a common choice, and Aurora vs RDS covers the trade-offs.

Migrating from SQL Server to PostgreSQL

Licence cost drives most of these migrations. Plan for the following:

  1. Convert the schema. Map nvarchar to text or varchar, datetime2 to timestamp, bit to boolean, IDENTITY to identity columns, and uniqueidentifier to uuid.
  2. Rewrite T-SQL. Stored procedures, functions, and triggers need rewriting in PL/pgSQL. This is usually the largest part of the project.
  3. Check collation-dependent queries. Case-insensitive lookups in SQL Server return fewer rows in PostgreSQL. Add ICU collations, citext, or lower() indexes.
  4. Review locking assumptions. Code that relied on blocking reads for correctness needs explicit SELECT ... FOR UPDATE.
  5. Consider Babelfish. On AWS, Babelfish for Aurora PostgreSQL accepts connections over SQL Server's TDS protocol and understands much of T-SQL, which can shorten the move. Check its compatibility list against your code first.

When to choose PostgreSQL

  • You are starting a new application with no Microsoft dependency.
  • Licence cost matters now or at the scale you plan for.
  • You want vector search, geospatial queries, or time series in the main database today.
  • You deploy on Linux, containers, or a cloud other than Azure.
  • You want the widest choice of managed hosting.

When to choose SQL Server

  • Your organisation runs Windows Server, Active Directory, .NET, and Azure.
  • You depend on SSIS, Analysis Services, or Power BI Report Server.
  • Your team has a large T-SQL codebase that would be expensive to rewrite.
  • You want built-in temporal tables, Query Store, and columnstore without extensions.
  • You want one vendor accountable for the database and its tooling.

The verdict

For new projects, PostgreSQL is the better default. It matches SQL Server on strictness and transactional DDL, avoids reader blocking by default, and costs nothing to license. Its extensions cover most of what SQL Server 2025 added.

SQL Server remains a strong database, and inside a Microsoft-centred organisation its tools and support can be worth the licence. If you are comparing the open source options too, see PostgreSQL vs MySQL and MySQL alternatives.

Keep Reading