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.

Você vai aprender
  • 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 do sqlite3.
  • 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 BY estrito, divisão por zero.
  • As mensagens de erro reais, em português, e o que fazer com cada uma, inclusive a "transação interrompida".
O que roda de verdade nesta página

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.

SQLitePostgreSQL
Como funcionao banco é um arquivo, aberto pelo próprio programaum servidor sempre ligado; os programas (como o psql) se conectam a ele
Usuáriosnão há: quem abre o arquivo, usacada conexão entra com um usuário do banco, com as suas permissões
Tiposflexíveis (aula Os Tipos no SQLite)rígidos: cada coluna aceita só o seu tipo
Programa de terminalsqlite3psql
Comandos do programade ponto: .tables, .schemade 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 postgres do 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, o psql já 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 .tables do sqlite3. O public é o esquema, uma espécie de pasta dentro do banco; quem não cria outras usa sempre o public.
\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.
Tarefasqlite3psql
Abrir o bancosqlite3 loja.dbpsql 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 id na mão dá o erro acima. (O DETALHE e a DICA saí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 id escolhido. É o que a Loja Aurora usa no PostgreSQL (veja no \d produtos lá em cima).
INSERT 0 1
A resposta do psql a um INSERT: 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;
TarefaSQLitePostgreSQL
Somar 7 diasdate(d, '+7 days')d + 7 ou d + INTERVAL '7 days'
Somar um mêsdate(d, '+1 month')d + INTERVAL '1 month' (o resultado vira data e hora)
Dias entre datasjulianday(b) - julianday(a) (dá 5.0)b - a (dá 5, um inteiro)
Anostrftime('%Y', d) (texto)extract(year FROM d) (número)
Formato brasileirostrftime('%d/%m/%Y', d)to_char(d, 'DD/MM/YYYY')
Hojedate('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:

ColunaNo SQLiteNo PostgreSQL
idINTEGER PRIMARY KEYINTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY
nomeTEXTVARCHAR(100) (ou TEXT, que também existe lá)
estadoTEXTCHAR(2)
precoREALNUMERIC(10,2)
dataTEXTDATE
vipINTEGER (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 vale WHERE 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 do psql explica 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 UPDATE do 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

TarefaEscreva assimPor quê
Limitar e paginarLIMIT n OFFSET migual nos dois
Juntar textos|| ou concat()igual nos dois
Valor no lugar de NULLcoalesce(x, y)padrão SQL; o ifnull do SQLite não existe no PostgreSQL
Inserir ou atualizarINSERT … ON CONFLICT (…) DO UPDATE SET … = excluded.…igual nos dois
Saber o id criado… RETURNING idigual nos dois
VIPWHERE vip = truefunciona com boolean no PostgreSQL e com 0/1 no SQLite
GROUP BYtodas as colunas soltas no GROUP BYo SQLite aceita tudo; o PostgreSQL, não
Nomesminúsculos, sem aspasas 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.

Uma função do SQLite que não existe lá
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

TarefaSQLitePostgreSQL
Programa de terminalsqlite3 loja.dbpsql loja
Listar tabelas / estrutura.tables / .schema t\dt / \d t
LimitarLIMIT 3 OFFSET 3igual, ou OFFSET 3 ROWS FETCH FIRST 3 ROWS ONLY
Nome com maiúsculatanto fazsem aspas vira minúsculo; com aspas vale letra por letra
LIKE sem ligar para maiúsculasLIKE (só letras sem acento)ILIKE
Numeração automáticaINTEGER PRIMARY KEYGENERATED … AS IDENTITY (ou SERIAL)
Juntar textos||, concat()||, concat()
Trocar NULLifnull, coalescecoalesce
Somar diasdate(d, '+7 days')d + 7
Dias entre datasjulianday(b) - julianday(a)b - a
Ano / formato BRstrftime('%Y', d) / strftime('%d/%m/%Y', d)extract(year FROM d) / to_char(d, 'DD/MM/YYYY')
Booleano0 / 1boolean: true / false
DinheiroREALNUMERIC(10,2)
UPSERTON CONFLICT … DO UPDATEigual, e também MERGE
Devolver o que mudouRETURNINGRETURNING
1 / 0NULLERRO: divisão por zero
Erro dentro de uma transaçãodesfaz só o comandointerrompe 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:

SELECT nome FROM clientes WHERE nome 'JOSÉ%';

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, createuser e createdb como o usuário postgres, e psql loja.
  • No psql, os comandos de barra (\dt, \d tabela, \x, \pset null, \i, \q) fazem o papel dos comandos de ponto do sqlite3.
  • Iguais ao SQLite: LIMIT/OFFSET, ||, concat(), ON CONFLICT, RETURNING, string_agg.
  • Diferentes: nomes sem aspas viram minúsculos; ILIKE; GENERATED AS IDENTITY; datas com d + 7, b - a, extract, to_char; boolean de verdade; MERGE.
  • O PostgreSQL é rigoroso: tipos não se misturam (operador não existe), textos não passam do tamanho, GROUP BY sem colunas soltas (salvo agrupando pela chave primária), divisão por zero é erro.
  • Um erro dentro de uma transação a interrompe inteira (loja=!>): digite ROLLBACK;.