PostgreSQL: Tipos de Dados do PostgreSQL: Guia Completo
Última atualização: 2026-08-26
Escolher o tipo de dado correto é a base do design de banco de dados — ele determina a eficiência de armazenamento, o desempenho da consulta e a precisão dos dados.
1. O Que Você Vai Aprender
- Tipos inteiros (SMALLINT / INTEGER / BIGINT)
- Sequências autoincrementais (SERIAL / BIGSERIAL / IDENTITY)
- Ponto flutuante e numéricos exatos (REAL / DOUBLE / DECIMAL / NUMERIC)
- Tipos de string (CHAR / VARCHAR / TEXT)
- Tipos de data/hora (DATE / TIME / TIMESTAMP / TIMESTAMPTZ / INTERVAL)
- Tipos especiais (UUID / INET / BOOLEAN / ENUM / JSONB)
2. História Real de um Desenvolvedor
(1) O Problema: Confusão Sobre Tipos de Dados
Bob enfrentou uma série de escolhas difíceis ao projetar as tabelas para um banco de dados de e-commerce:
- O ID do usuário deve ser INTEGER ou BIGINT? E se a contagem de usuários exceder 2,1 bilhões?
- Os preços dos produtos devem usar REAL ou DECIMAL? Dizem que ponto flutuante tem problemas de precisão.
- As descrições dos produtos devem usar VARCHAR(5000) ou TEXT?
- Os timestamps devem usar TIMESTAMP ou TIMESTAMPTZ? E os usuários globais?
- Os endereços IP devem ser armazenados como VARCHAR ou existe um tipo dedicado?
(2) A Solução: Seleção Precisa de Tipo
O PostgreSQL oferece um rico conjunto de tipos de dados dedicados; escolher precisamente economiza armazenamento enquanto garante precisão:
-- Escolhas corretas de tipo para um banco de dados de e-commerce
CREATE TABLE smart_products (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, -- ID preparado para o futuro
name VARCHAR(200) NOT NULL, -- string com limite
description TEXT, -- texto sem limite
price DECIMAL(10, 2) NOT NULL, -- dinheiro exato
weight_kg REAL, -- aproximado é aceitável
is_available BOOLEAN DEFAULT true, -- flag sim/não
created_at TIMESTAMPTZ DEFAULT NOW(), -- com fuso horário
source_ip INET, -- tipo de endereço IP
attributes JSONB DEFAULT '{}'::jsonb -- esquema flexível
);
(3) O Resultado
- DECIMAL garante zero perda de precisão para dinheiro (89,99 sempre permanece 89,99, nunca 89,9899999)
- TIMESTAMPTZ trata fusos horários automaticamente, então usuários globais veem seu próprio horário local
- INET suporta consultas de intervalo de IP, 10× mais eficiente que armazenar IPs como VARCHAR
- JSONB armazena atributos dinâmicos flexivelmente, evitando alterações frequentes de ALTER TABLE
3. Fluxo de Decisão de Tipo de Dado
graph TB
START[Que tipo de dado?] --> NUM{Numérico?}
NUM -->|Sim| INT{Precisa de precisão<br/>exata?}
INT -->|Sim, dinheiro/taxas| DEC[DECIMAL / NUMERIC]
INT -->|Não, medições| FLOAT[REAL / DOUBLE PRECISION]
NUM -->|Números inteiros| RANGE{Intervalo de valores?}
RANGE -->|-32768 a 32767| SMALL[SMALLINT]
RANGE -->|-2.1B a 2.1B| INT2[INTEGER]
RANGE -->|Maior| BIG[BIGINT]
NUM -->|ID autoincremental| AUTO[SERIAL / IDENTITY]
START --> STR{Texto?}
STR -->|Comprimento fixo| CHAR[CHAR]
STR -->|Variável limitado| VAR[VARCHAR n]
STR -->|Ilimitado| TEXT2[TEXT]
START --> TIME{Data/Hora?}
TIME -->|Apenas data| DATE2[DATE]
TIME -->|Apenas hora| TIME2[TIME]
TIME -->|Timestamp| TZ{Fuso horário?}
TZ -->|Sim, app global| TSTZ[TIMESTAMPTZ]
TZ -->|Não, apenas local| TS[TIMESTAMP]
TIME -->|Duração| IV[INTERVAL]
START --> SPEC{Especial?}
SPEC -->|Sim/Não| BOOL[BOOLEAN]
SPEC -->|UUID| UUID2[UUID]
SPEC -->|Endereço IP| IP[INET / CIDR]
SPEC -->|Lista enum| ENUM2[ENUM]
SPEC -->|Documento JSON| JSON[JSONB]
4. Tipos Inteiros
| Tipo | Armazenamento | Intervalo | Uso típico |
|---|---|---|---|
SMALLINT |
2 bytes | -32.768 ~ 32.767 | idade, avaliação, código de status |
INTEGER (INT) |
4 bytes | -2.147.483.648 ~ 2.147.483.647 | inteiros gerais, IDs de chave primária |
BIGINT |
8 bytes | ±9.223.372.036.854.775.807 | IDs de tabelas grandes, valores (em centavos) |
▶ Exemplo: Escolhendo Tipos Inteiros
-- SMALLINT para valores de intervalo pequeno
CREATE TABLE ratings (
user_id INTEGER REFERENCES users(id),
product_id INTEGER REFERENCES products(id),
score SMALLINT CHECK (score BETWEEN 1 AND 5), -- 1-5 está bem dentro do intervalo SMALLINT
PRIMARY KEY (user_id, product_id)
);
-- INTEGER para a maioria dos IDs
CREATE TABLE categories (
id SERIAL PRIMARY KEY, -- SERIAL = INTEGER + sequência autoincremental
name VARCHAR(100) NOT NULL
);
-- BIGINT para tabelas de alto crescimento
CREATE TABLE audit_log (
id BIGSERIAL PRIMARY KEY, -- BIGSERIAL = BIGINT + autoincremento
action VARCHAR(50),
details JSONB,
created_at TIMESTAMPTZ DEFAULT NOW()
);
Output:
CREATE TABLE
5. Sequências Autoincrementais
(1) SERIAL vs IDENTITY
| Método | Sintaxe | Padrão | Inserção manual? | Recomendado |
|---|---|---|---|---|
SERIAL |
id SERIAL PRIMARY KEY |
Específico do PG | Sim | Compatibilidade legada |
BIGSERIAL |
id BIGSERIAL PRIMARY KEY |
Específico do PG | Sim | Compatibilidade legada |
IDENTITY |
id INT GENERATED ALWAYS AS IDENTITY |
Padrão SQL | Precisa de OVERRIDING | ✅ Novos projetos |
▶ Exemplo: Colunas IDENTITY (Recomendado)
-- Coluna de identidade padrão SQL (PostgreSQL 10+)
CREATE TABLE orders_v2 (
id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
user_id INTEGER REFERENCES users(id),
total DECIMAL(12, 2) DEFAULT 0
);
-- Inserir sem especificar id (gerado automaticamente)
INSERT INTO orders_v2 (user_id, total) VALUES (1, 99.99);
-- Tentar inserir id manualmente (irá FALHAR com GENERATED ALWAYS)
-- INSERT INTO orders_v2 (id, user_id, total) VALUES (100, 1, 50.00);
-- Use GENERATED BY DEFAULT se precisar de substituição manual às vezes
CREATE TABLE logs (
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
message TEXT
);
Output:
INSERT 0 1
6. Ponto Flutuante e Numéricos Exatos
(1) REAL / DOUBLE vs DECIMAL
| Tipo | Armazenamento | Precisão | Melhor para |
|---|---|---|---|
REAL |
4 bytes | 6 dígitos significativos | computação científica, sensores |
DOUBLE PRECISION |
8 bytes | 15 dígitos significativos | computação científica, estatísticas |
DECIMAL(p,s) |
variável | exato | dinheiro, taxas (recomendado) |
NUMERIC(p,s) |
variável | exato | igual ao DECIMAL |
▶ Exemplo: Armadilhas de Precisão de Ponto Flutuante
-- Armadilha de precisão float: 0.1 + 0.2 != 0.3
SELECT 0.1::REAL + 0.2::REAL = 0.3::REAL AS float_equal;
-- Resultado: f (falso!)
-- DECIMAL não tem perda de precisão
SELECT 0.1::DECIMAL + 0.2::DECIMAL = 0.3::DECIMAL AS decimal_equal;
-- Resultado: t (verdadeiro!)
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
▶ Exemplo: Usando DECIMAL para Dinheiro
-- DECIMAL(precision, scale)
-- precision = total de dígitos, scale = dígitos após o ponto decimal
-- DECIMAL(10,2) = até 99.999.999,99
CREATE TABLE products_precise (
id SERIAL PRIMARY KEY,
name VARCHAR(200),
price DECIMAL(10, 2) NOT NULL, -- máx. 99.999.999,99
tax_rate DECIMAL(5, 4) DEFAULT 0.0875, -- 0,0875 = 8,75%
discount NUMERIC(5, 2) DEFAULT 0.00 -- máx. 999,99%
);
-- Cálculos de dinheiro são exatos
SELECT name,
price,
price * tax_rate AS tax_amount,
price + (price * tax_rate) AS price_with_tax
FROM products_precise;
Output:
count
-------
5
(1 row)
| Escolha de precisão | Parâmetros DECIMAL | Valor máximo | Melhor para |
|---|---|---|---|
| E-commerce pequeno | DECIMAL(8,2) |
999.999,99 | volume diário < 1M |
| E-commerce grande | DECIMAL(12,2) |
99.999.999.999,99 | e-commerce global |
| Crypto | DECIMAL(20,8) |
muito grande | BTC precisão de 8 decimais |
7. Tipos de String
| Tipo | Armazenamento | Comprimento máximo | Melhor para |
|---|---|---|---|
CHAR(n) |
comprimento fixo, preenchido com espaços | n | hashes, códigos ISO |
VARCHAR(n) |
comprimento variável | n | texto com limite de comprimento |
TEXT |
comprimento variável | ilimitado | texto sem limite de comprimento |
▶ Exemplo: Comparação de Tipos de String
-- CHAR: comprimento fixo (preenchido com espaços)
SELECT LENGTH('abc'::CHAR(5));
-- Resultado: 5 (preenchido para 5 caracteres)
-- VARCHAR: comprimento variável com limite
SELECT LENGTH('abc'::VARCHAR(5));
-- Resultado: 3 (armazenado como está, máx. 5)
-- TEXT: comprimento variável, sem limite
SELECT LENGTH('abc'::TEXT);
-- Resultado: 3 (sem limite)
-- Teste de desempenho: VARCHAR vs TEXT são idênticos no PG
-- (Diferente do MySQL onde VARCHAR é mais rápido que TEXT)
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
8. Tipos de Data e Hora
| Tipo | Armazenamento | Intervalo | Melhor para |
|---|---|---|---|
DATE |
4 bytes | 4713 AC ~ 5874897 DC | apenas data (aniversário, feriado) |
TIME |
8 bytes | 00:00:00 ~ 24:00:00 | apenas hora (horário comercial) |
TIMESTAMP |
8 bytes | 4713 AC ~ 294276 DC | data+hora (sem fuso horário) |
TIMESTAMPTZ |
8 bytes | igual ao acima | data+hora+fuso horário (✅ recomendado) |
INTERVAL |
16 bytes | ±178000000 anos | intervalo de tempo |
▶ Exemplo: TIMESTAMP vs TIMESTAMPTZ
-- Definir fuso horário para UTC
SET timezone = 'UTC';
-- Inserir o mesmo momento em tipos diferentes
INSERT INTO test_times (ts_no_tz, ts_with_tz) VALUES
('2026-07-13 10:00:00', '2026-07-13 10:00:00+00');
-- Alterar para fuso horário de Tóquio
SET timezone = 'Asia/Tokyo';
-- TIMESTAMP (sem fuso horário) mostra o mesmo literal
SELECT ts_no_tz FROM test_times;
-- Resultado: 2026-07-13 10:00:00 (inalterado, poderia ser qualquer fuso horário!)
-- TIMESTAMPTZ converte para o fuso horário local
SELECT ts_with_tz FROM test_times;
-- Resultado: 2026-07-13 19:00:00+09 (10:00 UTC = 19:00 Tóquio)
Output:
INSERT 0 1
▶ Exemplo: Funções de Data/Hora
-- Data e hora atuais
SELECT NOW(); -- 2026-07-13 10:30:00.123456+00
SELECT CURRENT_DATE; -- 2026-07-13
SELECT CURRENT_TIMESTAMP; -- igual a NOW()
-- Aritmética de data com INTERVAL
SELECT NOW() + INTERVAL '7 days'; -- uma semana a partir de agora
SELECT NOW() - INTERVAL '3 months'; -- 3 meses atrás
-- Cálculo de idade
SELECT AGE(TIMESTAMP '1990-05-15'); -- 36 anos 1 mês 28 dias
-- Extrair partes
SELECT EXTRACT(YEAR FROM NOW()); -- 2026
SELECT EXTRACT(MONTH FROM NOW()); -- 7
SELECT EXTRACT(DOW FROM NOW()); -- 1 (Segunda-feira, 0=Domingo)
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
9. Tipos Especiais
(1) UUID
-- Habilitar extensão uuid-ossp
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- Gerar diferentes versões de UUID
SELECT uuid_generate_v4(); -- UUID aleatório (mais comum)
SELECT uuid_generate_v1(); -- UUID baseado em tempo
| Versão UUID | Geração | Melhor para |
|---|---|---|
| v1 | timestamp + endereço MAC | ordenado por tempo |
| v4 | aleatório | maioria dos cenários (recomendado) |
| v7 | timestamp + aleatório (novo padrão) | ordenado por tempo + aleatório |
(2) INET / CIDR (Endereços de Rede)
▶ Exemplo: Armazenando e Consultando Endereços IP
-- INET: IP único ou intervalo de IP
CREATE TABLE access_log (
id BIGSERIAL PRIMARY KEY,
source_ip INET NOT NULL,
access_time TIMESTAMPTZ DEFAULT NOW()
);
INSERT INTO access_log (source_ip) VALUES
('192.168.1.100'),
('10.0.0.5'),
('2001:db8::1');
-- Consulta: encontrar todos os IPs em uma sub-rede (impossível com VARCHAR!)
SELECT source_ip FROM access_log
WHERE source_ip << '192.168.1.0/24'::INET;
-- << significa "está contido em"
-- CIDR: intervalo de rede
SELECT '192.168.1.0/24'::CIDR;
Output:
INSERT 0 1
| Operador | Significado | Exemplo |
|---|---|---|
<< |
contido em | 192.168.1.5' << '192.168.1.0/24' = true |
>> |
contém | '192.168.1.0/24' >> '192.168.1.5' = true |
= |
igual | '192.168.1.5'::INET = '192.168.1.5'::INET |
(3) BOOLEAN
▶ Exemplo: Três Representações de Booleano
-- Booleano aceita múltiplas representações
SELECT true, 't', 'true', 'yes', 'on', '1'; -- todos = TRUE
SELECT false, 'f', 'false', 'no', 'off', '0'; -- todos = FALSE
-- Booleano na cláusula WHERE
SELECT name FROM products WHERE is_available IS TRUE;
SELECT name FROM products WHERE is_available IS NOT FALSE;
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
(4) ENUM
▶ Exemplo: Tipos Enum Personalizados
-- Criar um tipo enum (uma vez, reutilizável entre tabelas)
CREATE TYPE order_status AS ENUM (
'pending', 'paid', 'shipped', 'delivered', 'cancelled'
);
-- Usar na definição da tabela
CREATE TABLE orders_enum (
id SERIAL PRIMARY KEY,
status order_status DEFAULT 'pending'
);
-- Valores enum são validados automaticamente
INSERT INTO orders_enum (status) VALUES ('pending'); -- OK
-- INSERT INTO orders_enum (status) VALUES ('unknown'); -- ERRO!
Output:
INSERT 0 1
ALTER TYPE ... ADD VALUE (permitido dentro de uma transação desde o PG 9.1). Se os valores mudam com frequência, um VARCHAR + restrição CHECK é mais flexível.
10. Tabela Comparativa de Tipos de Dados
| Necessidade | ❌ Não recomendado | ✅ Recomendado | Razão |
|---|---|---|---|
| Dinheiro | REAL / FLOAT | DECIMAL(p,s) | ponto flutuante perde precisão |
| Timestamp | TIMESTAMP | TIMESTAMPTZ | apps globais precisam de fusos horários |
| Endereço IP | VARCHAR | INET | INET suporta consultas de intervalo |
| Sim/Não | INTEGER (0/1) | BOOLEAN | semântica mais clara |
| Texto longo | VARCHAR(9999) | TEXT | TEXT sem limite e mesmo desempenho |
| Enum de status | VARCHAR + CHECK | ENUM ou VARCHAR+CHECK | ENUM se estável, CHECK se muda frequentemente |
| Atributos flexíveis | muitas colunas NULL | JSONB | uma coluna para campos dinâmicos |
| ID autoincremental | SERIAL | IDENTITY | sintaxe padrão SQL |
11. Exemplo Completo: Uma Tabela Usando Muitos Tipos
-- ============================================
-- Exemplo completo: tabela com todos os principais tipos
-- Um sistema de coleta de dados de sensores
-- ============================================
-- Criar tipo enum
CREATE TYPE sensor_status AS ENUM ('active', 'inactive', 'maintenance');
-- Criar tabela com diversos tipos de dados
CREATE TABLE sensors (
-- Identidade
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
uuid UUID DEFAULT uuid_generate_v4() UNIQUE,
-- Texto
name VARCHAR(100) NOT NULL,
description TEXT,
location_code CHAR(10),
-- Numérico
latitude DECIMAL(9, 6) CHECK (latitude BETWEEN -90 AND 90),
longitude DECIMAL(9, 6) CHECK (longitude BETWEEN -180 AND 180),
altitude REAL,
-- Rede
ip_address INET,
subnet CIDR,
-- Status
status sensor_status DEFAULT 'active',
is_online BOOLEAN DEFAULT false,
-- Tempo
installed_at DATE,
last_reading_at TIMESTAMPTZ,
reading_interval INTERVAL DEFAULT INTERVAL '5 minutes',
-- Dados flexíveis
metadata JSONB DEFAULT '{}'::jsonb,
-- Auditoria
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Inserir dados de exemplo
INSERT INTO sensors (name, latitude, longitude, ip_address, status, installed_at, metadata)
VALUES (
'Sensor de Temperatura A1',
35.6762, 139.6503,
'192.168.1.50',
'active',
'2025-01-15',
'{"model": "TX-200", "unit": "celsius", "range_min": -40, "range_max": 85}'::jsonb
);
-- Consulta com operadores específicos de tipo
SELECT name, ip_address, metadata->>'model' AS model
FROM sensors
WHERE ip_address << '192.168.1.0/24'::INET
AND status = 'active';
❓ Perguntas Frequentes
P: Qual é a diferença entre DECIMAL e NUMERIC? R: No PostgreSQL, DECIMAL e NUMERIC são completamente equivalentes e intercambiáveis. O padrão SQL faz uma distinção sutil, mas a implementação do PG é idêntica. DECIMAL é recomendado (mais intuitivo).
P: E se a sequência de uma coluna SERIAL pular números? R: SERIAL usa um objeto de sequência; após um INSERT com falha ou revertido, o valor da sequência não é revertido (isso é por design, para garantir segurança de concorrência). Lacunas são normais e não afetam a funcionalidade. Se você precisa de valores consecutivos, trate na camada de aplicação em vez de depender da sequência.
P: Alterar VARCHAR(50) para VARCHAR(100) bloqueia a tabela? R: No PostgreSQL, aumentar o comprimento de VARCHAR não bloqueia a tabela (sem reescrita de dados) e é concluído instantaneamente. Reduzir o comprimento ou alterar o tipo é o que bloqueia a tabela.
P: Qual devo escolher, JSON ou JSONB? R: Quase sempre JSONB. JSONB é armazenado como binário e suporta consultas indexadas, então é rápido. JSON é armazenado como texto e deve ser re-analisado em cada consulta. O único caso para JSON: quando você precisa preservar os espaços em branco/ordem de chaves da entrada (ex., logs de auditoria).
P: Por que TIMESTAMPTZ é recomendado sobre TIMESTAMP? R: TIMESTAMPTZ converte para UTC no armazenamento e de volta para o fuso horário do cliente na leitura. Isso significa que usuários globais veem seu próprio horário local sem confusão. TIMESTAMP armazena e retorna exatamente o que você fornece, o que é ambíguo para aplicações com múltiplos fusos horários.
P: Misturar múltiplos tipos de dados em uma tabela prejudica o desempenho? R: Não. O PG armazena informações de tipo por coluna e só lê as colunas que precisa. Usar tipos dedicados apropriadamente (ex., INET para IPs) na verdade melhora o desempenho da consulta, porque os operadores do tipo podem usar índices.
📖 Resumo
- Escolha de inteiro: SMALLINT (intervalo pequeno) / INTEGER (padrão) / BIGINT (IDs de tabelas grandes); prefira IDENTITY para autoincremento
- Dinheiro deve usar DECIMAL; tipos de ponto flutuante perdem precisão
- Strings: VARCHAR (comprimento limitado) / TEXT (ilimitado), com desempenho idêntico
- Tempo: TIMESTAMPTZ (com fuso horário) é recomendado; INTERVAL é para aritmética de tempo
- Tipos especiais: UUID (IDs distribuídos) / INET (endereços IP) / BOOLEAN (sim/não) / ENUM (enumerações fixas) / JSONB (atributos flexíveis)
- Princípio central: escolha o tipo mais preciso; na dúvida, erre para o lado maior (BIGINT custa 4 bytes extras por linha, mas reescrever uma tabela grande via ALTER TABLE custa muito mais)
📝 Exercícios
-
Básico (★): Crie uma tabela
countriescom: id (SERIAL chave primária), name (VARCHAR(100)), iso_code (CHAR(2)), population (BIGINT), gdp DECIMAL(15,2), is_developed (BOOLEAN). Insira 3 linhas de teste. -
Intermediário (★★): Crie uma tabela
server_logscom: id (BIGINT IDENTITY chave primária), source_ip (INET), request_time (TIMESTAMPTZ), response_time_ms (INTEGER), is_error (BOOLEAN), metadata (JSONB). Insira 2 registros de log e depois consulte todos os logs da rede10.0.0.0/8. -
Desafio (★★★): Projete uma tabela
financial_transactionsque lida com múltiplas moedas (USD/EUR/JPY/CNY) com diferentes precisões de valor (USD/EUR: 2 decimais, JPY: 0 decimais, CNY: 2 decimais). Implemente-a usando DECIMAL + restrições CHECK para que diferentes moedas exijam diferentes escalas decimais. Escreva o SQL CREATE TABLE mais 3 exemplos de INSERT para diferentes moedas.