SQL Dialetos: as diferenças entre os bancos

Quem fala português no Brasil entende quem fala português em Portugal, mas tropeça em algumas palavras: aqui é ônibus, lá é autocarro; aqui é trem, lá é comboio. Com o SQL acontece a mesma coisa. Todo banco de dados fala a mesma língua, o SQL padrão, mas cada um tem o seu sotaque, que os programadores chamam de dialeto. Nesta aula a Ana pega as consultas que já sabe fazer na Loja Aurora e vê como elas ficam no SQLite, no MySQL/MariaDB, no PostgreSQL e no SQL Server: o que é igual, o que muda e qual erro aparece quando ela usa a palavra de um banco no outro.

Você vai aprender
  • Por que existe um SQL padrão e, mesmo assim, cada banco tem o seu dialeto.
  • O núcleo que funciona igual em todos: SELECT, WHERE, JOIN, GROUP BY, INSERT, UPDATE, DELETE.
  • As oito diferenças que mais aparecem: limitar linhas (LIMIT × TOP × FETCH), aspas de nomes (" × ` × [ ]), numeração automática, concatenação de texto, datas, booleanos, UPSERT e tipos.
  • Os erros que cada banco mostra quando recebe o "sotaque" de outro.
  • Como escrever SQL que funciona em quase todos (o mais perto possível do padrão).
De onde vêm os resultados desta aula

Os quadros "Resultado" embaixo dos códigos rodam no SQLite de verdade, no seu navegador, como em todo o guia. As sessões de terminal do PostgreSQL 17 e do MariaDB 11.8 também são reais: rodamos as mesmas consultas, com os mesmos dados da Loja Aurora, nos dois bancos instalados na máquina da Ana. O SQL Server e o MySQL da Oracle nós não rodamos: o que aparece deles está marcado como ilustrativo e foi conferido na documentação oficial (learn.microsoft.com e dev.mysql.com).


A ideia: uma língua, vários sotaques

O SQL tem uma norma oficial, publicada pela ISO (a organização internacional de padrões) com o número ISO/IEC 9075. Ela diz como se escreve um SELECT, um JOIN, um CREATE TABLE. Os bancos seguem essa norma no essencial, mas:

  • muitos nasceram antes de a norma cobrir certos assuntos (limitar linhas, numerar automaticamente) e inventaram o seu jeito, que os usuários continuam usando;
  • cada um acrescenta extras próprios, como funções de data com nomes diferentes;
  • alguns têm regras próprias para coisas pequenas, como aspas e letras maiúsculas.

Pense numa receita de bolo escrita por quatro cozinheiras. Todas usam farinha, ovos e forno (o núcleo), mas uma escreve "xícara", outra "chávena", outra "200 g". Quem sabe a receita base adapta rápido. É isso que você vai treinar aqui.

BancoQuem fazO dialeto se chamaPrograma de terminalAula
SQLiteprojeto de domínio público (sqlite.org)SQL do SQLitesqlite3todo este guia
MySQLOracleSQL do MySQLmysqlSQL no MySQL e MariaDB
MariaDBcriado pelos desenvolvedores originais do MySQLquase o mesmo do MySQLmariadb
PostgreSQLcomunidade PostgreSQLSQL do PostgreSQLpsqlSQL no PostgreSQL
SQL ServerMicrosoftT-SQL (Transact-SQL)sqlcmdSQL no SQL Server

Os quatro bancos famosos já foram apresentados em Os Bancos Famosos. Aqui o foco é o código.


1. O que é igual em todos

A primeira boa notícia: quase tudo o que você aprendeu nos Níveis 1 e 2 funciona do mesmo jeito em qualquer banco. Esta consulta conta os pedidos dos clientes do Piauí, cidade por cidade. Ela usa JOIN, WHERE, GROUP BY, COUNT, apelidos com AS e ORDER BY:

SELECT c.cidade, COUNT(*) AS pedidos
FROM pedidos p
JOIN clientes c ON c.id = p.cliente_id
WHERE c.estado = 'PI'
GROUP BY c.cidade
ORDER BY pedidos DESC, c.cidade;

A Ana copiou exatamente o mesmo texto no PostgreSQL e no MariaDB. Veja que o resultado é o mesmo; só muda o "desenho" da tabela que cada programa de terminal faz:

$ psql loja
psql (17.11 (Debian 17.11-0+deb13u1))
Digite "help" para obter ajuda.

loja=> SELECT c.cidade, COUNT(*) AS pedidos FROM pedidos p JOIN clientes c ON c.id = p.cliente_id WHERE c.estado = 'PI' GROUP BY c.cidade ORDER BY pedidos DESC, c.cidade;
  cidade  | pedidos
----------+---------
 Teresina |      31
 Parnaíba |       4
 Oeiras   |       3
 Picos    |       3
(4 linhas)

loja=> \q
$ mariadb loja
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MariaDB connection id is 26
Server version: 11.8.6-MariaDB-0+deb13u1 from Debian -- Please help get to 10k stars at https://github.com/MariaDB/Server

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MariaDB [loja]> SELECT c.cidade, COUNT(*) AS pedidos FROM pedidos p JOIN clientes c ON c.id = p.cliente_id WHERE c.estado = 'PI' GROUP BY c.cidade ORDER BY pedidos DESC, c.cidade;
+-----------+---------+
| cidade    | pedidos |
+-----------+---------+
| Teresina  |      31 |
| Parnaíba  |       4 |
| Oeiras    |       3 |
| Picos     |       3 |
+-----------+---------+
4 rows in set (0,001 sec)

MariaDB [loja]> quit
Bye

(O mariadb loja entrou sem pedir senha porque a Ana guardou o usuário e a senha num arquivo de opções, o ~/.my.cnf; como fazer isso, e como entrar com -u ana -p, está em SQL no MySQL e MariaDB. A coluna cidade sai um espaço mais larga no MariaDB porque o programa mariadb mede a largura pelo número de bytes, e o "í" de Parnaíba ocupa dois. É só desenho, o dado está certo.)

Daqui em diante, para economizar espaço, as sessões mostram só o que a Ana digitou já dentro do psql (prompt loja=>) ou do mariadb (prompt MariaDB [loja]>).

No SQL Server a consulta também é a mesma. Este trecho é ilustrativo: não rodamos SQL Server, mas tudo o que ele usa está na documentação da Microsoft:

SELECT c.cidade, COUNT(*) AS pedidos
FROM pedidos p
JOIN clientes c ON c.id = p.cliente_id
WHERE c.estado = 'PI'
GROUP BY c.cidade
ORDER BY pedidos DESC, c.cidade;
Igual (ou quase) nos quatroAula
SELECT, FROM, WHERE, AND, OR, NOTSELECT, WHERE
ORDER BY, DISTINCT, ASORDER BY, Apelidos
LIKE, IN, BETWEEN, IS NULLLIKE, IS NULL
COUNT, SUM, AVG, MIN, MAX, GROUP BY, HAVINGFunções Agregadas
INNER JOIN, LEFT JOIN, UNION, subconsultas, CASE, COALESCEJOIN, CASE
INSERT, UPDATE, DELETEINSERT
WITH (CTE) e funções de janela (OVER)CTE, Janelas

Duas exceções conhecidas: o FULL JOIN não existe no MySQL (a sintaxe de JOIN da documentação do MySQL 8.4 só tem INNER, CROSS, LEFT, RIGHT e NATURAL; veja FULL JOIN), e o RIGHT JOIN e o FULL JOIN só chegaram ao SQLite na versão 3.39.


2. Limitar linhas: LIMIT × TOP × FETCH FIRST

Esta é a diferença mais famosa. A Ana quer os 3 produtos mais caros. No SQLite ela já sabe (aula LIMIT e OFFSET):

SELECT nome, preco
FROM produtos
ORDER BY preco DESC
LIMIT 3;

Cada banco escreve isso de um jeito. A norma ISO criou, em 2008, a forma FETCH FIRST ("busque as primeiras"), mas muitos bancos já tinham a sua:

BancoOs 3 primeirosPular 3 e pegar mais 3 (página 2)
SQLiteLIMIT 3LIMIT 3 OFFSET 3 ou LIMIT 3, 3
MySQL / MariaDBLIMIT 3LIMIT 3 OFFSET 3 ou LIMIT 3, 3
PostgreSQLLIMIT 3 ou FETCH FIRST 3 ROWS ONLYLIMIT 3 OFFSET 3 (a forma LIMIT 3, 3 dá erro)
SQL ServerSELECT TOP (3) …ORDER BY … OFFSET 3 ROWS FETCH NEXT 3 ROWS ONLY
Padrão ISOFETCH FIRST 3 ROWS ONLYOFFSET 3 ROWS FETCH NEXT 3 ROWS ONLY

Repare na forma LIMIT 3, 3: o primeiro número é quantas linhas pular, o segundo é quantas mostrar. O SQLite aceita, por compatibilidade com o MySQL:

SELECT nome, preco
FROM produtos
ORDER BY preco DESC
LIMIT 3, 3;

O PostgreSQL recusa essa forma, e a mensagem (em português, porque o computador da Ana está em português) já diz o que fazer:

loja=> SELECT nome, preco FROM produtos ORDER BY preco DESC LIMIT 3, 3;
ERRO:  sintaxe LIMIT #,# não tem suporte
LINHA 1: ...ECT nome, preco FROM produtos ORDER BY preco DESC LIMIT 3, 3...
                                                              ^
DICA:  Use cláusulas LIMIT e OFFSET separadas.
loja=> SELECT nome, preco FROM produtos ORDER BY preco DESC FETCH FIRST 3 ROWS ONLY;
           nome           | preco
--------------------------+--------
 Jogo de Panelas 5 peças  | 249.90
 Caixa de Som Portátil    | 189.99
 Fone de Ouvido Bluetooth | 129.90
(3 linhas)

Repare também no preço: o SQLite mostra 249.9, o PostgreSQL mostra 249.90. A coluna no PostgreSQL foi criada como NUMERIC(10,2) (número exato com 2 casas), e não como REAL. Tipos são o assunto da seção 8.

No SQL Server, o jeito clássico é o TOP, que vem logo depois do SELECT. A Microsoft recomenda sempre os parênteses, TOP (3), e sempre um ORDER BY, porque sem ele "as primeiras linhas" saem numa ordem indefinida (ilustrativo):

SELECT TOP (3) nome, preco
FROM produtos
ORDER BY preco DESC;

-- página 2 (a partir do SQL Server 2012): o ORDER BY é obrigatório
SELECT nome, preco
FROM produtos
ORDER BY preco DESC
OFFSET 3 ROWS FETCH NEXT 3 ROWS ONLY;

Experimente: a Ana achou esta consulta num tutorial de SQL Server. Rode e veja o erro: o SQLite não conhece TOP. Reescreva no jeito do SQLite para mostrar os 3 produtos mais caros.

SELECT TOP 3 nome, preco
FROM produtos
ORDER BY preco DESC;
SELECT nome, preco
FROM produtos
ORDER BY preco DESC
LIMIT 3;

3. Aspas nos nomes: " × ` × [ ]

Você aprendeu que texto vai entre aspas simples: 'PI'. E os nomes de tabelas e colunas? Normalmente vão sem aspas nenhuma. Mas às vezes um nome precisa de aspas: quando tem espaço (data de entrega), acento estranho ou é uma palavra reservada (uma coluna chamada order, por exemplo). Aí cada banco usa um sinal:

BancoAspas de nome (identificador)O que "nome" significa
Padrão ISO"nome"a coluna nome
SQLite"nome"; também aceita `nome` e [nome]a coluna nome (no editor do guia, sempre)
PostgreSQL"nome"a coluna nome, com maiúsculas e minúsculas exatas
MySQL / MariaDB`nome` (crase)o texto 'nome'! (a não ser no modo ANSI_QUOTES)
SQL Server[nome]; também "nome" com QUOTED_IDENTIFIER ON (o padrão da maioria das conexões)a coluna nome

O SQLite é o mais tolerante: aceita os três sinais, para facilitar quem traz código de outro banco:

SELECT [nome], `cidade`, "estado"
FROM clientes
WHERE id = 1;

Agora o perigo. No MariaDB (e no MySQL), aspas duplas são texto, como as simples. A Ana pediu a coluna "nome" e recebeu a palavra "nome":

MariaDB [loja]> SELECT "nome" FROM clientes WHERE id = 1;
+------+
| nome |
+------+
| nome |
+------+
1 row in set (0,000 sec)

MariaDB [loja]> SELECT `nome` FROM clientes WHERE id = 1;
+---------------------+
| nome                |
+---------------------+
| José Ribamar Silva  |
+---------------------+
1 row in set (0,000 sec)

Nenhum erro, nenhum aviso: só um resultado errado. É o tipo de diferença mais traiçoeira. A documentação do MySQL explica que o modo ANSI_QUOTES faz " virar aspas de nome, "como a crase", e então aspas duplas deixam de servir para texto.

E a crase no PostgreSQL? Ele não a conhece e dá erro de sintaxe:

loja=> SELECT `nome` FROM clientes WHERE id = 1;
ERRO:  erro de sintaxe em ou próximo a "FROM"
LINHA 1: SELECT `nome` FROM clientes WHERE id = 1;
                       ^

Maiúsculas nos nomes

Outra diferença ligada às aspas. No SQLite, Nome, NOME e nome são a mesma coluna, com ou sem aspas. No PostgreSQL, um nome sem aspas é convertido para minúsculas (a documentação diz: "nomes sem aspas são sempre convertidos para minúsculas"), mas com aspas ele vale letra por letra:

loja=> SELECT Nome FROM clientes WHERE id = 1;
        nome
--------------------
 José Ribamar Silva
(1 linha)

loja=> SELECT "Nome" FROM clientes WHERE id = 1;
ERRO:  coluna "Nome" não existe
LINHA 1: SELECT "Nome" FROM clientes WHERE id = 1;
                ^
DICA:  Talvez você queira fazer referência à coluna "clientes.nome".

No MySQL/MariaDB no Linux, o contrário acontece com tabelas: os nomes de tabela diferenciam maiúsculas (a documentação do MySQL explica que eles viram arquivos e pastas no disco), mas os de coluna não:

MariaDB [loja]> SELECT Nome FROM CLIENTES WHERE id = 1;
ERROR 1146 (42S02): Table 'loja.CLIENTES' doesn't exist

A regra que evita tudo isso, e que o guia segue desde o começo: nomes de tabela e coluna em minúsculas, sem espaço, sem acento, sem aspas. Assim o mesmo SQL funciona nos quatro bancos.


4. Numeração automática do id

Toda tabela da Loja Aurora tem um id. Quando a Ana cadastra um aviso novo, ela não quer escolher o número; quer que o banco dê o próximo. Cada banco tem a sua palavra para isso (no SQLite, veja AUTOINCREMENT):

BancoComo declarar o id automático
SQLiteid INTEGER PRIMARY KEY (ou … PRIMARY KEY AUTOINCREMENT)
MySQL / MariaDBid INT AUTO_INCREMENT PRIMARY KEY
PostgreSQLid INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY (padrão ISO) ou o antigo id SERIAL PRIMARY KEY
SQL Serverid INT IDENTITY(1,1) PRIMARY KEY
CREATE TABLE avisos (
    id    INTEGER PRIMARY KEY,
    texto TEXT NOT NULL
);
INSERT INTO avisos (texto) VALUES
    ('Promoção de volta às aulas'),
    ('Inventário no sábado');
SELECT * FROM avisos;

A mesma tabela, rodada de verdade no PostgreSQL e no MariaDB, com a palavra de cada um:

loja=> CREATE TABLE avisos (id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, texto VARCHAR(100) NOT NULL);
CREATE TABLE
loja=> INSERT INTO avisos (texto) VALUES ('Promoção de volta às aulas'), ('Inventário no sábado');
INSERT 0 2
loja=> SELECT * FROM avisos;
 id |           texto
----+----------------------------
  1 | Promoção de volta às aulas
  2 | Inventário no sábado
(2 linhas)
MariaDB [loja]> CREATE TABLE avisos (id INT AUTO_INCREMENT PRIMARY KEY, texto VARCHAR(100) NOT NULL);
Query OK, 0 rows affected (0,010 sec)

MariaDB [loja]> INSERT INTO avisos (texto) VALUES ('Promoção de volta às aulas'), ('Inventário no sábado');
Query OK, 2 rows affected (0,002 sec)
Records: 2  Duplicates: 0  Warnings: 0

No SQL Server (ilustrativo, sintaxe da documentação do IDENTITY; os dois números são o começo e o passo, e se você não puser nenhum o padrão é (1,1)):

CREATE TABLE avisos (
    id    INT IDENTITY(1,1) PRIMARY KEY,
    texto NVARCHAR(100) NOT NULL
);

5. Juntar textos: || × CONCAT × +

A Ana quer uma coluna só com "nome (cidade)". No SQLite ela usa ||, que é o operador do padrão ISO (aula Funções de Texto):

SELECT nome || ' (' || cidade || ')' AS cliente
FROM clientes
WHERE id <= 3;
Banco||CONCAT(a, b, …)Outro
SQLitejunta textossim (desde a versão 3.44)—
PostgreSQLjunta textossim—
MySQL / MariaDBé OU lógico (a não ser no modo PIPES_AS_CONCAT)sim (o jeito normal)—
SQL Serverjunta textos só a partir do SQL Server 2025sim+ junta textos

No MariaDB, o || é "ou", como o OR. Então ele tenta transformar cada texto em número, não consegue, e devolve 0, com 12 avisos (warnings):

MariaDB [loja]> SELECT nome || ' (' || cidade || ')' AS cliente FROM clientes WHERE id <= 3;
+---------+
| cliente |
+---------+
|       0 |
|       0 |
|       0 |
+---------+
3 rows in set, 12 warnings (0,000 sec)

MariaDB [loja]> SHOW WARNINGS;
+---------+------+----------------------------------------------------------+
| Level   | Code | Message                                                  |
+---------+------+----------------------------------------------------------+
| Warning | 1292 | Truncated incorrect DECIMAL value: 'José Ribamar Silva'  |
| Warning | 1292 | Truncated incorrect DOUBLE value: ' ('                   |
| Warning | 1292 | Truncated incorrect DECIMAL value: 'Teresina'            |
| Warning | 1292 | Truncated incorrect DOUBLE value: ')'                    |
| Warning | 1292 | Truncated incorrect DECIMAL value: 'Maria Clara Mendes'  |
| Warning | 1292 | Truncated incorrect DOUBLE value: ' ('                   |
| Warning | 1292 | Truncated incorrect DECIMAL value: 'Teresina'            |
| Warning | 1292 | Truncated incorrect DOUBLE value: ')'                    |
| Warning | 1292 | Truncated incorrect DECIMAL value: 'Ana Beatriz Sousa'   |
| Warning | 1292 | Truncated incorrect DOUBLE value: ' ('                   |
| Warning | 1292 | Truncated incorrect DECIMAL value: 'São Paulo'           |
| Warning | 1292 | Truncated incorrect DOUBLE value: ')'                    |
+---------+------+----------------------------------------------------------+
12 rows in set (0,000 sec)

MariaDB [loja]> SELECT CONCAT(nome, ' (', cidade, ')') AS cliente FROM clientes WHERE id <= 3;
+--------------------------------+
| cliente                        |
+--------------------------------+
| José Ribamar Silva (Teresina)  |
| Maria Clara Mendes (Teresina)  |
| Ana Beatriz Sousa (São Paulo)  |
+--------------------------------+
3 rows in set (0,000 sec)

O SHOW WARNINGS ("mostre os avisos") explica: "Truncated incorrect DECIMAL value", isto é, "valor DECIMAL incorreto, truncado". O banco tentou ler cada pedaço de texto como número. A documentação do MySQL 8.4 diz que esse uso do || como "ou" é uma extensão fora do padrão e está descontinuado (deve sumir numa versão futura). Até lá, no MySQL e no MariaDB, use CONCAT().

No SQL Server, o jeito tradicional é o + (ilustrativo):

SELECT nome + ' (' + cidade + ')' AS cliente
FROM clientes
WHERE id <= 3;

E o que o SQLite faz com esse +? Ele é sempre soma de números. O SQLite tenta converter os textos em número, não consegue, e trata cada um como 0:

SELECT nome + ' (' + cidade + ')' AS cliente
FROM clientes
WHERE id <= 3;

Experimente: esta consulta veio de um sistema em SQL Server e, no SQLite, devolve só zeros. Troque os + pelo operador de juntar texto do SQLite (ou use CONCAT) para aparecer "Carlos Eduardo Lima (Fortaleza)".

SELECT nome + ' (' + cidade + ')' AS cliente
FROM clientes
WHERE estado = 'CE';
SELECT nome || ' (' || cidade || ')' AS cliente
FROM clientes
WHERE estado = 'CE';
E quando um dos textos é NULL?

Com ||, um pedaço NULL deixa tudo NULL (no SQLite e no PostgreSQL; e o || do SQL Server 2025 também, segundo a Microsoft). Já o CONCAT() trata NULL como texto vazio no SQLite e no PostgreSQL, mas no MySQL/MariaDB o CONCAT() devolve NULL. O cliente 7 não tem email:

MariaDB [loja]> SELECT CONCAT(nome, ' <', email, '>') AS contato FROM clientes WHERE id IN (6, 7);
+--------------------------------------------------+
| contato                                          |
+--------------------------------------------------+
| Rafael Monteiro <rafael.monteiro@exemplo.com.br> |
| NULL                                             |
+--------------------------------------------------+
2 rows in set (0,000 sec)
loja=> SELECT CONCAT(nome, ' <', email, '>') AS contato FROM clientes WHERE id IN (6, 7);
                     contato
--------------------------------------------------
 Rafael Monteiro <rafael.monteiro@exemplo.com.br>
 Luís Otávio Brito <>
(2 linhas)

Se o dado pode faltar, proteja com COALESCE(email, '') (aula IFNULL e COALESCE), que funciona igual nos quatro bancos.


6. Datas

Aqui as diferenças são grandes. O SQLite não tem um tipo "data": ele guarda o texto '2024-01-05' e usa funções como date(), strftime() e julianday() (aula Funções de Data). Os outros têm um tipo DATE de verdade, com funções próprias. A Ana quer, para os 3 primeiros pedidos, o prazo de 7 dias depois do pedido e quantos dias a entrega levou:

SELECT id,
       date(data, '+7 days') AS prazo,
       julianday(entregue_em) - julianday(data) AS dias
FROM pedidos
WHERE id <= 3;
loja=> SELECT id, data + 7 AS prazo, entregue_em - data AS dias FROM pedidos WHERE id <= 3;
 id |   prazo    | dias
----+------------+------
  1 | 2024-01-12 |    5
  2 | 2024-01-21 |    7
  3 | 2024-02-03 |    4
(3 linhas)
MariaDB [loja]> SELECT id, DATE_ADD(data, INTERVAL 7 DAY) AS prazo, DATEDIFF(entregue_em, data) AS dias FROM pedidos WHERE id <= 3;
+----+------------+------+
| id | prazo      | dias |
+----+------------+------+
|  1 | 2024-01-12 |    5 |
|  2 | 2024-01-21 |    7 |
|  3 | 2024-02-03 |    4 |
+----+------------+------+
3 rows in set (0,001 sec)
SELECT id,
       DATEADD(day, 7, data) AS prazo,
       DATEDIFF(day, data, entregue_em) AS dias
FROM pedidos
WHERE id <= 3;

Os mesmos números nos quatro (o SQLite escreve 5.0 porque o julianday() trabalha com números com casas decimais). Mas repare na ordem dos argumentos do DATEDIFF: no MySQL/MariaDB é DATEDIFF(fim, início); no SQL Server é DATEDIFF(unidade, início, fim). Mesma palavra, ordem diferente.

TarefaSQLiteMySQL / MariaDBPostgreSQLSQL Server
Hojedate('now') ou CURRENT_DATECURDATE() ou CURRENT_DATECURRENT_DATECAST(GETDATE() AS date); CURRENT_DATE só no 2025
Agora (data e hora)datetime('now')NOW()NOW()GETDATE() ou SYSDATETIME()
Somar 7 diasdate(d, '+7 days')DATE_ADD(d, INTERVAL 7 DAY)d + 7 ou d + INTERVAL '7 days'DATEADD(day, 7, d)
Dias entre duas datasjulianday(b) - julianday(a)DATEDIFF(b, a)b - aDATEDIFF(day, a, b)
O anostrftime('%Y', d)YEAR(d)EXTRACT(YEAR FROM d)YEAR(d)
Formatar 05/01/2024strftime('%d/%m/%Y', d)DATE_FORMAT(d, '%d/%m/%Y')to_char(d, 'DD/MM/YYYY')FORMAT(d, 'dd/MM/yyyy')

Um ponto comum que ajuda muito: os quatro entendem a data escrita como 'AAAA-MM-DD' (o formato ISO, o mesmo que a Loja Aurora usa). Guarde datas sempre assim.


7. Verdadeiro e falso (booleanos)

A coluna vip da Loja Aurora diz se o cliente é VIP. No SQLite ela guarda 0 ou 1; as palavras TRUE e FALSE existem, mas são só apelidos para 1 e 0:

SELECT nome, vip, TRUE AS verdadeiro, FALSE AS falso
FROM clientes
WHERE vip = TRUE AND estado = 'SP';
BancoTipoComo aparece no resultadoWHERE vip = 1 funciona?
SQLitenão tem; usa INTEGER 0/11sim
MySQL / MariaDBBOOLEAN, que é apelido de TINYINT(1)1sim
PostgreSQLBOOLEAN de verdadet ou fnão: use vip = true ou só WHERE vip
SQL Serverbit (guarda 1, 0 ou NULL)1sim
loja=> SELECT nome, vip FROM clientes WHERE vip = true AND estado = 'SP';
       nome        | vip
-------------------+-----
 Ana Beatriz Sousa | t
(1 linha)

loja=> SELECT nome, vip FROM clientes WHERE vip = 1;
ERRO:  operador não existe: boolean = integer
LINHA 1: SELECT nome, vip FROM clientes WHERE vip = 1;
                                                  ^
DICA:  Nenhum operador corresponde ao nome e tipo de dados dos argumentos fornecidos. Talvez seja necessário adicionar uma conversão de tipo de dados explícita.

O PostgreSQL é o mais rigoroso com tipos: um boolean não se compara com um número. Escrever vip = TRUE funciona no SQLite, no MySQL/MariaDB e no PostgreSQL. No SQL Server o bit se compara com 1 (a documentação diz que os textos 'TRUE' e 'FALSE' podem ser convertidos para 1 e 0).


8. Inserir ou atualizar (UPSERT)

A Ana quer renomear a categoria 8 para "Jardim e Quintal", e, se ela não existisse, criá-la. Esse "insere, e se já existe atualiza" tem o apelido de UPSERT (update + insert). No SQLite e no PostgreSQL é igual:

INSERT INTO categorias (id, nome) VALUES (8, 'Jardim e Quintal')
ON CONFLICT (id) DO UPDATE SET nome = excluded.nome;

SELECT id, nome FROM categorias WHERE id = 8;
INSERT INTO … VALUES (8, …)
Tenta inserir a categoria 8 normalmente.
ON CONFLICT (id)
"Em caso de conflito no id": se já existe uma linha com esse id…
DO UPDATE SET nome = excluded.nome
…atualize essa linha. excluded ("a excluída") é a linha que você tentou inserir e foi barrada.
BancoUPSERT
SQLiteINSERT … ON CONFLICT (id) DO UPDATE SET nome = excluded.nome
PostgreSQLigual ao SQLite (e também MERGE)
MySQLINSERT … VALUES (…) AS nova ON DUPLICATE KEY UPDATE nome = nova.nome
MariaDBINSERT … ON DUPLICATE KEY UPDATE nome = VALUE(nome)
SQL ServerMERGE INTO … USING … WHEN MATCHED THEN UPDATE … WHEN NOT MATCHED THEN INSERT …;
MariaDB [loja]> INSERT INTO categorias (id, nome) VALUES (8, 'Jardim e Quintal') ON DUPLICATE KEY UPDATE nome = VALUE(nome);
Query OK, 2 rows affected (0,002 sec)

"2 rows affected" assusta, mas é a convenção do MySQL/MariaDB: 1 quer dizer "inseri uma linha nova", 2 quer dizer "atualizei uma que já existia". Até MySQL e MariaDB, que são parentes, divergem aqui: a forma AS nova … nova.nome é a recomendada pela documentação do MySQL 8.4 (que marca o VALUES() antigo como descontinuado), mas o MariaDB 11.8 recusa essa forma com erro de sintaxe. Os detalhes estão em SQL no MySQL e MariaDB; o MERGE do SQL Server está em SQL no SQL Server.


9. Tipos de dados

O SQLite é flexível: uma coluna INTEGER aceita até um texto (aula Os Tipos no SQLite). Os outros bancos são rígidos: cada coluna aceita só o seu tipo, e muitos tipos têm tamanho. Quando a Ana levou a Loja Aurora para os outros bancos, ela trocou os tipos assim:

ColunaSQLiteMySQL / MariaDBPostgreSQLSQL Server
idINTEGER PRIMARY KEYINT AUTO_INCREMENTINTEGER GENERATED … AS IDENTITYINT IDENTITY(1,1)
nomeTEXTVARCHAR(100)VARCHAR(100) ou TEXTNVARCHAR(100)
estadoTEXTCHAR(2)CHAR(2)CHAR(2)
precoREALDECIMAL(10,2)NUMERIC(10,2)DECIMAL(10,2)
nascimentoTEXT ('AAAA-MM-DD')DATEDATEDATE
vipINTEGER 0/1BOOLEANBOOLEANBIT

A tabela completa, com todos os tipos, está em Referência: Tipos de Dados.


Formas diferentes de escrever o mesmo SQL

Quando um código precisa rodar em mais de um banco, você tem duas estratégias:

EstratégiaExemploVantagemDesvantagem
Ficar no padrãoFETCH FIRST 3 ROWS ONLY, CASE, COALESCE, CURRENT_DATE, nomes minúsculos sem aspasroda em mais bancos sem mudar nadanem todo banco tem tudo (o SQLite e o MySQL não têm FETCH FIRST)
Usar o sotaque localLIMIT, TOP, DATE_ADD, strftimeé o que a documentação e os exemplos daquele banco usamna troca de banco, tem de reescrever
Separar as partes que mudama parte da data numa view ou numa função do seu programasó um pedaço muda na migraçãodá um pouco mais de trabalho no começo

Na prática, quase todo mundo usa o sotaque do banco onde trabalha e consulta uma tabela como as desta página quando precisa migrar.


Quando dá errado

Os erros de dialeto têm uma cara típica: erro de sintaxe perto da palavra estranha, ou função que não existe. E às vezes, o pior caso: nenhum erro, só um resultado errado.

Usou TOP no SQLite (ou em qualquer banco que não seja o SQL Server)
SELECT TOP 3 nome FROM produtos;

O SQLite leu SELECT TOP achando que TOP era o nome de uma coluna; aí veio o 3 e ele não entendeu mais nada: near "3": syntax error ("perto de 3: erro de sintaxe"). O MariaDB e o PostgreSQL também tropeçam no 3:

MariaDB [loja]> SELECT TOP 3 nome FROM produtos;
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near '3 nome FROM produtos' at line 1
loja=> SELECT TOP 3 nome FROM produtos;
ERRO:  erro de sintaxe em ou próximo a "3"
LINHA 1: SELECT TOP 3 nome FROM produtos;
                    ^

Como consertar: troque por LIMIT 3 no fim.

Função de outro banco
SELECT id, DATEDIFF(entregue_em, data) AS dias
FROM pedidos
WHERE id <= 5;

no such function: DATEDIFF: "não existe a função DATEDIFF". O SQLite não tem essa função do MySQL/SQL Server.

Como consertar: procure a função equivalente na tabela da seção 6: julianday(entregue_em) - julianday(data).

Aspas duplas viraram texto (MySQL/MariaDB)

Você já viu na seção 3: SELECT "nome" FROM clientes no MariaDB devolve a palavra nome em vez do nome do cliente. Pior ainda num filtro: WHERE "estado" = 'PI' compara o texto "estado" com "PI", que nunca é igual, e a consulta volta vazia, sem erro.

Como consertar: nomes sem aspas. Se precisar de aspas num nome no MySQL/MariaDB, use a crase: `estado`.

|| no MySQL/MariaDB devolve 0

Também na seção 5: o || é "ou" lógico lá, e o resultado vira 0, sem erro, só com um aviso discreto no fim: 3 rows in set, 12 warnings. Sempre que aparecer warnings depois de uma consulta, peça SHOW WARNINGS; e leia.

Como consertar: CONCAT(nome, ' (', cidade, ')').

Comparou booleano com número (PostgreSQL)

Seção 7: WHERE vip = 1 dá ERRO: operador não existe: boolean = integer. Como consertar: WHERE vip = true ou simplesmente WHERE vip.


Referência rápida

AssuntoSQLiteMySQL / MariaDBPostgreSQLSQL Server
LimitarLIMIT 3LIMIT 3LIMIT 3 / FETCH FIRST 3 ROWS ONLYTOP (3) / OFFSET 0 ROWS FETCH NEXT 3 ROWS ONLY
Aspas de nome"x" (aceita `x`, [x])`x`"x"[x] ou "x"
Textosempre aspas simples: 'Teresina' (no SQL Server, N'Teresina' para texto Unicode)
id automáticoINTEGER PRIMARY KEYAUTO_INCREMENTGENERATED … AS IDENTITY / SERIALIDENTITY(1,1)
Juntar texto||, concat()CONCAT()||, concat()+, CONCAT(), || (2025)
Hojedate('now')CURDATE()CURRENT_DATECAST(GETDATE() AS date)
Somar diasdate(d,'+7 days')DATE_ADD(d, INTERVAL 7 DAY)d + 7DATEADD(day, 7, d)
Booleano0/1BOOLEAN = TINYINT(1)BOOLEAN (t/f)bit
UPSERTON CONFLICT … DO UPDATEON DUPLICATE KEY UPDATEON CONFLICT … DO UPDATEMERGE
Erro de tabela inexistenteno such table: xERROR 1146 (42S02): Table 'loja.x' doesn't existERRO: relação "x" não existeInvalid object name 'x'. (Msg 208)

Exercícios

1. Os 5 primeiros no SQL Server

Complete a consulta do SQL Server que mostra os 5 clientes mais novos:

SELECT (5) nome, nascimento FROM clientes ORDER BY nascimento DESC;

2. Aspas de nome no MySQL

No MySQL, a coluna se chama order (uma palavra reservada) e precisa de aspas de nome. Qual sinal vai dos dois lados? Escreva o sinal:

SELECT order FROM vendas;

3. No MariaDB, a Ana rodou SELECT nome || ' - ' || cidade FROM clientes;. O que aparece?

4. Qual destas consultas funciona igual no SQLite, no MySQL/MariaDB e no PostgreSQL?

5. No PostgreSQL, a tabela foi criada com a coluna nome. Qual destas consultas dá ERRO: coluna … não existe?

6. Qual é a forma do padrão ISO para pegar só as 3 primeiras linhas?

Missão final: traduzir do MySQL para o SQLite

Missão: a Ana recebeu esta consulta de um colega que usa MySQL. Ela mostra, para os 5 primeiros pedidos, o prazo (7 dias depois da data do pedido) e quantos dias a entrega levou. Traduza as duas funções para o SQLite, mantendo os apelidos prazo e dias. Dica: a tabela da seção 6.

SELECT id,
       DATE_ADD(data, INTERVAL 7 DAY) AS prazo,
       DATEDIFF(entregue_em, data) AS dias
FROM pedidos
WHERE id <= 5;
SELECT id,
       date(data, '+7 days') AS prazo,
       julianday(entregue_em) - julianday(data) AS dias
FROM pedidos
WHERE id <= 5;

Se você usou CAST(julianday(entregue_em) - julianday(data) AS INTEGER) para tirar o .0, também está certo.


Resumo

  • Todos os bancos falam SQL (norma ISO/IEC 9075), cada um com o seu dialeto. O núcleo (SELECT, WHERE, JOIN, GROUP BY, INSERT, UPDATE, DELETE) é igual.
  • Limitar: LIMIT (SQLite, MySQL/MariaDB, PostgreSQL), TOP (n) (SQL Server), FETCH FIRST n ROWS ONLY (padrão; PostgreSQL, SQL Server com OFFSET, MariaDB).
  • Aspas de nome: "x" (padrão, PostgreSQL, SQLite), `x` (MySQL/MariaDB), [x] (SQL Server). No MySQL/MariaDB, "x" é texto. Use nomes minúsculos sem aspas e evite o problema.
  • id automático: INTEGER PRIMARY KEY, AUTO_INCREMENT, GENERATED AS IDENTITY/SERIAL, IDENTITY(1,1).
  • Juntar texto: || no SQLite e no PostgreSQL; CONCAT() no MySQL/MariaDB (lá || é "ou"); + no SQL Server.
  • Datas e booleanos são onde os bancos mais diferem: confira a tabela antes de migrar. O PostgreSQL não compara boolean com número.
  • Erros de dialeto: "erro de sintaxe perto de…" ou "função não existe". O pior caso não dá erro nenhum: só um resultado errado.