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


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:

(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:

SQL
-- 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


3. Fluxo de Decisão de Tipo de Dado

100%
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

SQL
-- 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:

TEXT 📖 Somente leitura
CREATE TABLE
💡 Tip: Não tem certeza se usa INTEGER ou BIGINT? Para IDs de chave primária, se você espera mais de 1 bilhão de linhas, vá direto para BIGINT. Custa apenas 4 bytes extras por linha (apenas 4 MB por milhão de linhas), mas poupa uma custosa reescrita de tabela grande via ALTER TABLE depois.


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)

SQL
-- 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:

TEXT 📖 Somente leitura
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

SQL
-- 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:

TEXT 📖 Somente leitura
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

▶ Exemplo: Usando DECIMAL para Dinheiro

SQL
-- 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:

TEXT 📖 Somente leitura
 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

SQL
-- 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:

TEXT 📖 Somente leitura
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)
💡 Tip: No PostgreSQL, VARCHAR e TEXT têm desempenho de consulta e armazenamento idênticos. O PG não fica mais lento só porque TEXT não tem limite de comprimento. A única razão para escolher um sobre o outro é se você precisa que o banco de dados imponha um limite de comprimento.


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

SQL
-- 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:

TEXT 📖 Somente leitura
INSERT 0 1

▶ Exemplo: Funções de Data/Hora

SQL
-- 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:

TEXT 📖 Somente leitura
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

9. Tipos Especiais

(1) UUID

SQL
-- 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

SQL
-- 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:

TEXT 📖 Somente leitura
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

SQL
-- 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:

TEXT 📖 Somente leitura
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

(4) ENUM

▶ Exemplo: Tipos Enum Personalizados

SQL
-- 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:

TEXT 📖 Somente leitura
INSERT 0 1
⚠️ Note: Uma vez que um tipo ENUM do PG é criado, adicionar novos valores requer 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

SQL
-- ============================================
-- 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


📝 Exercícios

  1. Básico (★): Crie uma tabela countries com: 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.

  2. Intermediário (★★): Crie uma tabela server_logs com: 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 rede 10.0.0.0/8.

  3. Desafio (★★★): Projete uma tabela financial_transactions que 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.

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%