Quando você escreve SQL para análise de dados no dia a dia, a maior fonte de atrito entre bancos não costuma ser o “conceito” (filtrar, agregar, juntar), e sim detalhes de compatibilidade: nomes de funções, tipos retornados, comportamento com nulos, concatenação, conversões implícitas, limites/paginação, e funções utilitárias que aparecem o tempo todo em relatórios. Este capítulo é um guia prático de compatibilidade entre MySQL, PostgreSQL e SQL Server nas funções mais usadas, com foco em como escrever queries portáveis e como adaptar rapidamente quando a portabilidade não é possível.
O que “compatibilidade” significa na prática
Compatibilidade aqui não é “o SQL roda igual em qualquer lugar”, e sim: (1) a função existe com o mesmo nome; (2) aceita os mesmos tipos; (3) retorna o mesmo tipo; (4) tem o mesmo comportamento em casos-limite (nulos, strings vazias, collation, timezone); (5) a sintaxe é idêntica. Em análise de dados, pequenas diferenças podem mudar um relatório: uma concatenação que vira NULL, um arredondamento diferente, uma conversão que falha, ou uma função de data que interpreta timezone de forma distinta.
Uma forma útil de pensar é separar em três camadas: (a) SQL “padrão” (ANSI) que tende a ser mais portável; (b) funções com equivalentes diretos (mudam nome/sintaxe); (c) funções sem equivalente direto (exigem reescrita).
Mapa rápido: funções utilitárias mais comuns e equivalentes
Use este mapa como referência rápida. Nos tópicos seguintes, você verá exemplos e armadilhas.
- Coalescência de nulos:
COALESCE(MySQL/PostgreSQL/SQL Server) - Condicional:
CASE WHEN(todos);IIF(SQL Server);IF(MySQL) - Conversão/cast:
CAST(todos);CONVERT(SQL Server);::tipo(PostgreSQL) - Arredondamento:
ROUND(todos);CEILING/FLOOR(todos, com pequenas diferenças de tipo) - Concatenação:
CONCAT(todos); operador||(PostgreSQL); operador+(SQL Server, com cuidado) - Comprimento de string:
LENGTH(MySQL/PostgreSQL);LEN(SQL Server) - Substring:
SUBSTRING(todos);SUBSTR(MySQL/PostgreSQL) - Posição:
POSITION(PostgreSQL);INSTR(MySQL);CHARINDEX(SQL Server) - Data/hora atual:
CURRENT_DATE/CURRENT_TIMESTAMP(PostgreSQL);CURDATE()/NOW()(MySQL);GETDATE()/SYSDATETIME()(SQL Server) - Diferença entre datas:
DATEDIFF(MySQL/SQL Server, mas semântica diferente); em PostgreSQL usa-se subtração e/ouAGE/DATE_PART - Paginação:
LIMIT/OFFSET(MySQL/PostgreSQL);OFFSET ... FETCH(SQL Server) - Top N:
LIMIT(MySQL/PostgreSQL);TOP(SQL Server) - Agregação de strings:
STRING_AGG(PostgreSQL/SQL Server);GROUP_CONCAT(MySQL)
COALESCE e nulos: o “padrão” que salva portabilidade
COALESCE(a, b, c) retorna o primeiro valor não nulo. É padrão e funciona nos três bancos. Em relatórios, ele aparece para substituir nulos por zero, por “Sem categoria”, ou para escolher entre colunas alternativas.
Continue em nosso aplicativo
Você poderá ouvir o audiobook com a tela desligada, ganhar gratuitamente o certificado deste curso e ainda ter acesso a outros 5.000 cursos online gratuitos.
ou continue lendo abaixo...Baixar o aplicativo
Exemplo prático
SELECT customer_id, COALESCE(segment, 'Sem segmento') AS segment, COALESCE(revenue, 0) AS revenue FROM customers;Armadilha comum: tipo de retorno. Se você mistura tipos (ex.: número e texto), cada banco pode tentar converter de forma diferente. Regra prática: garanta tipos compatíveis, usando CAST quando necessário.
SELECT COALESCE(CAST(score AS DECIMAL(10,2)), 0.00) AS score FROM customer_scores;CASE WHEN vs IF/IIF: escolha o que é mais portável
CASE WHEN é o caminho mais compatível. MySQL tem IF(cond, a, b) e SQL Server tem IIF(cond, a, b), mas eles não são universais.
Padrão recomendado (portável)
SELECT order_id, CASE WHEN status = 'paid' THEN 1 WHEN status = 'canceled' THEN 0 ELSE NULL END AS is_paid FROM orders;Armadilha: em SQL Server, IIF pode avaliar ambos os ramos em alguns cenários (dependendo de conversões), o que pode causar erro em conversões inválidas. CASE costuma ser mais previsível para evitar conversões forçadas.
CAST, CONVERT e conversões implícitas
CAST(expr AS tipo) é padrão e funciona nos três. SQL Server também tem CONVERT(tipo, expr). PostgreSQL tem a sintaxe expr::tipo, útil, mas menos portável.
Exemplo: converter texto para data
Quando datas chegam como texto (ex.: '2026-01-09'), a conversão pode falhar se o formato não for aceito.
-- Portável (desde que o formato seja aceito pelo banco) SELECT CAST(order_date_text AS DATE) AS order_date FROM raw_orders;SQL Server: formatos dependem de configurações e podem ser ambíguos. Em cenários reais, prefira formatos ISO (YYYY-MM-DD) e, quando necessário, use CONVERT com estilo (não-portável, mas controlado).
-- SQL Server (exemplo de controle de estilo) SELECT CONVERT(date, order_date_text, 23) AS order_date FROM raw_orders;PostgreSQL: quando o texto não está em formato padrão, é comum usar TO_DATE (não existe igual nos três).
-- PostgreSQL SELECT TO_DATE(order_date_text, 'DD/MM/YYYY') AS order_date FROM raw_orders;Arredondamento e tipos numéricos: ROUND, CEILING, FLOOR
ROUND, CEILING e FLOOR existem nos três. O cuidado aqui é com o tipo retornado e com inteiros vs decimais.
Exemplo: padronizar casas decimais em métricas
SELECT product_id, ROUND(avg_ticket, 2) AS avg_ticket_2d FROM product_kpis;Armadilha: em SQL Server, operações entre inteiros podem resultar em inteiro (divisão inteira), e o arredondamento não “recupera” o que já foi truncado. Garanta decimal antes de dividir.
-- SQL Server: evitar divisão inteira SELECT ROUND(1.0 * revenue / NULLIF(orders, 0), 2) AS avg_ticket FROM kpis;Nota: NULLIF(x, 0) é padrão e funciona nos três; ele evita divisão por zero retornando NULL quando o denominador é 0.
Concatenação de strings: CONCAT vs operadores
Concatenação é uma das maiores fontes de diferença.
- PostgreSQL: operador
||é comum e idiomático. - SQL Server: operador
+concatena, mas pode virar NULL se um lado for NULL (dependendo de configurações) e pode tentar somar se tipos forem numéricos. - MySQL: operador
||pode ser tratado como OR lógico dependendo do modo SQL; por isso, não é recomendado para concatenação.
Para portabilidade, prefira CONCAT(a, b, c), que existe nos três.
Exemplo portável
SELECT CONCAT(first_name, ' ', last_name) AS full_name FROM customers;Armadilha com NULL: em alguns bancos, CONCAT trata NULL como string vazia; em outros, pode variar por versão/configuração. Se você precisa de comportamento consistente, use COALESCE explicitamente.
SELECT CONCAT(COALESCE(first_name, ''), ' ', COALESCE(last_name, '')) AS full_name FROM customers;Comprimento e substring: LENGTH/LEN e SUBSTRING
Para comprimento:
- MySQL/PostgreSQL:
LENGTH(texto) - SQL Server:
LEN(texto)(observação:LENnão conta espaços à direita)
Para substring, SUBSTRING(texto, inicio, tamanho) existe nos três e é uma boa escolha para portabilidade.
Exemplo: extrair DDD de um telefone
SELECT SUBSTRING(phone, 1, 2) AS ddd FROM customers;Armadilha: indexação começa em 1 nos três para SUBSTRING, mas funções alternativas (como SUBSTR) podem ter nuances. Se o campo pode ser menor que o esperado, combine com validação de tamanho.
SELECT CASE WHEN LENGTH(phone) >= 2 THEN SUBSTRING(phone, 1, 2) ELSE NULL END AS ddd FROM customers;Nota: a versão acima usa LENGTH; em SQL Server, troque por LEN.
Buscar posição de texto: INSTR, CHARINDEX, POSITION
Encontrar a posição de um caractere/subtexto muda bastante:
- MySQL:
INSTR(texto, subtexto) - SQL Server:
CHARINDEX(subtexto, texto) - PostgreSQL:
POSITION(subtexto IN texto)
Exemplo: achar o “@” em e-mails
-- MySQL SELECT INSTR(email, '@') AS at_pos FROM customers;-- SQL Server SELECT CHARINDEX('@', email) AS at_pos FROM customers;-- PostgreSQL SELECT POSITION('@' IN email) AS at_pos FROM customers;Estratégia de portabilidade: quando você precisa de lógica baseada em posição, considere se dá para reescrever com LIKE e SUBSTRING de forma mais simples, ou encapsular em uma camada (view) específica por banco.
Paginação e Top N: LIMIT/OFFSET vs TOP vs OFFSET/FETCH
Em relatórios, é comum pegar “os 100 primeiros” ou paginar resultados.
- MySQL/PostgreSQL:
LIMIT neOFFSET m - SQL Server:
TOP (n)ouORDER BY ... OFFSET m ROWS FETCH NEXT n ROWS ONLY
Top N (mais simples)
-- MySQL / PostgreSQL SELECT product_id, revenue FROM product_kpis ORDER BY revenue DESC LIMIT 10;-- SQL Server SELECT TOP (10) product_id, revenue FROM product_kpis ORDER BY revenue DESC;Paginação (página 3 com 50 linhas por página)
-- MySQL / PostgreSQL SELECT order_id, customer_id, created_at FROM orders ORDER BY created_at DESC LIMIT 50 OFFSET 100;-- SQL Server SELECT order_id, customer_id, created_at FROM orders ORDER BY created_at DESC OFFSET 100 ROWS FETCH NEXT 50 ROWS ONLY;Armadilha: paginação sem ORDER BY não é determinística. Em SQL Server, OFFSET/FETCH exige ORDER BY.
Agregação de strings: GROUP_CONCAT vs STRING_AGG
Listar valores concatenados por grupo é útil para auditoria e explicação de resultados (ex.: listar categorias compradas por cliente). Aqui há divergência:
- MySQL:
GROUP_CONCAT(expr ORDER BY ... SEPARATOR ',') - PostgreSQL:
STRING_AGG(expr, ',')comORDER BYdentro da agregação em versões modernas - SQL Server:
STRING_AGG(expr, ',')(ordenação pode exigir sintaxe adicional dependendo da versão)
Exemplo: listar SKUs por pedido
-- MySQL SELECT order_id, GROUP_CONCAT(sku ORDER BY sku SEPARATOR ',') AS skus FROM order_items GROUP BY order_id;-- PostgreSQL SELECT order_id, STRING_AGG(sku, ',' ORDER BY sku) AS skus FROM order_items GROUP BY order_id;-- SQL Server (ordenação pode variar por versão) SELECT order_id, STRING_AGG(sku, ',') AS skus FROM order_items GROUP BY order_id;Armadilha: limites de tamanho. MySQL tem limite configurável para GROUP_CONCAT. SQL Server pode truncar dependendo do tipo (use tipos grandes como varchar(max) quando necessário). PostgreSQL tende a ser mais flexível, mas o tamanho final ainda depende de memória e do tipo.
Funções de data/hora “atuais”: NOW, GETDATE, CURRENT_TIMESTAMP
Mesmo algo simples como “agora” muda de nome e precisão.
- MySQL:
NOW()(timestamp),CURDATE()(date) - PostgreSQL:
CURRENT_TIMESTAMP,NOW()(sinônimo),CURRENT_DATE - SQL Server:
GETDATE()(datetime),SYSDATETIME()(mais precisão)
Exemplo: carimbar extração
-- Portável em PostgreSQL e padrão SQL SELECT CURRENT_TIMESTAMP AS extracted_at;Armadilha: timezone. PostgreSQL diferencia timestamp com e sem timezone. MySQL e SQL Server têm comportamentos e tipos próprios. Em pipelines analíticos, padronize para UTC na camada de ingestão ou normalize com funções específicas do banco (o que reduz portabilidade).
DATEDIFF e diferença entre datas: semânticas diferentes
DATEDIFF existe em MySQL e SQL Server, mas não é “igual”:
- MySQL:
DATEDIFF(data_fim, data_inicio)retorna diferença em dias (inteiro). - SQL Server:
DATEDIFF(parte, inicio, fim)retorna contagem de “viradas” da parte (day, month, etc.), o que pode surpreender. - PostgreSQL: normalmente subtrai-se datas (
date - date) para obter dias, ou usa-seAGEeDATE_PARTpara partes.
Exemplo: dias desde a compra
-- MySQL SELECT DATEDIFF(CURDATE(), order_date) AS days_since_order FROM orders;-- SQL Server SELECT DATEDIFF(day, order_date, CAST(GETDATE() AS date)) AS days_since_order FROM orders;-- PostgreSQL SELECT (CURRENT_DATE - order_date) AS days_since_order FROM orders;Armadilha no SQL Server: DATEDIFF(day, '2026-01-01 23:59', '2026-01-02 00:01') retorna 1 (virou o dia), mesmo com poucos minutos. Se você precisa de diferença exata em horas/minutos, use a parte apropriada ou trabalhe com intervalos/segundos.
Passo a passo prático: como adaptar uma query “multi-banco” com funções comuns
A seguir, um procedimento prático para pegar uma query típica de relatório e torná-la compatível (ou facilmente adaptável) entre MySQL, PostgreSQL e SQL Server, focando nas funções mais usadas.
Passo 1: liste as funções e sintaxes não-portáveis
Pegue sua query e marque itens como: LIMIT, TOP, GROUP_CONCAT, LEN, GETDATE, operador de concatenação, casts específicos, e funções de posição.
Passo 2: substitua por equivalentes padrão quando existir
Troque IF/IIF por CASE, troque concatenação por CONCAT, troque conversões por CAST (quando possível), e use COALESCE/NULLIF para controlar nulos e divisões.
Passo 3: isole o que inevitavelmente muda por banco
Alguns itens quase sempre mudam: paginação (LIMIT vs TOP vs OFFSET/FETCH), agregação de strings (GROUP_CONCAT vs STRING_AGG), e funções específicas de parsing de data. Para esses, mantenha “blocos” alternativos, ou crie views por banco com a mesma interface.
Exemplo completo: relatório simples com pontos de compatibilidade
Objetivo: listar clientes com um “nome completo”, dias desde o cadastro, e uma lista de tags/categorias associadas (string agregada), retornando apenas os 20 mais recentes.
Versão MySQL
SELECT c.customer_id, CONCAT(COALESCE(c.first_name, ''), ' ', COALESCE(c.last_name, '')) AS full_name, DATEDIFF(CURDATE(), c.created_at) AS days_since_signup, GROUP_CONCAT(ct.tag ORDER BY ct.tag SEPARATOR ',') AS tags FROM customers c LEFT JOIN customer_tags ct ON ct.customer_id = c.customer_id GROUP BY c.customer_id, c.first_name, c.last_name, c.created_at ORDER BY c.created_at DESC LIMIT 20;Versão PostgreSQL
SELECT c.customer_id, CONCAT(COALESCE(c.first_name, ''), ' ', COALESCE(c.last_name, '')) AS full_name, (CURRENT_DATE - c.created_at::date) AS days_since_signup, STRING_AGG(ct.tag, ',' ORDER BY ct.tag) AS tags FROM customers c LEFT JOIN customer_tags ct ON ct.customer_id = c.customer_id GROUP BY c.customer_id, c.first_name, c.last_name, c.created_at ORDER BY c.created_at DESC LIMIT 20;Versão SQL Server
SELECT TOP (20) c.customer_id, CONCAT(COALESCE(c.first_name, ''), ' ', COALESCE(c.last_name, '')) AS full_name, DATEDIFF(day, CAST(c.created_at AS date), CAST(GETDATE() AS date)) AS days_since_signup, STRING_AGG(ct.tag, ',') AS tags FROM customers c LEFT JOIN customer_tags ct ON ct.customer_id = c.customer_id GROUP BY c.customer_id, c.first_name, c.last_name, c.created_at ORDER BY c.created_at DESC;O que observar no exemplo:
- Concatenação: padronizada com
CONCAT+COALESCE. - Dias desde cadastro: MySQL usa
DATEDIFFsimples; PostgreSQL usa subtração de datas; SQL Server usaDATEDIFF(day, ...)com casts para date. - Agregação de tags: MySQL usa
GROUP_CONCAT; PostgreSQL/SQL Server usamSTRING_AGG. - Top N: MySQL/PostgreSQL usam
LIMIT; SQL Server usaTOP.
Checklist de compatibilidade para o dia a dia
- Prefira
COALESCE,NULLIF,CASEeCASTpara máxima portabilidade. - Evite operador de concatenação (
||ou+) quando a query precisa rodar em mais de um banco; useCONCAT. - Para paginação e Top N, aceite que a sintaxe muda e mantenha variantes por banco.
- Para agregação de strings, planeje a troca
GROUP_CONCAT↔STRING_AGGe valide limites de tamanho. - Em diferenças de datas, valide a semântica (dias “exatos” vs “viradas” de unidade) e padronize casts para
datequando o objetivo é contar dias de calendário. - Em SQL Server, cuidado com divisão inteira; force decimal antes de dividir.
- Em SQL Server,
LENignora espaços à direita; se isso importa, teste com dados reais e considere alternativas.