Referência Completa de Tipos de Dados SQL

Manual de Todos os Campos do Banco de Dados

Explore cada tipo de campo (VARCHAR, CHAR, INT, DECIMAL, TIMESTAMP, JSON, ENUM...) agrupado por funcionalidade, como configurar no DrawDB e como validar e gravar com PHP 8 + PDO.

✕

4. Textos Curtos & Variáveis 2 tipos

Campos de texto de tamanho fixo (CHAR) ou variável (VARCHAR) para nomes, e-mails, CPFs, telefones, siglas e hashes.

VARCHAR(n) (Texto de Tamanho Variável)

⭐ Essencial 💾 Tamanho real da string em bytes + 1 byte (até 255) ou + 2 bytes (> 255)

O campo de texto mais usado do banco de dados: armazena apenas os caracteres digitados + 1 ou 2 bytes de tamanho.

Diferente do CHAR, o VARCHAR(n) ocupa apenas o espaço real do texto armazenado mais 1 byte de controle (se n <= 255) ou 2 bytes (se n > 255). O número `n` define a quantidade MÁXIMA de caracteres permitidos (não de bytes!). Em `utf8mb4`, cada caractere pode usar de 1 a 4 bytes. Mesmo que o espaço em disco seja dinâmico, definir um limite `n` realista (ex: `VARCHAR(120)` em vez de `VARCHAR(255)` em tudo) economiza memória RAM, pois o MySQL aloca buffers em memória com base no tamanho máximo declarado durante ordenações (`ORDER BY`) e tabelas temporárias!

Limites de Capacidade: De 1 até 16.383 caracteres em utf8mb4 (limite de 65.535 bytes por linha no MySQL)

✅ Quando Usar (Casos Ideais)

  • • Nomes de pessoas, razões sociais, títulos de produtos (`VARCHAR(100)` a `VARCHAR(180)`).
  • • Endereços de e-mail (`VARCHAR(150)` ou `VARCHAR(254)` conforme RFC).
  • • Senhas criptografadas com `password_hash()` do PHP (Bcrypt/Argon2id) -> use `VARCHAR(255)`.
  • • Telefones com DDI/DDD (`VARCHAR(20)`), URLs (`VARCHAR(500)`), Slugs (`VARCHAR(160)`).

🚫 Quando NÃO Usar (Armadilhas)

  • • Códigos cujo tamanho é SEMPRE idêntico em 100% dos registros, como UF (`SP`, `RJ` -> 2 caracteres) ou CPF sem máscara (`11` dígitos) — nesses casos, prefira `CHAR`.
  • • Textos imensos sem limite previsível como corpo de matérias ou artigos completos — use `TEXT`.

📐 Como Configurar no DrawDB Abrir DrawDB ↗

Type no DrawDB: VARCHAR
Size / Values: Ex: 100 (nome), 150 (email), 255 (senha_hash), 20 (telefone)
Default sugerido: Vazio ou string padrão
Equivalência nos Dialetos do DrawDB:
MySQL / MariaDB VARCHAR(150)
PostgreSQL VARCHAR(150)
SQLite TEXT (SQLite trata VARCHAR como afinidade TEXT)

📋 Exemplos Reais de Colunas com VARCHAR:

nome_completo
VARCHAR(120) NOT NULL
Nome do cliente ou usuário
email
VARCHAR(150) NOT NULL UNIQUE
E-mail de login único
senha_hash
VARCHAR(255) NOT NULL
Hash gerado por password_hash() no PHP
telefone
VARCHAR(20) NULL
Telefone celular / WhatsApp (ex: +5511999998888)
🗄️ SQL DDL (Importar no DrawDB: Import > SQL)
CREATE TABLE clientes (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    nome_completo VARCHAR(120) NOT NULL,
    email VARCHAR(150) NOT NULL UNIQUE,
    senha_hash VARCHAR(255) NOT NULL,
    telefone VARCHAR(20) NULL,
    cidade VARCHAR(80) NOT NULL
);
🐘 Como Validar e Salvar no PHP 8 + PDO
<?php
declare(strict_types=1);

$nome  = trim($_POST['nome'] ?? '');
$email = trim($_POST['email'] ?? '');
$senha = $_POST['senha'] ?? '';

// 1. Validação do comprimento em caracteres UTF-8 com mb_strlen()
if ($nome === '' || mb_strlen($nome, 'UTF-8') > 120) {
    throw new InvalidArgumentException('O nome deve ter entre 1 e 120 caracteres.');
}

// 2. Validação nativa de e-mail e limite do VARCHAR(150)
if (!filter_var($email, FILTER_VALIDATE_EMAIL) || mb_strlen($email, 'UTF-8') > 150) {
    throw new InvalidArgumentException('E-mail inválido.');
}

// 3. Hash seguro de senha no PHP (sempre cabe em VARCHAR(255))
$senhaHash = password_hash($senha, PASSWORD_DEFAULT);

$stmt = $pdo->prepare('
    INSERT INTO clientes (nome_completo, email, senha_hash)
    VALUES (:nome, :email, :senha_hash)
');
$stmt->bindValue(':nome', $nome, PDO::PARAM_STR);
$stmt->bindValue(':email', mb_strtolower($email, 'UTF-8'), PDO::PARAM_STR);
$stmt->bindValue(':senha_hash', $senhaHash, PDO::PARAM_STR);
$stmt->execute();

CHAR(n) (Texto de Tamanho Fixo)

⭐ Essencial 💾 Fixo: n caracteres (em ASCII/latin1 = n bytes; em utf8mb4 = n até 4*n bytes)

Texto de comprimento EXATO e fixo para siglas (UF), CPF, CNPJ, CEP, códigos ISO e hashes.

O tipo CHAR(n) reserva espaço fixo para exatamente `n` caracteres. Não gasta byte extra para gravar o comprimento da string e oferece leitura e indexação ligeiramente mais rápidas quando todos os valores possuem sempre a mesma quantidade de caracteres.

Limites de Capacidade: De 0 até 255 caracteres

✅ Quando Usar (Casos Ideais)

  • • Sigla de Estado / UF (`CHAR(2)` -> `SP`, `RJ`, `MG`).
  • • Código ISO de País ou Moeda (`CHAR(2)` -> `BR`, `US` ou `CHAR(3)` -> `BRL`, `USD`).
  • • CPF sem pontuação (`CHAR(11)`), CNPJ sem pontuação (`CHAR(14)`), CEP sem traço (`CHAR(8)`).
  • • Hashes hexadecimais de tamanho fixo: SHA-256 (`CHAR(64)`), MD5 (`CHAR(32)`), UUID (`CHAR(36)`).

🚫 Quando NÃO Usar (Armadilhas)

  • • Nomes de pessoas, endereços ou textos cujo tamanho varia bastante (desperdiça espaço — use `VARCHAR`).

📐 Como Configurar no DrawDB Abrir DrawDB ↗

Type no DrawDB: CHAR
Size / Values: Ex: 2 (UF), 8 (CEP), 11 (CPF), 14 (CNPJ), 64 (SHA-256)
Default sugerido: Ex: BR (para país) ou BRL (para moeda)
Equivalência nos Dialetos do DrawDB:
MySQL / MariaDB CHAR(11)
PostgreSQL CHAR(11)
SQLite TEXT

📋 Exemplos Reais de Colunas com CHAR:

cpf
CHAR(11) NOT NULL UNIQUE
CPF apenas dígitos (ex: 01234567890)
uf
CHAR(2) NOT NULL
Sigla do estado (ex: SP)
cep
CHAR(8) NOT NULL
CEP apenas dígitos (ex: 01310100)
moeda
CHAR(3) NOT NULL DEFAULT 'BRL'
Código ISO 4217 da moeda
🗄️ SQL DDL (Importar no DrawDB: Import > SQL)
CREATE TABLE enderecos_faturamento (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    cliente_id BIGINT UNSIGNED NOT NULL,
    cpf CHAR(11) NOT NULL UNIQUE,
    cep CHAR(8) NOT NULL,
    uf CHAR(2) NOT NULL,
    pais CHAR(2) NOT NULL DEFAULT 'BR'
);
🐘 Como Validar e Salvar no PHP 8 + PDO
<?php
declare(strict_types=1);

// Higienizando CPF e CEP no PHP (removendo pontos e traços antes de salvar em CHAR)
$cpfBruto = '123.456.789-01';
$cepBruto = '01310-100';
$ufBruta  = 'sp';

$cpf = preg_replace('/\D/', '', $cpfBruto); // "12345678901" (11 dígitos)
$cep = preg_replace('/\D/', '', $cepBruto); // "01310100" (8 dígitos)
$uf  = strtoupper(trim($ufBruta));          // "SP" (2 letras)

if (strlen($cpf) !== 11 || strlen($cep) !== 8 || strlen($uf) !== 2) {
    throw new InvalidArgumentException('Dados com tamanho incorreto para os campos CHAR.');
}

$stmt = $pdo->prepare('
    INSERT INTO enderecos_faturamento (cliente_id, cpf, cep, uf)
    VALUES (:cliente_id, :cpf, :cep, :uf)
');
$stmt->bindValue(':cliente_id', 1, PDO::PARAM_INT);
$stmt->bindValue(':cpf', $cpf, PDO::PARAM_STR);
$stmt->bindValue(':cep', $cep, PDO::PARAM_STR);
$stmt->bindValue(':uf', $uf, PDO::PARAM_STR);
$stmt->execute();

5. Textos Longos & Conteúdo 1 tipo

Campos dimensionados para grandes volumes de texto como artigos de blog, descrições de produtos, HTML, Markdown e logs.

TEXT / MEDIUMTEXT / LONGTEXT

⭐ Essencial 💾 TEXT: até 64 KB | MEDIUMTEXT: até 16 MB | LONGTEXT: até 4 GB

Campos para textos extensos como descrições detalhadas, artigos de blog, contratos e logs.

Os tipos da família TEXT são armazenados fora da estrutura principal da linha no banco de dados (off-page storage no InnoDB), deixando na linha apenas um ponteiro de 20 bytes. Isso permite guardar textos imensos sem estourar o limite de 64 KB por linha das tabelas relacionais.

Limites de Capacidade: TINYTEXT: 255 B | TEXT: 65.535 B (~64 KB) | MEDIUMTEXT: 16,7 MB | LONGTEXT: 4,2 GB

✅ Quando Usar (Casos Ideais)

  • • `TEXT` (até 64 KB): Descrições de produtos, biografias, comentários de usuários, observações de pedidos.
  • • `MEDIUMTEXT` (até 16 MB): Artigos completos de blog em HTML/Markdown, termos de uso, templates de e-mail.
  • • `LONGTEXT` (até 4 GB): Livros inteiros, dumps extensos ou payloads massivos de integrações.

🚫 Quando NÃO Usar (Armadilhas)

  • • Não use `TEXT` para campos curtos que serão frequentemente filtrados com `=` ou `ORDER BY` (como nome, e-mail, cidade) — use `VARCHAR`.
  • • No MySQL, colunas `TEXT` não aceitam valor `DEFAULT` literal direto em versões antigas e exigem prefixo ao criar índices normais (ou índice `FULLTEXT`).

📐 Como Configurar no DrawDB Abrir DrawDB ↗

Type no DrawDB: TEXT (ou MEDIUMTEXT / LONGTEXT)
Size / Values: Vazio
Default sugerido: Deixe vazio (ou NULL se o campo for opcional)
Equivalência nos Dialetos do DrawDB:
MySQL / MariaDB TEXT, MEDIUMTEXT ou LONGTEXT
PostgreSQL TEXT (suporta até 1 GB automaticamente sem precisar de MEDIUM/LONG)
SQLite TEXT

📋 Exemplos Reais de Colunas com TEXT:

descricao_completa
TEXT NULL
Ficha técnica e descrição de um produto
conteudo_markdown
MEDIUMTEXT NOT NULL
Corpo completo de um artigo ou aula
🗄️ SQL DDL (Importar no DrawDB: Import > SQL)
CREATE TABLE artigos_blog (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    titulo VARCHAR(180) NOT NULL,
    slug VARCHAR(200) NOT NULL UNIQUE,
    resumo VARCHAR(300) NOT NULL,
    conteudo_html MEDIUMTEXT NOT NULL,
    FULLTEXT INDEX idx_busca_artigo (titulo, resumo, conteudo_html)
);
🐘 Como Validar e Salvar no PHP 8 + PDO
<?php
declare(strict_types=1);

$titulo   = 'Guia Definitivo de Tipos de Dados no MySQL e PostgreSQL';
$slug     = 'guia-definitivo-tipos-dados-mysql-postgresql';
$resumo   = 'Aprenda quando usar VARCHAR, CHAR, TEXT, INT e DECIMAL com exemplos em PHP.';
$conteudo = "<p>Conteúdo longo do artigo...</p>";

$stmt = $pdo->prepare('
    INSERT INTO artigos_blog (titulo, slug, resumo, conteudo_html)
    VALUES (:titulo, :slug, :resumo, :conteudo)
');
$stmt->bindValue(':titulo', $titulo, PDO::PARAM_STR);
$stmt->bindValue(':slug', $slug, PDO::PARAM_STR);
$stmt->bindValue(':resumo', $resumo, PDO::PARAM_STR);
$stmt->bindValue(':conteudo', $conteudo, PDO::PARAM_STR);
$stmt->execute();

7. Estruturas Flexíveis & JSON 1 tipo

Armazenamento de objetos, arrays e metadados dinâmicos validados diretamente pelo banco de dados com suporte a buscas internas.

JSON / JSONB (Documentos Estruturados)

⭐ Essencial 💾 Aproximadamente o tamanho de um LONGBLOB/LONGTEXT (até 1 GB no Postgres / 4 GB no MySQL)

Armazena objetos e arrays JSON validados automaticamente e consultáveis via SQL.

O tipo nativo `JSON` (MySQL 5.7.8+ / MariaDB) e `JSONB` (PostgreSQL) valida automaticamente se a string enviada é um JSON válido e a armazena em um formato binário otimizado que permite buscar chaves específicas (`JSON_EXTRACT` ou operador `->`) sem precisar ler e fazer parse do texto inteiro.

Limites de Capacidade: Documentos JSON válidos (objetos `{}` ou arrays `[]`)

✅ Quando Usar (Casos Ideais)

  • • Atributos dinâmicos de produtos de categorias muito diferentes (ex: ficha técnica de uma Camiseta vs Placa de Vídeo).
  • • Preferências e configurações de interface do usuário (tema escuro, notificações, idioma).
  • • Payloads brutos de Webhooks (Stripe, Mercado Pago, Asaas) e snapshots de auditoria (antes/depois da edição).

🚫 Quando NÃO Usar (Armadilhas)

  • • NÃO use JSON como muleta para evitar criar tabelas e relacionamentos normais! Nunca guarde chaves estrangeiras dentro de um array JSON (`"produtos_ids": [1, 5, 9]`) — crie uma tabela associativa!

📐 Como Configurar no DrawDB Abrir DrawDB ↗

Type no DrawDB: JSON (ou JSONB no PostgreSQL)
Size / Values: Vazio
Default sugerido: Deixe vazio ou NULL
Equivalência nos Dialetos do DrawDB:
MySQL / MariaDB JSON
PostgreSQL JSONB (Binário indexável recomendado) ou JSON
SQLite TEXT (com funções da extensão JSON1 nativas)

📋 Exemplos Reais de Colunas com JSON:

especificacoes_tecnicas
JSON NULL
Atributos variáveis do produto (voltagem, memória, cor)
configuracoes
JSON NOT NULL
Preferências customizadas do usuário
🗄️ SQL DDL (Importar no DrawDB: Import > SQL)
CREATE TABLE produtos_catalogo (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    nome VARCHAR(150) NOT NULL,
    preco DECIMAL(10, 2) NOT NULL,
    atributos_extras JSON NULL COMMENT 'Ex: {"voltagem": "220v", "garantia_meses": 12}'
);
🐘 Como Validar e Salvar no PHP 8 + PDO
<?php
declare(strict_types=1);

// 1. Codificando array associativo do PHP para JSON com tratamento de erro nativo (JSON_THROW_ON_ERROR)
$atributos = [
    'processador' => 'Ryzen 7',
    'memoria_ram_gb' => 32,
    'conexoes' => ['HDMI 2.1', 'USB-C', 'Wi-Fi 6E'],
];

$jsonString = json_encode($atributos, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);

$stmt = $pdo->prepare('
    INSERT INTO produtos_catalogo (nome, preco, atributos_extras)
    VALUES (:nome, :preco, :atributos)
');
$stmt->bindValue(':nome', 'Notebook Pro 16', PDO::PARAM_STR);
$stmt->bindValue(':preco', '7499.90', PDO::PARAM_STR);
$stmt->bindValue(':atributos', $jsonString, PDO::PARAM_STR);
$stmt->execute();

// 2. Lendo do banco e decodificando de volta para Array PHP
$dadosAtributos = json_decode($jsonString, true, 512, JSON_THROW_ON_ERROR);
echo $dadosAtributos['processador']; // "Ryzen 7"

8. Opções & Conjuntos (Listas) 1 tipo

Restrição de valores a uma lista fechada pré-definida de opções (como status de pedido ou nível de acesso).

ENUM (Lista Fechada de Opções)

⭐ Essencial 💾 1 Byte (até 255 opções) ou 2 Bytes (até 65.535 opções)

Permite que a coluna assuma apenas UM valor dentre uma lista pré-definida de opções.

Internamente no MySQL, o ENUM armazena o texto das opções no metadado da tabela e grava apenas um índice numérico compacto de 1 byte na linha (para até 255 opções), mas exibe e filtra como string. No PHP 8.1+, combina perfeitamente com `BackedEnum`. Alternativamente, em sistemas onde a lista de opções muda com frequência, muitos arquitetos preferem `VARCHAR(30)` validado por um `enum` do PHP ou uma tabela de domínio auxiliar.

Limites de Capacidade: 1 valor escolhido por registro dentre a lista declarada

✅ Quando Usar (Casos Ideais)

  • • Status de máquina de estados estável: `ENUM('pendente', 'pago', 'enviado', 'cancelado')`.
  • • Tipo de pessoa: `ENUM('PF', 'PJ')`.
  • • Nível de severidade de um log: `ENUM('info', 'warning', 'error', 'critical')`.

🚫 Quando NÃO Usar (Armadilhas)

  • • Listas que o administrador do sistema pode cadastrar ou editar pelo painel (ex: categorias de produtos, cargos da empresa) — para isso, crie uma tabela separada com Foreign Key!

📐 Como Configurar no DrawDB Abrir DrawDB ↗

Type no DrawDB: ENUM
Size / Values: 'pendente', 'pago', 'enviado', 'cancelado'
Default sugerido: 'pendente'
Equivalência nos Dialetos do DrawDB:
MySQL / MariaDB ENUM('pendente', 'pago', 'cancelado') NOT NULL DEFAULT 'pendente'
PostgreSQL CREATE TYPE status_pedido AS ENUM (...) ou VARCHAR(20) CHECK (...)
SQLite TEXT CHECK(status IN ('pendente', 'pago', 'cancelado'))

📋 Exemplos Reais de Colunas com ENUM:

status
ENUM('pendente', 'pago', 'cancelado') NOT NULL DEFAULT 'pendente'
Ciclo de vida de um pedido
tipo_pessoa
ENUM('PF', 'PJ') NOT NULL
Pessoa Física ou Jurídica
🗄️ SQL DDL (Importar no DrawDB: Import > SQL)
CREATE TABLE pagamentos (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    pedido_id BIGINT UNSIGNED NOT NULL,
    metodo ENUM('pix', 'cartao_credito', 'boleto') NOT NULL,
    status ENUM('aguardando', 'aprovado', 'recusado', 'estornado') NOT NULL DEFAULT 'aguardando'
);
🐘 Como Validar e Salvar no PHP 8 + PDO
<?php
declare(strict_types=1);

// Usando Backed Enum nativo do PHP 8.1+ sincronizado com o banco!
enum StatusPagamento: string {
    case Aguardando = 'aguardando';
    case Aprovado   = 'aprovado';
    case Recusado   = 'recusado';
    case Estornado  = 'estornado';
}

// Validando uma string recebida via requisição com tryFrom()
$statusRecebido = StatusPagamento::tryFrom($_POST['status'] ?? 'aguardando');
if ($statusRecebido === null) {
    throw new ValueError('Status de pagamento inválido.');
}

$stmt = $pdo->prepare('
    INSERT INTO pagamentos (pedido_id, metodo, status)
    VALUES (:pedido_id, :metodo, :status)
');
$stmt->bindValue(':pedido_id', 1020, PDO::PARAM_INT);
$stmt->bindValue(':metodo', 'pix', PDO::PARAM_STR);
$stmt->bindValue(':status', $statusRecebido->value, PDO::PARAM_STR);
$stmt->execute();

10. Binários & Hashes Brutos 1 tipo

Armazenamento de bytes puros, chaves criptográficas, UUIDs compactados e arquivos (e quando usar Storage externo).

BINARY / VARBINARY / BLOB (Dados Binários)

💾 BINARY(16): 16 Bytes | BLOB: até 64 KB | MEDIUMBLOB: até 16 MB | LONGBLOB: até 4 GB

Armazena bytes brutos sem codificação de caracteres (chaves criptográficas, UUID binário, certificados).

`BINARY(n)` e `VARBINARY(n)` funcionam exatamente como `CHAR` e `VARCHAR`, mas guardam bytes puros em vez de caracteres de texto (sem collation). Já `BLOB` (Binary Large Object) guarda arquivos binários maiores. REGRA DE OURO DA ARQUITETURA: Evite salvar fotos de produtos, PDFs ou vídeos dentro de colunas `BLOB` no banco! Salve os arquivos no disco ou em Object Storage (Amazon S3, Cloudflare R2, MinIO) e grave apenas o caminho/URL em um `VARCHAR(255)` no banco.

Limites de Capacidade: Bytes puros sem conversão de charset

✅ Quando Usar (Casos Ideais)

  • • `BINARY(16)`: Armazenar UUID compactado em 16 bytes (`UUID_TO_BIN` no MySQL 8) ou endereços IPv4/IPv6 com `inet_pton()` do PHP (`VARBINARY(16)`).
  • • `VARBINARY` / `BLOB`: Certificados digitais A1 (`.pfx`) criptografados, chaves criptográficas pequenas ou assinaturas digitais.

🚫 Quando NÃO Usar (Armadilhas)

  • • Fotos de usuários, imagens de produtos, vídeos ou relatórios em PDF — eles inflam o backup do banco de dados e esgotam a memória RAM de cache (Buffer Pool). Use `VARCHAR(255)` com a URL do arquivo!

📐 Como Configurar no DrawDB Abrir DrawDB ↗

Type no DrawDB: VARBINARY ou BLOB
Size / Values: 16 (para IP ou UUID binário em VARBINARY)
Default sugerido: Vazio
Equivalência nos Dialetos do DrawDB:
MySQL / MariaDB BINARY(16), VARBINARY(255) ou BLOB
PostgreSQL BYTEA (ou tipo INET nativo para endereços IP)
SQLite BLOB

📋 Exemplos Reais de Colunas com VARBINARY:

ip_binario
VARBINARY(16) NOT NULL
Suporta tanto IPv4 (4 bytes) quanto IPv6 (16 bytes)
foto_perfil_url
VARCHAR(255) NULL
Alternativa recomendada ao BLOB para imagens!
🗄️ SQL DDL (Importar no DrawDB: Import > SQL)
CREATE TABLE sessoes_seguranca (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    usuario_id BIGINT UNSIGNED NOT NULL,
    ip_endereco VARBINARY(16) NOT NULL COMMENT 'IPv4 ou IPv6 compactado via inet_pton',
    token_hash BINARY(32) NOT NULL COMMENT 'SHA-256 binário puro de 32 bytes'
);
🐘 Como Validar e Salvar no PHP 8 + PDO
<?php
declare(strict_types=1);

// Armazenando endereço IP (IPv4 ou IPv6) em VARBINARY(16) via inet_pton() do PHP
$ipCliente = '2001:0db8:85a3:0000:0000:8a2e:0370:7334';
$ipBinario = inet_pton($ipCliente); // Converte para 16 bytes binários

// Hash SHA-256 binário puro (32 bytes em vez de 64 caracteres hexadecimais)
$tokenHashBinario = hash('sha256', 'token-secreto-do-usuario', true);

$stmt = $pdo->prepare('
    INSERT INTO sessoes_seguranca (usuario_id, ip_endereco, token_hash)
    VALUES (:usuario_id, :ip, :token)
');
$stmt->bindValue(':usuario_id', 1, PDO::PARAM_INT);
$stmt->bindValue(':ip', $ipBinario, PDO::PARAM_LOB);
$stmt->bindValue(':token', $tokenHashBinario, PDO::PARAM_LOB);
$stmt->execute();

// Para converter de volta dos bytes para string de IP legível no PHP:
$ipLegivel = inet_ntop($ipBinario);