SQL no PostgreSQL
O PostgreSQL (diz-se "póstgres-quiu-él", e quase todo mundo o chama só de "Postgres") é um banco de dados de código aberto, gratuito, mantido por uma comunidade, e é a escolha de muitas empresas e de muitos programadores para sistemas novos. A Ana montou um PostgreSQL no notebook e carregou nele a Loja Aurora, para ver o que muda em relação ao SQLite. A boa notícia: o PostgreSQL é o banco que segue mais de perto o SQL padrão, então quase tudo o que você aprendeu funciona igual, e várias coisas (||, LIMIT, ON CONFLICT, RETURNING) são escritas exatamente como no SQLite. As diferenças estão nos tipos, que lá são rigorosos, em algumas funções e no programa de terminal, o psql.
- Como instalar o PostgreSQL, criar o seu usuário e o seu banco, e abrir o
psql. - Os comandos de barra do
psql(\dt,\d,\x,\pset null,\q), o "irmão" dos comandos de ponto dosqlite3. - O que é igual ao SQLite (
LIMIT,||,ON CONFLICT,RETURNING) e o que muda: nomes em minúsculas,GENERATED AS IDENTITY, datas, booleanos,ILIKE,MERGE. - Por que o PostgreSQL recusa coisas que o SQLite aceita: tipos rígidos,
GROUP BYestrito, divisão por zero. - As mensagens de erro reais, em português, e o que fazer com cada uma, inclusive a "transação interrompida".
Os quadros "Resultado" rodam o SQLite no seu navegador, como em todo o guia. As sessões de terminal do psql (as que começam com loja=>) também são reais: saíram de um PostgreSQL 17 na máquina da Ana, com os mesmos dados da Loja Aurora. Como o computador da Ana está em português, as mensagens do PostgreSQL saem em português. Os comandos de instalação, que pedem a senha de administrador, aparecem como ilustrativos; eles e as regras citadas foram conferidos na documentação oficial (postgresql.org).
A ideia: o primo mais rigoroso
Se o SQLite é aquele amigo que aceita tudo ("você quis dizer texto? então tá"), o PostgreSQL é o professor que corrige a prova com caneta vermelha: um texto onde devia ir número, uma coluna esquecida no GROUP BY, um estado com três letras, e ele recusa, com uma mensagem que explica o que houve. Dá um pouco mais de trabalho no começo e evita muitos dados errados depois.
| SQLite | PostgreSQL | |
|---|---|---|
| Como funciona | o banco é um arquivo, aberto pelo próprio programa | um servidor sempre ligado; os programas (como o psql) se conectam a ele |
| Usuários | não há: quem abre o arquivo, usa | cada conexão entra com um usuário do banco, com as suas permissões |
| Tipos | flexíveis (aula Os Tipos no SQLite) | rígidos: cada coluna aceita só o seu tipo |
| Programa de terminal | sqlite3 | psql |
| Comandos do programa | de ponto: .tables, .schema | de barra: \dt, \d |
1. Instalar e criar o seu banco
No Debian e no Ubuntu, o PostgreSQL vem nos repositórios da própria distribuição. A página de download do PostgreSQL indica um comando só (ilustrativo; precisa da senha de administrador):
sudo apt install postgresql
No Windows, a página oficial de download aponta o instalador interativo da EDB. Depois de instalar, falta uma coisa: no PostgreSQL, os usuários do banco são separados dos usuários do computador. A documentação explica que, para criar a primeira conta, é preciso agir como o usuário do sistema sob o qual o PostgreSQL foi instalado, que costuma se chamar postgres. Os programas createuser e createdb fazem o serviço (ilustrativo):
sudo -u postgres createuser ana sudo -u postgres createdb -O ana loja
- sudo -u postgres
- Roda o comando seguinte como o usuário
postgresdo sistema, o "dono" do servidor. - createuser ana
- Cria, dentro do PostgreSQL, um usuário chamado
ana. Com o mesmo nome do usuário do computador, opsqljá sabe quem você é e não precisa de-U. - createdb -O ana loja
- Cria o banco
loja, com a Ana como dona (-O, de owner). Um servidor guarda vários bancos; normalmente, um para cada projeto.
Para ter a Loja Aurora lá, a Ana criou as tabelas com os tipos do PostgreSQL (seção 9) e carregou os mesmos dados do guia. A partir daqui, tudo é real.
2. O psql e os comandos de barra
O psql é o programa de terminal do PostgreSQL. Ele se abre com o nome do banco:
$ psql loja
psql (17.11 (Debian 17.11-0+deb13u1))
Digite "help" para obter ajuda.
loja=> \dt
Lista de relações
Esquema | Nome | Tipo | Dono
---------+--------------+--------+------
public | categorias | tabela | ana
public | clientes | tabela | ana
public | fornecedores | tabela | ana
public | funcionarios | tabela | ana
public | itens_pedido | tabela | ana
public | pedidos | tabela | ana
public | produtos | tabela | ana
(7 linhas)
loja=> \q
- loja=>
- O prompt: o nome do banco e
=>. A documentação explica que, para um superusuário do banco, o sinal é#(loja=#) em vez de>. - \dt
- "Descreva as tabelas" (describe tables): o
.tablesdosqlite3. Opublicé o esquema, uma espécie de pasta dentro do banco; quem não cria outras usa sempre opublic. - \q
- Sai do
psql(quit), como o.quit.
Assim como os comandos de ponto, os comandos de barra são do programa psql, não do SQL: não levam ; e não funcionam em outros programas. Já as consultas SQL, sim, terminam em ;, como no sqlite3. Daqui em diante, as sessões mostram só o que a Ana digitou dentro do psql. O \d produtos é o .schema produtos, bem mais detalhado:
loja=> \d produtos
Tabela "public.produtos"
Coluna | Tipo | Ordenação | Pode ser nulo | Padrão
---------------+------------------------+-----------+---------------+----------------------------------
id | integer | | not null | generated by default as identity
nome | character varying(100) | | not null |
categoria_id | integer | | not null |
fornecedor_id | integer | | not null |
preco | numeric(10,2) | | not null |
estoque | integer | | not null | 0
descontinuado | boolean | | not null | false
Índices:
"produtos_pkey" PRIMARY KEY, btree (id)
Restrições de chave estrangeira:
"produtos_categoria_id_fkey" FOREIGN KEY (categoria_id) REFERENCES categorias(id)
"produtos_fornecedor_id_fkey" FOREIGN KEY (fornecedor_id) REFERENCES fornecedores(id)
Referenciada por:
TABLE "itens_pedido" CONSTRAINT "itens_pedido_produto_id_fkey" FOREIGN KEY (produto_id) REFERENCES produtos(id)
Ele mostra as colunas com os tipos, quem pode ser nulo, os valores padrão, o índice da chave primária, as chaves estrangeiras que saem da tabela e as que chegam nela ("Referenciada por"). Repare que o PostgreSQL deu nomes às restrições sozinho, como produtos_pkey.
Mostrar o NULL e virar a tabela de lado
Dois ajustes do psql que ajudam muito. Por padrão, ele mostra o NULL como nada, e a documentação admite que isso "pode ser facilmente confundido com um texto vazio". O \pset null escolhe o que mostrar no lugar. E o \x liga a "exibição expandida", uma coluna por linha, ótima para tabelas largas:
loja=> \pset null '(nulo)' A exibição de nulos é "(nulo)". loja=> SELECT id, nome, email FROM clientes WHERE id IN (6, 7); id | nome | email ----+-------------------+-------------------------------- 6 | Rafael Monteiro | rafael.monteiro@exemplo.com.br 7 | Luís Otávio Brito | (nulo) (2 linhas) loja=> \x A exibição expandida está ativada. loja=> SELECT * FROM fornecedores WHERE id = 2; -[ RECORD 1 ]------------- id | 2 nome | Loja D'Água cidade | Fortaleza estado | CE pais | Brasil telefone | (85) 98712-3344 loja=> \x A exibição expandida está desativada.
| Tarefa | sqlite3 | psql |
|---|---|---|
| Abrir o banco | sqlite3 loja.db | psql loja (ou psql -U usuario -h servidor loja) |
| Listar as tabelas | .tables | \dt |
| Ver a estrutura de uma tabela | .schema produtos | \d produtos |
| Como mostrar o NULL | .nullvalue (nulo) | \pset null '(nulo)' |
| Uma coluna por linha | .mode line | \x |
| Rodar um arquivo .sql | .read arquivo.sql | \i arquivo.sql |
| Ajuda dos comandos | .help | \? |
| Sair | .quit | \q |
3. Limitar linhas: igual ao SQLite
Aqui não há o que traduzir. A "página 2" dos produtos mais caros, no SQLite:
SELECT nome, preco FROM produtos ORDER BY preco DESC LIMIT 3 OFFSET 3;
No PostgreSQL, o mesmo texto, e também a forma do padrão SQL, com FETCH FIRST:
loja=> SELECT nome, preco FROM produtos ORDER BY preco DESC LIMIT 3 OFFSET 3;
nome | preco
------------------------+--------
Faqueiro Inox 24 peças | 119.40
Teclado Compacto | 99.00
Mochila Escolar Azul | 89.99
(3 linhas)
loja=> SELECT nome, preco FROM produtos ORDER BY preco DESC OFFSET 3 ROWS FETCH FIRST 3 ROWS ONLY;
nome | preco
------------------------+--------
Faqueiro Inox 24 peças | 119.40
Teclado Compacto | 99.00
Mochila Escolar Azul | 89.99
(3 linhas)
Repare nos preços: 119.40 e 99.00, com duas casas, enquanto o SQLite mostra 119.4 e 99.0. No PostgreSQL da Ana, o preço é numeric(10,2), um número exato com 2 casas decimais. A forma do MySQL, LIMIT 3, 3, o PostgreSQL recusa e explica:
loja=> SELECT nome FROM produtos LIMIT 3, 3;
ERRO: sintaxe LIMIT #,# não tem suporte
LINHA 1: SELECT nome FROM produtos LIMIT 3, 3;
^
DICA: Use cláusulas LIMIT e OFFSET separadas.
Repare no formato das mensagens: a linha ERRO:, a LINHA 1: com a consulta, a setinha ^ apontando o lugar exato e, às vezes, uma DICA:. Leia sempre a dica.
4. Nomes: minúsculas, e aspas que mudam tudo
No PostgreSQL, um nome escrito sem aspas é convertido para minúsculas. A documentação diz: "nomes sem aspas são sempre convertidos para minúsculas", e FOO, foo e "foo" são o mesmo nome. Por isso Nome funciona. Mas com aspas duplas, o nome 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".
A lição é a mesma do guia inteiro: nomes de tabelas e colunas em minúsculas, sem espaço e sem acento, e você nunca vai precisar de aspas. (A documentação lembra que o padrão SQL manda converter para maiúsculas, e que o PostgreSQL faz o contrário; quem escreve tudo em minúsculas não sente a diferença.)
E aspas duplas para texto? No PostgreSQL, como no editor do guia, elas são nome de coluna:
loja=> SELECT nome FROM clientes WHERE cidade = "Teresina";
ERRO: coluna "Teresina" não existe
LINHA 1: SELECT nome FROM clientes WHERE cidade = "Teresina";
^
SELECT nome FROM clientes WHERE cidade = "Teresina";
O editor recusa do mesmo jeito. (O sqlite3 do Debian, no terminal, aceita "Teresina" como texto quando não existe coluna com esse nome; não conte com isso.) Texto vai entre aspas simples.
5. Maiúsculas no LIKE: ILIKE
No PostgreSQL, o LIKE diferencia maiúsculas de minúsculas: 'josé%' não acha "José". Para comparar sem ligar para isso, existe o ILIKE (o "i" é de insensitive), que, segundo a documentação, faz a comparação "de acordo com o idioma ativo" e não faz parte do padrão SQL: é uma extensão do PostgreSQL. Com o idioma em português, ele acerta até as letras acentuadas:
loja=> SELECT nome FROM clientes WHERE nome LIKE 'josé%';
nome
------
(0 linha)
loja=> SELECT nome FROM clientes WHERE nome ILIKE 'JOSÉ%';
nome
--------------------
José Ribamar Silva
(1 linha)
No SQLite é diferente: o LIKE já ignora maiúsculas, mas só nas letras sem acento (aula LIKE), e não existe ILIKE:
SELECT nome FROM clientes WHERE nome LIKE 'josé%'; SELECT nome FROM clientes WHERE nome LIKE 'JOSÉ%';
Experimente: a Ana copiou esta consulta do PostgreSQL, que procura as clientes chamadas Ana. No SQLite, ela dá erro. Troque a palavra que só o PostgreSQL conhece pela do SQLite (aqui funciona, porque "ana" não tem acento).
SELECT nome FROM clientes WHERE nome ILIKE 'ana%';
SELECT nome FROM clientes WHERE nome LIKE 'ana%';
6. Numeração automática: GENERATED AS IDENTITY (e o velho SERIAL)
O jeito do padrão SQL, que o PostgreSQL recomenda, é a coluna de identidade. A Ana criou uma tabela de avisos para testar:
loja=> CREATE TABLE avisos (id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, texto TEXT NOT NULL);
CREATE TABLE
loja=> INSERT INTO avisos (texto) VALUES ('Loja fecha às 18h') RETURNING id;
id
----
1
(1 linha)
INSERT 0 1
loja=> INSERT INTO avisos (id, texto) VALUES (10, 'Promoção de papelaria');
ERRO: não é possível inserir um valor diferente de DEFAULT na coluna "id"
DETALHE: Column "id" is an identity column defined as GENERATED ALWAYS.
DICA: Use OVERRIDING SYSTEM VALUE to override.
- GENERATED ALWAYS AS IDENTITY
- "Gerado sempre, como identidade": o banco numera, e só o banco. Escolher o
idna mão dá o erro acima. (ODETALHEe aDICAsaíram em inglês: nem toda mensagem do PostgreSQL tem tradução.) - GENERATED BY DEFAULT AS IDENTITY
- "Gerado por padrão": o banco numera se você não disser nada, mas aceita um
idescolhido. É o que a Loja Aurora usa no PostgreSQL (veja no\d produtoslá em cima). - INSERT 0 1
- A resposta do
psqla umINSERT: o 1 é o número de linhas inseridas (o 0 é um resto histórico e é sempre 0).
Em códigos mais antigos, você vai ver id SERIAL PRIMARY KEY. A documentação descreve o serial como "um jeito específico do PostgreSQL" de criar uma coluna que se numera sozinha, e aponta a coluna de identidade como o jeito do padrão SQL. No SQLite, o equivalente é o INTEGER PRIMARY KEY (aula AUTOINCREMENT).
7. RETURNING, ON CONFLICT e MERGE
Três recursos que o SQLite pegou emprestados do PostgreSQL, então a escrita é a mesma.
RETURNING
Devolve as linhas que o comando mexeu. No SQLite:
INSERT INTO categorias (nome, descricao)
VALUES ('Pet', 'Produtos para animais')
RETURNING id, nome;
No PostgreSQL, o mesmo texto, e também num UPDATE. Compare o preço:
loja=> INSERT INTO categorias (nome, descricao) VALUES ('Pet', 'Produtos para animais') RETURNING id, nome;
id | nome
----+------
9 | Pet
(1 linha)
INSERT 0 1
loja=> UPDATE produtos SET preco = preco * 1.1 WHERE id = 1 RETURNING id, nome, preco;
id | nome | preco
----+---------------------------+-------
1 | Caderno Pautado 96 folhas | 13.59
(1 linha)
UPDATE 1
UPDATE produtos SET preco = preco * 1.1 WHERE id = 1 RETURNING id, nome, preco;
O SQLite guardou 13.585; o PostgreSQL, 13.59. A coluna numeric(10,2) só tem duas casas, e o valor foi arredondado ao ser gravado. Para dinheiro, é isso que você quer.
ON CONFLICT: o UPSERT
"Insere, e se já existe, atualiza". Igual nos dois, inclusive o excluded, a "tabela especial" que, diz a documentação, representa a linha que se tentou inserir (no PostgreSQL, a Ana escreveu EXCLUDED em maiúsculas, que é o mesmo nome):
INSERT INTO categorias (id, nome, descricao) VALUES (8, 'Jardim e Quintal', 'Ferramentas e artigos de jardinagem') ON CONFLICT (id) DO UPDATE SET nome = excluded.nome; SELECT * FROM categorias WHERE id = 8;
loja=> INSERT INTO categorias (id, nome, descricao) VALUES (8, 'Jardim e Quintal', 'Ferramentas e artigos de jardinagem') ON CONFLICT (id) DO UPDATE SET nome = EXCLUDED.nome; INSERT 0 1 loja=> SELECT * FROM categorias WHERE id = 8; id | nome | descricao ----+------------------+------------------------------------- 8 | Jardim e Quintal | Ferramentas e artigos de jardinagem (1 linha)
MERGE
O PostgreSQL também tem o MERGE do padrão SQL (desde a versão 15), o mesmo comando que o SQL Server usa (aula SQL no SQL Server). O SQLite não tem:
loja=> MERGE INTO categorias AS destino USING (VALUES (8, 'Jardim e Quintal', 'Ferramentas e artigos de jardinagem')) AS origem (id, nome, descricao) ON destino.id = origem.id WHEN MATCHED THEN UPDATE SET nome = origem.nome WHEN NOT MATCHED THEN INSERT (nome, descricao) VALUES (origem.nome, origem.descricao); MERGE 1
MERGE 1: uma linha mexida (a categoria 8 existia, então foi o WHEN MATCHED). Para o UPSERT simples, o ON CONFLICT é mais curto; o MERGE serve quando há várias regras (atualizar umas, inserir outras, apagar outras). Desde o PostgreSQL 17, o MERGE também aceita RETURNING.
8. Textos e datas
Juntar textos
|| e concat() funcionam como no SQLite: com ||, um NULL deixa tudo NULL; o concat(), segundo a documentação, ignora os argumentos NULL. O cliente 7 não tem email:
SELECT nome || ' - ' || email AS com_barras,
concat(nome, ' - ', email) AS com_concat
FROM clientes
WHERE id IN (6, 7);
loja=> SELECT nome || ' - ' || email AS com_barras, concat(nome, ' - ', email) AS com_concat FROM clientes WHERE id IN (6, 7);
com_barras | com_concat
--------------------------------------------------+--------------------------------------------------
Rafael Monteiro - rafael.monteiro@exemplo.com.br | Rafael Monteiro - rafael.monteiro@exemplo.com.br
| Luís Otávio Brito -
(2 linhas)
A célula vazia na segunda linha é o NULL (nesta sessão a Ana não tinha ligado o \pset null). Para juntar os textos de um grupo, os dois bancos têm string_agg(nome, ', ') (no SQLite, desde a versão 3.44, que também tem o antigo group_concat).
Datas
No PostgreSQL, data é do tipo date, uma data de verdade, e as contas ficam mais curtas: somar um número a uma data soma dias, e subtrair duas datas dá o número de dias entre elas (a tabela de operadores da documentação diz: date + integer → date e date - date → integer):
loja=> SELECT id, data, data + 7 AS prazo, entregue_em - data AS dias, extract(year FROM data) AS ano, to_char(data, 'DD/MM/YYYY') AS data_br FROM pedidos WHERE id = 1;
id | data | prazo | dias | ano | data_br
----+------------+------------+------+------+------------
1 | 2024-01-05 | 2024-01-12 | 5 | 2024 | 05/01/2024
(1 linha)
loja=> SELECT data, data + INTERVAL '1 month' AS um_mes_depois FROM pedidos WHERE id = 1;
data | um_mes_depois
------------+---------------------
2024-01-05 | 2024-02-05 00:00:00
(1 linha)
O mesmo no SQLite, que guarda a data como texto e usa funções:
SELECT id, data,
date(data, '+7 days') AS prazo,
julianday(entregue_em) - julianday(data) AS dias,
strftime('%Y', data) AS ano,
strftime('%d/%m/%Y', data) AS data_br
FROM pedidos
WHERE id = 1;
| Tarefa | SQLite | PostgreSQL |
|---|---|---|
| Somar 7 dias | date(d, '+7 days') | d + 7 ou d + INTERVAL '7 days' |
| Somar um mês | date(d, '+1 month') | d + INTERVAL '1 month' (o resultado vira data e hora) |
| Dias entre datas | julianday(b) - julianday(a) (dá 5.0) | b - a (dá 5, um inteiro) |
| Ano | strftime('%Y', d) (texto) | extract(year FROM d) (número) |
| Formato brasileiro | strftime('%d/%m/%Y', d) | to_char(d, 'DD/MM/YYYY') |
| Hoje | date('now') | current_date |
Experimente: no PostgreSQL, a Ana achou os pedidos que levaram mais de 7 dias para chegar com WHERE entregue_em - data > 7. No SQLite, subtrair dois textos de data não dá dias (ela tentou: a consulta abaixo não acha nada). Troque a conta pela do SQLite, com julianday().
SELECT id, data, entregue_em FROM pedidos WHERE entregue_em - data > 7;
SELECT id, data, entregue_em FROM pedidos WHERE julianday(entregue_em) - julianday(data) > 7;
9. Tipos rígidos: boolean, numeric, varchar
Na Loja Aurora do PostgreSQL, a Ana usou tipos que o SQLite não tem de verdade:
| Coluna | No SQLite | No PostgreSQL |
|---|---|---|
id | INTEGER PRIMARY KEY | INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY |
nome | TEXT | VARCHAR(100) (ou TEXT, que também existe lá) |
estado | TEXT | CHAR(2) |
preco | REAL | NUMERIC(10,2) |
data | TEXT | DATE |
vip | INTEGER (0 ou 1) | BOOLEAN |
E o PostgreSQL faz valer cada tipo:
loja=> SELECT COUNT(*) FROM clientes WHERE vip;
count
-------
5
(1 linha)
loja=> SELECT COUNT(*) FROM clientes WHERE vip = 1;
ERRO: operador não existe: boolean = integer
LINHA 1: SELECT COUNT(*) 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.
loja=> UPDATE clientes SET estado = 'PIA' WHERE id = 1;
ERRO: valor é muito longo para tipo character(2)
loja=> SELECT estado + 1 FROM clientes LIMIT 1;
ERRO: operador não existe: character + integer
LINHA 1: SELECT estado + 1 FROM clientes LIMIT 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.
- WHERE vip
- Com uma coluna
boolean, a condição pode ser a própria coluna: "onde vip é verdadeiro". Também valeWHERE vip = true. - operador não existe: boolean = integer
- Um booleano não se compara com um número. No SQLite,
vip = 1é o normal. - valor é muito longo para tipo character(2)
- O estado tem 2 letras, e o PostgreSQL não guarda 3. A documentação explica que tentar guardar um texto mais longo que o tamanho declarado é erro.
- operador não existe: character + integer
- Somar número com texto não existe. (Um texto escrito na consulta, como
'12' + 1, o PostgreSQL ainda converte e dá 13; uma coluna de texto, não.)
No SQLite, as mesmas coisas passam caladas:
SELECT COUNT(*) AS vips FROM clientes WHERE vip = 1; UPDATE clientes SET estado = 'PIA' WHERE id = 1 RETURNING id, estado; SELECT estado + 1 AS soma FROM clientes LIMIT 1;
O SQLite gravou "PIA" num campo de estado e calculou 'PI' + 1 = 1 sem reclamar. O rigor do PostgreSQL parece chato, mas é isso que impede um estado de três letras de entrar no cadastro.
Divisão
loja=> SELECT 7 / 2 AS inteiros, 7 / 2.0 AS com_decimal;
inteiros | com_decimal
----------+--------------------
3 | 3.5000000000000000
(1 linha)
loja=> SELECT 1 / 0;
ERRO: divisão por zero
SELECT 7 / 2 AS inteiros, 7 / 2.0 AS com_decimal, 1 / 0 AS por_zero;
Inteiro dividido por inteiro corta a parte quebrada nos dois (a documentação do PostgreSQL: a divisão de inteiros "trunca o resultado em direção a zero"). A diferença é a divisão por zero: erro no PostgreSQL, NULL no SQLite. E o 7 / 2.0 do PostgreSQL sai com muitas casas, porque é um numeric.
10. GROUP BY estrito (com uma exceção esperta)
A coluna "solta" no GROUP BY, que o SQLite aceita mostrando um valor qualquer (aula Agrupando por Mais de uma Coluna), o PostgreSQL recusa:
loja=> SELECT estado, cidade, COUNT(*) FROM clientes GROUP BY estado;
ERRO: coluna "clientes.cidade" deve aparecer na cláusula GROUP BY ou ser usada em uma função de agregação
LINHA 1: SELECT estado, cidade, COUNT(*) FROM clientes GROUP BY estad...
^
Mas há uma exceção que a documentação explica: se você agrupa pela chave primária de uma tabela, as outras colunas daquela tabela podem aparecer sem estar no GROUP BY, porque cada id tem um nome só, uma cidade só, e não há dúvida sobre qual valor mostrar (a documentação chama isso de dependência funcional). Por isso esta consulta funciona:
loja=> SELECT c.id, c.nome, c.cidade, COUNT(p.id) AS pedidos FROM clientes c LEFT JOIN pedidos p ON p.cliente_id = c.id WHERE c.id >= 28 GROUP BY c.id; id | nome | cidade | pedidos ----+----------------+-----------+--------- 29 | Elias Furtado | Teresina | 0 30 | Rosângela Dias | Fortaleza | 0 28 | Tainá Soares | São Paulo | 0 (3 linhas)
(Repare na ordem 29, 30, 28: sem ORDER BY, nenhum banco promete ordem.) Para a consulta funcionar em qualquer banco, ponha no GROUP BY todas as colunas que aparecem soltas: GROUP BY c.id, c.nome, c.cidade.
11. A transação interrompida
Esta é uma diferença que pega todo mundo que vem do SQLite. Dentro de uma transação (aula Transações), se um comando dá erro, o PostgreSQL interrompe a transação inteira: ele não aceita mais nada até você desfazê-la. Veja o prompt mudar:
loja=> BEGIN;
BEGIN
loja=*> UPDATE produtos SET estoque = estoque - 1 WHERE id = 1;
UPDATE 1
loja=*> SELECT nome FROM produto WHERE id = 1;
ERRO: relação "produto" não existe
LINHA 1: SELECT nome FROM produto WHERE id = 1;
^
loja=!> SELECT estoque FROM produtos WHERE id = 1;
ERRO: transação atual foi interrompida, comandos ignorados até o fim do bloco de transação
loja=!> ROLLBACK;
ROLLBACK
loja=> SELECT estoque FROM produtos WHERE id = 1;
estoque
---------
120
(1 linha)
- loja=*>
- O
*no prompt quer dizer "dentro de uma transação" (a documentação dopsqlexplica os sinais). - relação "produto" não existe
- Um erro de digitação: a tabela é
produtos. ("Relação" é como o PostgreSQL chama tabelas e coisas parecidas.) - loja=!>
- O
!quer dizer "transação com falha". A partir daqui, até um comando certo é recusado, com "transação atual foi interrompida, comandos ignorados até o fim do bloco de transação". - ROLLBACK
- Desfaz a transação (aula ROLLBACK e Savepoints). O estoque voltou a 120: o
UPDATEdo começo também foi desfeito.
A Ana fez o mesmo no sqlite3, para comparar:
$ sqlite3 loja.db SQLite version 3.50.4 2025-07-30 19:33:53 Enter ".help" for usage hints. sqlite> BEGIN; sqlite> UPDATE produtos SET estoque = estoque - 1 WHERE id = 1; sqlite> SELECT nome FROM produto WHERE id = 1; Parse error: no such table: produto sqlite> SELECT estoque FROM produtos WHERE id = 1; 119 sqlite> ROLLBACK; sqlite> SELECT estoque FROM produtos WHERE id = 1; 120 sqlite> .quit
No SQLite, o comando errado falhou sozinho e a transação continuou: o SELECT seguinte rodou e mostrou o estoque já com o UPDATE (119). No PostgreSQL, depois de um erro é tudo ou nada. Quando ver o ! no prompt, digite ROLLBACK; e comece de novo.
Formas que funcionam nos dois
| Tarefa | Escreva assim | Por quê |
|---|---|---|
| Limitar e paginar | LIMIT n OFFSET m | igual nos dois |
| Juntar textos | || ou concat() | igual nos dois |
| Valor no lugar de NULL | coalesce(x, y) | padrão SQL; o ifnull do SQLite não existe no PostgreSQL |
| Inserir ou atualizar | INSERT … ON CONFLICT (…) DO UPDATE SET … = excluded.… | igual nos dois |
| Saber o id criado | … RETURNING id | igual nos dois |
| VIP | WHERE vip = true | funciona com boolean no PostgreSQL e com 0/1 no SQLite |
| GROUP BY | todas as colunas soltas no GROUP BY | o SQLite aceita tudo; o PostgreSQL, não |
| Nomes | minúsculos, sem aspas | as aspas duplas mudam o sentido no PostgreSQL |
Quando dá errado
As mensagens abaixo são reais, do PostgreSQL 17 da Ana, em português. Ao lado, o que o SQLite diria.
relação "cliente" não existe
loja=> SELECT nome FROM cliente;
ERRO: relação "cliente" não existe
LINHA 1: SELECT nome FROM cliente;
^
A tabela se chama clientes. No SQLite: no such table: cliente. Se o nome está certo e o erro continua, confira se você abriu o banco certo (psql loja) e se não criou a tabela com aspas e maiúsculas ("Clientes").
erro de sintaxe em ou próximo a "3": o TOP do SQL Server
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;
^
O PostgreSQL leu TOP como o nome de uma coluna e tropeçou no 3. É a mesma reação do SQLite (near "3": syntax error). Como consertar: … ORDER BY … LIMIT 3.
operador não existe
Visto na seção 9: você comparou ou somou tipos diferentes (boolean = integer, character + integer). Leia a DICA: "talvez seja necessário adicionar uma conversão de tipo de dados explícita". Às vezes a solução é comparar com o tipo certo (vip = true); às vezes é converter com CAST(x AS integer) ou com x::integer, a forma histórica do PostgreSQL (o CAST é a do padrão SQL).
deve aparecer na cláusula GROUP BY
Visto na seção 10. Como consertar: ponha a coluna no GROUP BY, numa função de agregação, ou agrupe pela chave primária da tabela dela.
transação atual foi interrompida
Visto na seção 11: um comando anterior, na mesma transação, deu erro. Como consertar: ROLLBACK; e refaça a transação, já sem o erro.
loja=> SELECT ifnull(NULL, 'x');
ERRO: função ifnull(unknown, unknown) não existe
LINHA 1: SELECT ifnull(NULL, 'x');
^
DICA: Nenhuma função corresponde com o nome e os tipos de argumentos informados. Você precisa adicionar conversões de tipo explícitas.
loja=> SELECT julianday('2024-01-05');
ERRO: função julianday(unknown) não existe
LINHA 1: SELECT julianday('2024-01-05');
^
DICA: Nenhuma função corresponde com o nome e os tipos de argumentos informados. Você precisa adicionar conversões de tipo explícitas.
Funções como ifnull(), julianday(), strftime() e date(d, '+7 days') são do SQLite. A DICA fala em conversões de tipo porque o PostgreSQL também diferencia funções pelo tipo dos argumentos; aqui o problema é mais simples: a função não existe. No PostgreSQL, use coalesce(), a subtração de datas, extract()/to_char() e d + 7 (seção 8). O caminho inverso também vale: ILIKE, extract() e to_char() não existem no SQLite.
Referência rápida: SQLite × PostgreSQL
| Tarefa | SQLite | PostgreSQL |
|---|---|---|
| Programa de terminal | sqlite3 loja.db | psql loja |
| Listar tabelas / estrutura | .tables / .schema t | \dt / \d t |
| Limitar | LIMIT 3 OFFSET 3 | igual, ou OFFSET 3 ROWS FETCH FIRST 3 ROWS ONLY |
| Nome com maiúscula | tanto faz | sem aspas vira minúsculo; com aspas vale letra por letra |
| LIKE sem ligar para maiúsculas | LIKE (só letras sem acento) | ILIKE |
| Numeração automática | INTEGER PRIMARY KEY | GENERATED … AS IDENTITY (ou SERIAL) |
| Juntar textos | ||, concat() | ||, concat() |
| Trocar NULL | ifnull, coalesce | coalesce |
| Somar dias | date(d, '+7 days') | d + 7 |
| Dias entre datas | julianday(b) - julianday(a) | b - a |
| Ano / formato BR | strftime('%Y', d) / strftime('%d/%m/%Y', d) | extract(year FROM d) / to_char(d, 'DD/MM/YYYY') |
| Booleano | 0 / 1 | boolean: true / false |
| Dinheiro | REAL | NUMERIC(10,2) |
| UPSERT | ON CONFLICT … DO UPDATE | igual, e também MERGE |
| Devolver o que mudou | RETURNING | RETURNING |
| 1 / 0 | NULL | ERRO: divisão por zero |
| Erro dentro de uma transação | desfaz só o comando | interrompe a transação: ROLLBACK |
Exercícios
1. Os comandos de barra
No psql, qual comando lista as tabelas do banco (o .tables do sqlite3)?
2. Sem ligar para maiúsculas
Complete a consulta do PostgreSQL que acha "José Ribamar Silva" mesmo escrita em maiúsculas:
3. No PostgreSQL, a coluna vip é boolean. Qual destas condições dá erro?
4. O prompt do psql virou loja=!> e nenhum comando funciona. O que fazer?
5. No PostgreSQL, SELECT "Nome" FROM clientes; dá coluna "Nome" não existe, mas SELECT Nome FROM clientes; funciona. Por quê?
Missão final: traduza do PostgreSQL para o SQLite
Missão: no PostgreSQL, a Ana contou os pedidos de cada ano com a consulta abaixo. No SQLite, a função extract não existe. Reescreva para o SQLite, mantendo as colunas ano e pedidos.
SELECT extract(year FROM data) AS ano, COUNT(*) AS pedidos FROM pedidos GROUP BY ano ORDER BY ano;
SELECT strftime('%Y', data) AS ano, COUNT(*) AS pedidos
FROM pedidos
GROUP BY ano
ORDER BY ano;
32 pedidos em 2024, 32 em 2025 e 26 em 2026 (até outubro). Com isso você viu os dois bancos com servidor mais usados ao lado do SQLite. A aula As Diferenças entre os Bancos reúne os quatro numa tabela só, e SQL nas Linguagens mostra como um programa conversa com eles.
Resumo
- O PostgreSQL é um banco com servidor, gratuito e de código aberto, e o que mais segue o SQL padrão. No Debian/Ubuntu,
apt install postgresql; depois,createuserecreatedbcomo o usuáriopostgres, epsql loja. - No
psql, os comandos de barra (\dt,\d tabela,\x,\pset null,\i,\q) fazem o papel dos comandos de ponto dosqlite3. - Iguais ao SQLite:
LIMIT/OFFSET,||,concat(),ON CONFLICT,RETURNING,string_agg. - Diferentes: nomes sem aspas viram minúsculos;
ILIKE;GENERATED AS IDENTITY; datas comd + 7,b - a,extract,to_char;booleande verdade;MERGE. - O PostgreSQL é rigoroso: tipos não se misturam (
operador não existe), textos não passam do tamanho,GROUP BYsem colunas soltas (salvo agrupando pela chave primária), divisão por zero é erro. - Um erro dentro de uma transação a interrompe inteira (
loja=!>): digiteROLLBACK;.