O MySQL é o banco de dados mais comum no ecossistema PHP, mas o PostgreSQL é o mais poderoso. Enquanto o MySQL foi por muito tempo otimizado para leitura rápida e simplicidade, o PostgreSQL nasceu com foco em conformidade com o padrão SQL, integridade de dados e recursos avançados que outros bancos levaram anos para implementar — ou nunca implementaram.
Neste artigo vamos conectar o PHP ao PostgreSQL usando PDO, explorar os tipos de dados nativos do Postgres que não existem no MySQL, trabalhar com JSONB para dados semiestruturados e entender quando faz sentido escolher o PostgreSQL em vez do MySQL.
Por que PostgreSQL?
O PostgreSQL, frequentemente chamado apenas de "Postgres", é um sistema de banco de dados relacional de código aberto com mais de 35 anos de desenvolvimento ativo. Algumas vantagens concretas sobre o MySQL:
Tipos de dados ricos. O Postgres suporta arrays nativos, tipos geométricos (ponto, linha, polígono), ranges (intervalo de datas, de inteiros), tipos de rede (inet, cidr), e o JSONB — uma versão indexável e eficiente do JSON. O MySQL tem JSON desde 2015, mas a implementação do Postgres é significativamente mais poderosa.
Conformidade com SQL. O Postgres implementa o padrão SQL de forma muito mais completa, incluindo Common Table Expressions (CTEs) recursivos, window functions avançadas, e suporte a transações DDL (você pode fazer rollback de um CREATE TABLE, por exemplo).
Extensibilidade. O Postgres permite criar seus próprios tipos de dados, operadores e funções. Extensions como PostGIS (para dados geográficos), pgvector (para embeddings de IA), e TimescaleDB (para séries temporais) transformam o Postgres em um banco especializado quando necessário.
Confiabilidade. O Postgres tem reputação sólida de nunca perder dados. Seu sistema MVCC (Multi-Version Concurrency Control) é robusto e bem testado.
Instalação e configuração
No Linux (Ubuntu/Debian):
sudo apt install postgresql postgresql-contrib php-pgsql
sudo systemctl start postgresql
Criar banco e usuário:
sudo -u postgres psql
-- Dentro do psql:
CREATE DATABASE loja;
CREATE USER app_user WITH PASSWORD 'senha123';
GRANT ALL PRIVILEGES ON DATABASE loja TO app_user;
\q
Verificar conexão:
psql -h localhost -U app_user -d loja
Conectando com PDO
A beleza do PDO é que a mudança de MySQL para PostgreSQL exige apenas trocar o DSN. O resto do código permanece idêntico:
<?php
declare(strict_types=1);
// MySQL seria: mysql:host=localhost;dbname=loja;charset=utf8mb4
$dsn = 'pgsql:host=localhost;port=5432;dbname=loja';
try {
$pdo = new PDO(
dsn: $dsn,
username: 'app_user',
password: 'senha123',
options: [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]
);
echo "Conectado ao PostgreSQL com sucesso!\n";
} catch (PDOException $e) {
throw new RuntimeException(
"Falha na conexão: " . $e->getMessage()
);
}
Note o PDO::ATTR_EMULATE_PREPARES => false. No PostgreSQL, isso é especialmente importante: garante que o PDO use prepared statements reais do servidor, o que melhora performance e segurança. O Postgres tem um cache interno de planos de execução para prepared statements, e desativar a emulação permite que esse cache seja utilizado.
Criando o schema
<?php
$pdo->exec("
CREATE TABLE IF NOT EXISTS produtos (
id SERIAL PRIMARY KEY,
nome VARCHAR(200) NOT NULL,
preco NUMERIC(10, 2) NOT NULL CHECK (preco >= 0),
estoque INTEGER NOT NULL DEFAULT 0,
ativo BOOLEAN NOT NULL DEFAULT TRUE,
tags TEXT[],
metadata JSONB,
criado_em TIMESTAMPTZ NOT NULL DEFAULT NOW()
)
");
Observe alguns tipos específicos do PostgreSQL: - SERIAL: auto-incremento (equivalente a AUTO_INCREMENT do MySQL). No Postgres moderno, prefere-se GENERATED ALWAYS AS IDENTITY, mas SERIAL ainda é amplamente usado. - NUMERIC(10, 2): tipo exato para dinheiro, sem erros de ponto flutuante. - BOOLEAN: tipo nativo no Postgres (no MySQL é TINYINT(1)). - TEXT[]: array de strings. - JSONB: JSON armazenado em formato binário, indexável. - TIMESTAMPTZ: timestamp com timezone (melhor prática para aplicações multi-fuso).
Arrays nativos
<?php
// Inserindo um produto com array de tags
$stmt = $pdo->prepare("
INSERT INTO produtos (nome, preco, estoque, tags)
VALUES (:nome, :preco, :estoque, :tags)
RETURNING id, criado_em
");
// Arrays são passados como strings no formato do Postgres
$stmt->execute([
':nome' => 'Teclado Mecânico',
':preco' => '299.90',
':estoque' => 15,
':tags' => '{periférico,teclado,mecânico}',
]);
$resultado = $stmt->fetch();
echo "Produto criado com ID: {$resultado['id']}\n";
echo "Criado em: {$resultado['criado_em']}\n";
// Note o RETURNING — permite retornar dados da linha inserida
// sem fazer um SELECT separado. O MySQL não tem isso.
// Consultando e decodificando o array
$stmt = $pdo->prepare("SELECT nome, tags FROM produtos WHERE id = :id");
$stmt->execute([':id' => $resultado['id']]);
$produto = $stmt->fetch();
// O PDO retorna o array como string: {periférico,teclado,mecânico}
// Precisamos fazer o parse manualmente
$tags = str_getcsv(trim($produto['tags'], '{}'));
print_r($tags); // ['periférico', 'teclado', 'mecânico']
// Buscar produtos que contenham uma tag específica
$stmt = $pdo->prepare("
SELECT nome, tags
FROM produtos
WHERE :tag = ANY(tags)
");
$stmt->execute([':tag' => 'mecânico']);
JSONB — JSON indexável
O JSONB é um dos recursos mais poderosos do PostgreSQL. Diferente do tipo JSON simples (que armazena o texto original), o JSONB armazena em formato binário parseado, permitindo indexação e consultas eficientes:
<?php
// Inserindo com JSONB
$metadata = json_encode([
'fabricante' => 'Keychron',
'switch' => 'Brown',
'bluetooth' => true,
'dimensoes' => ['largura' => 36, 'altura' => 13, 'profundidade' => 12],
'cores' => ['preto', 'cinza'],
]);
$stmt = $pdo->prepare("
UPDATE produtos
SET metadata = :metadata::jsonb
WHERE id = :id
");
$stmt->execute([':metadata' => $metadata, ':id' => 1]);
// Consultando campos específicos do JSONB com operador ->
$stmt = $pdo->query("
SELECT
nome,
metadata->>'fabricante' AS fabricante,
metadata->>'switch' AS switch,
(metadata->>'bluetooth')::bool AS bluetooth,
metadata->'dimensoes'->>'largura' AS largura
FROM produtos
WHERE metadata->>'fabricante' = 'Keychron'
AND (metadata->>'bluetooth')::bool = true
");
while ($row = $stmt->fetch()) {
echo "{$row['nome']} — {$row['fabricante']}, switch {$row['switch']}\n";
}
// Criando índice GIN para consultas JSONB eficientes
$pdo->exec("CREATE INDEX idx_produtos_metadata ON produtos USING gin(metadata)");
// Com o índice, consultas usando @> (contém) são muito eficientes:
$stmt = $pdo->prepare("
SELECT nome FROM produtos
WHERE metadata @> :filtro::jsonb
");
$stmt->execute([':filtro' => json_encode(['bluetooth' => true])]);
Transações e recursos avançados
<?php
// CTEs (Common Table Expressions) — disponíveis no MySQL 8+
// mas muito mais comuns e idiomáticas no PostgreSQL
$stmt = $pdo->query("
WITH
produtos_ativos AS (
SELECT id, nome, preco, estoque
FROM produtos
WHERE ativo = true AND estoque > 0
),
estatisticas AS (
SELECT
COUNT(*) AS total,
AVG(preco) AS preco_medio,
SUM(estoque) AS estoque_total
FROM produtos_ativos
)
SELECT p.nome, p.preco, e.preco_medio
FROM produtos_ativos p, estatisticas e
ORDER BY p.preco DESC
LIMIT 10
");
// UPSERT nativo com ON CONFLICT
$stmt = $pdo->prepare("
INSERT INTO produtos (nome, preco, estoque)
VALUES (:nome, :preco, :estoque)
ON CONFLICT (nome)
DO UPDATE SET
preco = EXCLUDED.preco,
estoque = EXCLUDED.estoque,
ativo = true
RETURNING id, (xmax = 0) AS inserido
");
// xmax = 0 indica que foi um INSERT; xmax != 0 indica UPDATE
Quando usar PostgreSQL vs MySQL
| Critério | PostgreSQL | MySQL |
|---|---|---|
| Consultas complexas | ✅ Excelente | ✅ Bom |
| Dados geográficos (PostGIS) | ✅ Referência | ❌ Limitado |
| JSONB/documentos híbridos | ✅ Excelente | ⚠️ Funcional |
| Performance de leitura simples | ✅ Ótimo | ✅ Ótimo |
| Ecossistema WordPress/legado | ⚠️ Menor | ✅ Dominante |
| Hospedagem compartilhada | ⚠️ Menos disponível | ✅ Ubíquo |
| Conformidade SQL | ✅ Excelente | ⚠️ Parcial |
Exercício da semana
-
Instale o PostgreSQL localmente e conecte via PDO. Crie um banco
bibliotecacom tabelalivros(id, titulo, autores TEXT[], generos TEXT[], metadata JSONB, publicado_em DATE). -
Insira 5 livros com arrays de autores e metadados JSON (editora, ISBN, numero_paginas). Use
RETURNINGpara capturar oidgerado. -
Escreva uma query que use
ANY()para buscar livros de um determinado gênero. Depois, use@>para buscar pelo JSONB (ex: todos da editora "Companhia das Letras"). -
Implemente um UPSERT: se o livro (pelo ISBN no JSONB) já existir, atualize o preço; caso contrário, insira.
-
Desafio: crie um índice GIN no campo
generos(array) e no campometadata(JSONB). UseEXPLAIN ANALYZEpara comparar o custo das queries com e sem índice.
Referências
- Documentação oficial do PostgreSQL: https://www.postgresql.org/docs/
- PHP Manual — PDO_PGSQL: https://www.php.net/manual/pt_BR/ref.pdo-pgsql.php
- PostgreSQL vs MySQL — comparação técnica detalhada: https://www.postgresql.org/about/featurematrix/
- JSONB no PostgreSQL — guia completo: https://www.postgresql.org/docs/current/datatype-json.html