Quem migra do SQL Server para o PostgreSQL leva um susto inicial: a maioria das funções existe, mas com nome diferente, parâmetros invertidos ou sintaxe ligeiramente distinta. Este guia cobre os principais casos.
Funções de data e hora
A maior diferença começa aqui. O SQL Server usa FORMAT e DATEADD/DATEDIFF para quase tudo; o PostgreSQL preferere TO_CHAR e aritmética direta com INTERVAL.
Data atual
| SQL Server | PostgreSQL |
|---|---|
GETDATE() | NOW() ou CURRENT_TIMESTAMP |
GETUTCDATE() | NOW() AT TIME ZONE 'UTC' |
SYSDATETIME() | CLOCK_TIMESTAMP() |
Formatação de datas
No SQL Server, FORMAT(date, 'dd/MM/yyyy') usa o formato .NET. No PostgreSQL, TO_CHAR usa máscaras próprias:
-- SQL Server
SELECT FORMAT(GETDATE(), 'dd/MM/yyyy') -- '28/05/2026'
SELECT FORMAT(GETDATE(), 'yyyy-MM-dd HH:mm') -- '2026-05-28 14:30'
-- PostgreSQL equivalente
SELECT TO_CHAR(NOW(), 'DD/MM/YYYY') -- '28/05/2026'
SELECT TO_CHAR(NOW(), 'YYYY-MM-DD HH24:MI') -- '2026-05-28 14:30'Máscaras mais usadas no TO_CHAR:
| Máscara PG | Resultado | Equivalente .NET |
|---|---|---|
YYYY | 2026 | yyyy |
MM | 05 | MM |
DD | 28 | dd |
HH24 | 14 | HH |
MI | 30 | mm |
SS | 45 | ss |
DAY | WEDNESDAY | dddd |
Month | May | MMMM |
Adicionar e subtrair datas
O PostgreSQL troca DATEADD por aritmética com INTERVAL:
-- SQL Server
SELECT DATEADD(day, 7, GETDATE())
SELECT DATEADD(month, 1, GETDATE())
SELECT DATEADD(year, -1, GETDATE())
-- PostgreSQL equivalente
SELECT NOW() + INTERVAL '7 days'
SELECT NOW() + INTERVAL '1 month'
SELECT NOW() - INTERVAL '1 year'Diferença entre datas
DATEDIFF não existe no PostgreSQL. Para dias, subtração direta funciona; para outras unidades, use EXTRACT ou DATE_PART:
-- SQL Server
SELECT DATEDIFF(day, '2026-01-01', GETDATE())
SELECT DATEDIFF(month, '2026-01-01', GETDATE())
-- PostgreSQL equivalente
SELECT CURRENT_DATE - '2026-01-01'::date -- dias (retorna INTEGER)
SELECT EXTRACT(YEAR FROM AGE(NOW(), '2026-01-01'::date)) * 12
+ EXTRACT(MONTH FROM AGE(NOW(), '2026-01-01'::date)) -- mesesExtrair partes de uma data
-- SQL Server
SELECT YEAR(GETDATE()), MONTH(GETDATE()), DAY(GETDATE())
-- PostgreSQL: duas opções equivalentes
SELECT EXTRACT(YEAR FROM NOW()),
EXTRACT(MONTH FROM NOW()),
EXTRACT(DAY FROM NOW())
SELECT DATE_PART('year', NOW()),
DATE_PART('month', NOW()),
DATE_PART('day', NOW())Outras funções de data
| SQL Server | PostgreSQL |
|---|---|
EOMONTH(date) | DATE_TRUNC('month', date) + INTERVAL '1 month' - INTERVAL '1 day' |
DATEFROMPARTS(y, m, d) | MAKE_DATE(y, m, d) |
CAST('2026-05-28' AS DATE) | '2026-05-28'::date ou CAST('2026-05-28' AS DATE) |
ISDATE(str) | sem equivalente direto; use TO_DATE com tratamento de erro |
Funções de string
Comprimento e posição
| SQL Server | PostgreSQL |
|---|---|
LEN(str) | LENGTH(str) ou CHAR_LENGTH(str) |
CHARINDEX(busca, str) | POSITION(busca IN str) ou STRPOS(str, busca) |
PATINDEX('%pat%', str) | sem equivalente direto; use regex com ~ |
Atenção na ordem dos parâmetros: CHARINDEX(busca, str) inverte em relação ao STRPOS(str, busca).
-- SQL Server
SELECT CHARINDEX('lo', 'hello world') -- 4
-- PostgreSQL
SELECT POSITION('lo' IN 'hello world') -- 4
SELECT STRPOS('hello world', 'lo') -- 4Substrings
-- SQL Server
SELECT SUBSTRING('hello world', 7, 5) -- 'world'
SELECT LEFT('hello', 3) -- 'hel'
SELECT RIGHT('hello', 3) -- 'llo'
-- PostgreSQL (SUBSTRING e LEFT/RIGHT são iguais)
SELECT SUBSTRING('hello world' FROM 7 FOR 5) -- 'world'
SELECT SUBSTR('hello world', 7, 5) -- 'world' (sintaxe alternativa)
SELECT LEFT('hello', 3) -- 'hel' (igual)
SELECT RIGHT('hello', 3) -- 'llo' (igual)Substituição dentro de strings
-- SQL Server: STUFF(str, inicio, qtd_chars, substituicao)
SELECT STUFF('hello world', 7, 5, 'there') -- 'hello there'
-- PostgreSQL: OVERLAY
SELECT OVERLAY('hello world' PLACING 'there' FROM 7 FOR 5) -- 'hello there'
-- REPLACE funciona igual nos dois
SELECT REPLACE('hello world', 'world', 'there')Repetição e outras
| SQL Server | PostgreSQL |
|---|---|
REPLICATE(str, n) | REPEAT(str, n) |
REVERSE(str) | REVERSE(str) (igual) |
LTRIM(str) / RTRIM(str) | igual |
TRIM(str) | igual (disponível no SQL Server 2017+ e PG 9.1+) |
UPPER(str) / LOWER(str) | igual |
SPACE(n) | REPEAT(' ', n) |
STR(num, tamanho, decimais) | TO_CHAR(num, 'formato') |
Concatenação
No PostgreSQL, o operador + não concatena strings. Use || ou CONCAT:
-- SQL Server
SELECT 'hello' + ' ' + 'world'
-- PostgreSQL
SELECT 'hello' || ' ' || 'world'
SELECT CONCAT('hello', ' ', 'world') -- também funciona, ignora NULLsLIKE e busca case-insensitive
No SQL Server, LIKE é case-insensitive por padrão (depende do collation). No PostgreSQL, LIKE é case-sensitive. Use ILIKE para ignorar maiúsculas:
-- PostgreSQL
SELECT * FROM clientes WHERE nome LIKE 'JOAO%' -- case-sensitive
SELECT * FROM clientes WHERE nome ILIKE 'joao%' -- case-insensitiveConversão de tipos
O PostgreSQL oferece um atalho de cast com :: que o SQL Server não tem:
-- SQL Server
SELECT CAST(42 AS VARCHAR)
SELECT CONVERT(VARCHAR, GETDATE(), 103) -- com código de formato
-- PostgreSQL
SELECT CAST(42 AS VARCHAR) -- mesma sintaxe CAST
SELECT 42::TEXT -- atalho com ::
SELECT NOW()::DATE -- casting de timestamp para date
SELECT TO_CHAR(NOW(), 'DD/MM/YYYY') -- equivalente ao CONVERT com códigoTRY_CAST e TRY_CONVERT
O PostgreSQL não tem equivalente nativo. A alternativa é uma função com tratamento de exceção ou regexp para validar antes de converter:
-- SQL Server
SELECT TRY_CAST('abc' AS INT) -- retorna NULL sem erro
-- PostgreSQL: sem equivalente direto
-- Opção 1: regexp antes de converter
SELECT CASE WHEN '123' ~ '^[0-9]+$' THEN '123'::INT ELSE NULL END
-- Opção 2: função customizada com EXCEPTION
CREATE OR REPLACE FUNCTION try_cast_int(p_text TEXT)
RETURNS INT AS $$
BEGIN
RETURN p_text::INT;
EXCEPTION WHEN OTHERS THEN
RETURN NULL;
END;
$$ LANGUAGE plpgsql;Formatação de números
-- SQL Server
SELECT FORMAT(1234567.89, 'N2') -- '1,234,567.89'
SELECT FORMAT(0.45, 'P0') -- '45%'
-- PostgreSQL com TO_CHAR
SELECT TO_CHAR(1234567.89, 'FM999,999,990.00') -- '1,234,567.89'
SELECT TO_CHAR(0.45, 'FM990.00%') -- '45.00%'Máscaras numéricas do TO_CHAR:
| Máscara | Significado |
|---|---|
9 | dígito, suprime zero à esquerda |
0 | dígito, mantém zero à esquerda |
FM | remove espaços extras do resultado |
. | separador decimal |
, | separador de milhar |
% | multiplica por 100 e adiciona % |
Condicionais
| SQL Server | PostgreSQL |
|---|---|
IIF(cond, verdadeiro, falso) | CASE WHEN cond THEN verdadeiro ELSE falso END |
CHOOSE(idx, val1, val2, ...) | sem equivalente; use CASE |
NULLIF(a, b) | NULLIF(a, b) (igual) |
ISNULL(a, b) | COALESCE(a, b) |
COALESCE(a, b, ...) | COALESCE(a, b, ...) (igual) |
-- SQL Server
SELECT IIF(saldo > 0, 'positivo', 'negativo')
-- PostgreSQL
SELECT CASE WHEN saldo > 0 THEN 'positivo' ELSE 'negativo' ENDPaginação
-- SQL Server
SELECT TOP 10 * FROM pedidos ORDER BY data DESC
SELECT * FROM pedidos
ORDER BY data DESC
OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY
-- PostgreSQL
SELECT * FROM pedidos ORDER BY data DESC LIMIT 10
SELECT * FROM pedidos
ORDER BY data DESC
LIMIT 10 OFFSET 20Agrupamento e agregação
A maioria funciona igual. As diferenças ficam nas funções estatísticas:
| SQL Server | PostgreSQL |
|---|---|
STDEV(col) | STDDEV(col) |
VAR(col) | VARIANCE(col) |
STRING_AGG(col, sep) | STRING_AGG(col, sep) (igual, PG 9.0+) |
COUNT_BIG(*) | sem equivalente; COUNT(*) já retorna BIGINT no PG |
Tabelas temporárias e variáveis
-- SQL Server
SELECT * INTO #temp FROM pedidos WHERE status = 'pendente'
CREATE TABLE #temp (id INT, nome VARCHAR(100))
DECLARE @variavel INT = 10
-- PostgreSQL
CREATE TEMP TABLE temp AS SELECT * FROM pedidos WHERE status = 'pendente'
CREATE TEMPORARY TABLE temp (id INT, nome VARCHAR(100))
-- variáveis só existem dentro de funções/procedures com DO $$ ... $$Outros elementos de sintaxe
| SQL Server | PostgreSQL | Observação |
|---|---|---|
PRINT 'mensagem' | RAISE NOTICE 'mensagem' | para debug em scripts |
IDENTITY(1,1) | SERIAL ou GENERATED ALWAYS AS IDENTITY | auto-increment |
@@IDENTITY / SCOPE_IDENTITY() | RETURNING id / lastval() | retornar id inserido |
WITH (NOLOCK) | sem equivalente direto | PG usa MVCC; considere READ UNCOMMITTED |
GO | não existe | separador de batches; não é necessário no PG |
SET NOCOUNT ON | não existe | PG não exibe “x rows affected” por padrão em clients |
[nome com espaço] | "nome com espaço" | identificadores com espaço usam aspas duplas |
Identidade gerada
-- SQL Server
CREATE TABLE pedidos (
id INT IDENTITY(1,1) PRIMARY KEY,
descricao VARCHAR(200)
)
-- PostgreSQL opção 1: SERIAL (legado, ainda muito usado)
CREATE TABLE pedidos (
id SERIAL PRIMARY KEY,
descricao TEXT
)
-- PostgreSQL opção 2: padrão SQL moderno (PG 10+)
CREATE TABLE pedidos (
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
descricao TEXT
)Retornar o ID inserido
-- SQL Server
INSERT INTO pedidos (descricao) VALUES ('teste')
SELECT SCOPE_IDENTITY()
-- PostgreSQL: RETURNING é mais limpo
INSERT INTO pedidos (descricao) VALUES ('teste')
RETURNING idConexões
- sqlserver-visao-geral - Visão geral do SQL Server
- sqlserver-tuning-queries - Tuning de queries no SQL Server
- db-relacional - Fundamentos do modelo relacional
- gcp-cloud-sql - Cloud SQL no GCP (inclui PostgreSQL gerenciado)
- gcp-alloydb - AlloyDB, PostgreSQL compatível do GCP
- databricks-lakebase - Lakebase, o PostgreSQL serverless do Databricks