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.
- 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).
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.
| Banco | Quem faz | O dialeto se chama | Programa de terminal | Aula |
|---|---|---|---|---|
| SQLite | projeto de domínio público (sqlite.org) | SQL do SQLite | sqlite3 | todo este guia |
| MySQL | Oracle | SQL do MySQL | mysql | SQL no MySQL e MariaDB |
| MariaDB | criado pelos desenvolvedores originais do MySQL | quase o mesmo do MySQL | mariadb | |
| PostgreSQL | comunidade PostgreSQL | SQL do PostgreSQL | psql | SQL no PostgreSQL |
| SQL Server | Microsoft | T-SQL (Transact-SQL) | sqlcmd | SQL 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 quatro | Aula |
|---|---|
SELECT, FROM, WHERE, AND, OR, NOT | SELECT, WHERE |
ORDER BY, DISTINCT, AS | ORDER BY, Apelidos |
LIKE, IN, BETWEEN, IS NULL | LIKE, IS NULL |
COUNT, SUM, AVG, MIN, MAX, GROUP BY, HAVING | Funções Agregadas |
INNER JOIN, LEFT JOIN, UNION, subconsultas, CASE, COALESCE | JOIN, CASE |
INSERT, UPDATE, DELETE | INSERT |
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:
| Banco | Os 3 primeiros | Pular 3 e pegar mais 3 (página 2) |
|---|---|---|
| SQLite | LIMIT 3 | LIMIT 3 OFFSET 3 ou LIMIT 3, 3 |
| MySQL / MariaDB | LIMIT 3 | LIMIT 3 OFFSET 3 ou LIMIT 3, 3 |
| PostgreSQL | LIMIT 3 ou FETCH FIRST 3 ROWS ONLY | LIMIT 3 OFFSET 3 (a forma LIMIT 3, 3 dá erro) |
| SQL Server | SELECT TOP (3) … | ORDER BY … OFFSET 3 ROWS FETCH NEXT 3 ROWS ONLY |
| Padrão ISO | FETCH FIRST 3 ROWS ONLY | OFFSET 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:
| Banco | Aspas 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):
| Banco | Como declarar o id automático |
|---|---|
| SQLite | id INTEGER PRIMARY KEY (ou … PRIMARY KEY AUTOINCREMENT) |
| MySQL / MariaDB | id INT AUTO_INCREMENT PRIMARY KEY |
| PostgreSQL | id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY (padrão ISO) ou o antigo id SERIAL PRIMARY KEY |
| SQL Server | id 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 |
|---|---|---|---|
| SQLite | junta textos | sim (desde a versão 3.44) | — |
| PostgreSQL | junta textos | sim | — |
| MySQL / MariaDB | é OU lógico (a não ser no modo PIPES_AS_CONCAT) | sim (o jeito normal) | — |
| SQL Server | junta textos só a partir do SQL Server 2025 | sim | + 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';
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.
| Tarefa | SQLite | MySQL / MariaDB | PostgreSQL | SQL Server |
|---|---|---|---|---|
| Hoje | date('now') ou CURRENT_DATE | CURDATE() ou CURRENT_DATE | CURRENT_DATE | CAST(GETDATE() AS date); CURRENT_DATE só no 2025 |
| Agora (data e hora) | datetime('now') | NOW() | NOW() | GETDATE() ou SYSDATETIME() |
| Somar 7 dias | date(d, '+7 days') | DATE_ADD(d, INTERVAL 7 DAY) | d + 7 ou d + INTERVAL '7 days' | DATEADD(day, 7, d) |
| Dias entre duas datas | julianday(b) - julianday(a) | DATEDIFF(b, a) | b - a | DATEDIFF(day, a, b) |
| O ano | strftime('%Y', d) | YEAR(d) | EXTRACT(YEAR FROM d) | YEAR(d) |
| Formatar 05/01/2024 | strftime('%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';
| Banco | Tipo | Como aparece no resultado | WHERE vip = 1 funciona? |
|---|---|---|---|
| SQLite | não tem; usa INTEGER 0/1 | 1 | sim |
| MySQL / MariaDB | BOOLEAN, que é apelido de TINYINT(1) | 1 | sim |
| PostgreSQL | BOOLEAN de verdade | t ou f | não: use vip = true ou só WHERE vip |
| SQL Server | bit (guarda 1, 0 ou NULL) | 1 | sim |
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.
| Banco | UPSERT |
|---|---|
| SQLite | INSERT … ON CONFLICT (id) DO UPDATE SET nome = excluded.nome |
| PostgreSQL | igual ao SQLite (e também MERGE) |
| MySQL | INSERT … VALUES (…) AS nova ON DUPLICATE KEY UPDATE nome = nova.nome |
| MariaDB | INSERT … ON DUPLICATE KEY UPDATE nome = VALUE(nome) |
| SQL Server | MERGE 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:
| Coluna | SQLite | MySQL / MariaDB | PostgreSQL | SQL Server |
|---|---|---|---|---|
id | INTEGER PRIMARY KEY | INT AUTO_INCREMENT | INTEGER GENERATED … AS IDENTITY | INT IDENTITY(1,1) |
nome | TEXT | VARCHAR(100) | VARCHAR(100) ou TEXT | NVARCHAR(100) |
estado | TEXT | CHAR(2) | CHAR(2) | CHAR(2) |
preco | REAL | DECIMAL(10,2) | NUMERIC(10,2) | DECIMAL(10,2) |
nascimento | TEXT ('AAAA-MM-DD') | DATE | DATE | DATE |
vip | INTEGER 0/1 | BOOLEAN | BOOLEAN | BIT |
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égia | Exemplo | Vantagem | Desvantagem |
|---|---|---|---|
| Ficar no padrão | FETCH FIRST 3 ROWS ONLY, CASE, COALESCE, CURRENT_DATE, nomes minúsculos sem aspas | roda em mais bancos sem mudar nada | nem todo banco tem tudo (o SQLite e o MySQL não têm FETCH FIRST) |
| Usar o sotaque local | LIMIT, TOP, DATE_ADD, strftime | é o que a documentação e os exemplos daquele banco usam | na troca de banco, tem de reescrever |
| Separar as partes que mudam | a parte da data numa view ou numa função do seu programa | só um pedaço muda na migração | dá 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.
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.
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).
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`.
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, ')').
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
| Assunto | SQLite | MySQL / MariaDB | PostgreSQL | SQL Server |
|---|---|---|---|---|
| Limitar | LIMIT 3 | LIMIT 3 | LIMIT 3 / FETCH FIRST 3 ROWS ONLY | TOP (3) / OFFSET 0 ROWS FETCH NEXT 3 ROWS ONLY |
| Aspas de nome | "x" (aceita `x`, [x]) | `x` | "x" | [x] ou "x" |
| Texto | sempre aspas simples: 'Teresina' (no SQL Server, N'Teresina' para texto Unicode) | |||
| id automático | INTEGER PRIMARY KEY | AUTO_INCREMENT | GENERATED … AS IDENTITY / SERIAL | IDENTITY(1,1) |
| Juntar texto | ||, concat() | CONCAT() | ||, concat() | +, CONCAT(), || (2025) |
| Hoje | date('now') | CURDATE() | CURRENT_DATE | CAST(GETDATE() AS date) |
| Somar dias | date(d,'+7 days') | DATE_ADD(d, INTERVAL 7 DAY) | d + 7 | DATEADD(day, 7, d) |
| Booleano | 0/1 | BOOLEAN = TINYINT(1) | BOOLEAN (t/f) | bit |
| UPSERT | ON CONFLICT … DO UPDATE | ON DUPLICATE KEY UPDATE | ON CONFLICT … DO UPDATE | MERGE |
| Erro de tabela inexistente | no such table: x | ERROR 1146 (42S02): Table 'loja.x' doesn't exist | ERRO: relação "x" não existe | Invalid 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:
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:
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 comOFFSET, 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
booleancom 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.