This week I went back through my PostgreSQL notes and listed what is different between SQL Server and Postgres. These are the things I would pay close attention to when doing a migration.
Most migration issues are not caused by the data. They come from assuming Postgres works the same way as SQL Server.
Case Sensitivity
One thing I learned is that Postgres does not handle object names the same way as SQL Server. If you create an object with quotes, Postgres keeps the exact case. If you leave the quotes off, Postgres converts the name to lowercase. String comparisons are another thing I would check. A lot of SQL Server environments are case insensitive by default. Postgres is usually case sensitive unless you set that up yourself.
I would check table names, column names, schema names, application code, ORM mappings, and anything using quoted identifiers. If possible, I would stick with lowercase object names.
Indexes
Indexes do not always translate the same way when you migrate them over. In SQL Server, the clustered index controls the table order. Postgres stores tables as heaps. Postgres does have a CLUSTER command, but it only rewrites the table once. New rows do not stay in that order.
I would review included columns, filtered indexes versus partial indexes, fill factor, statistics, and query plans. Recreating the same indexes does not mean you will get the same performance.
PostgreSQL Versions
Postgres releases a major version about once a year and supports each version for five years. I have found a lot of companies running older versions, so I would check the version early in the migration. I would look at end-of-life dates, features the application needs, extension support, upgrade paths, and what the cloud provider offers.
As of late 2026, PostgreSQL 13 is already out of community support. PostgreSQL 14 reaches the end of community support in November 2026.
Extensions
In a previous post, I talked about alternatives to SQL Server Agent. A lot of extra Postgres features come from extensions. One thing I learned is that not every extension is available everywhere. I would check whether the cloud provider supports it, whether it needs superuser access, whether it needs extra configuration, and what can be used if it is not something available. Managed Postgres services do not all offer the same extensions.
Managed Versus Self-Managed
Managed and self-managed Postgres are not the same. Managed services usually size some settings for you. Self-managed servers often start with conservative defaults. I would review shared buffers, work memory, maintenance work memory, connection limits, and autovacuum.
I was surprised by how low some of the default memory settings can be. Fresh installs can still have shared_buffers set around 128 MB.
I would also check the number of connections used by the application. Postgres uses a process for each connection. Applications that open hundreds of connections will probably need a connection pooler.
Job Scheduling
Jobs are not SQL Server Agent. For years, people scheduled Postgres jobs with cron. pg_cron makes it easier to schedule SQL inside Postgres, but it is not a full replacement for SQL Server Agent. pg_cron runs SQL, not shell scripts. If the instance is down, the job does not run.
Other choices include pg_timetable and cloud schedulers. Before cutover, I would list every maintenance job, backup job, ETL job, and monitoring job. I would make sure each job has a replacement and test it before the migration.
Stored Procedures
If the environment uses a lot of stored procedures, I would budget time for them. Postgres has procedures and functions. They are not written like T-SQL. I would review syntax, transactions, error handling, dynamic SQL, temporary tables, and how results come back. Multiple result sets from one procedure are common in SQL Server. They do not move cleanly to Postgres.
Identity Columns and Sequences
SQL Server identities do not always translate directly. Postgres has IDENTITY, SERIAL, and standalone sequences.
Data Types
Some SQL Server data types do not have a direct match in Postgres. I would check money, uniqueidentifier, rowversion, tinyint, bit, datetime types, sql_variant, hierarchyid, and CLR types. Each one needs to be mapped and tested before the migration. Also, rowversion is not a date or time type.
Linked Servers
Linked servers do not exist in Postgres. The usual replacements are postgres_fdw and dblink. I would also check what each linked server is being used for. Replacing the connection does not mean the existing queries will work without changes.
Database Mail
Postgres does not have a direct replacement for Database Mail. If the application or a job depends on it, I would plan a replacement before cutover.
Cross-Database Queries
SQL Server databases on the same instance can query each other. Postgres databases are isolated. A lot of migrations use schemas in one Postgres database instead of creating several databases. I would search the SQL Server code for three-part names before deciding how to set up the Postgres databases and schemas.
Security and Permissions
I would review roles, role inheritance, authentication, service accounts, and search_path settings. Postgres roles belong to the whole cluster. The search_path controls which objects Postgres finds when the schema name is left out.
Collation
Collation needs to be checked before the move. Sorting, unique constraints, and string comparisons may not match what SQL Server was doing. This can change query results even when the data moved correctly.
Temporary Tables
Postgres temporary tables belong to the session. They can also be set to drop when the transaction commits. There is no global temporary table like a SQL Server ##table. I would also check connection pooling. A temporary table created on one connection will not be there if the next statement uses another connection.
Date Functions
GETDATE(), GETUTCDATE(), DATEADD(), DATEDIFF(), and DATEPART() all need Postgres replacements. I would also check how the application handles time zones. Postgres has timestamp with time zone and timestamp without time zone.
Vacuum and Bloat
Postgres keeps old row versions until vacuum cleans them up. Long-running transactions can stop vacuum from removing those rows. This can leave tables and indexes bloated. I would check autovacuum settings, long-running transactions, and tables with a lot of updates or deletes.
Query Hints
A lot of SQL Server hints do not exist in Postgres. NOLOCK, FORCESEEK, and INDEX hints need another approach. NOLOCK is also the wrong way to think about Postgres. Postgres uses MVCC, so readers and writers usually do not block each other.
High Availability
Always On Availability Groups do not translate directly to Postgres. I would review replication, failover, read replicas, backups, and restore tests. Postgres failover often uses streaming replication with other tools or a cloud high-availability service. I would test failover and restores before the migration. I would not assume the cloud service handles everything.
My Thoughts
Most migration issues are not caused by the data. They come from assuming Postgres works the same way as SQL Server.
These are the things I would check before the move. It is better to find them while you still have time to fix them than after the migration.