🧬 CAPÍTULO 06: ANATOMIA DE ATRIBUTOS, DOMÍNIOS E TIPOS


🎯 Objetivos de Aprendizagem

Ao final deste capítulo (estimativa: 2 horas de estudo autoguiado), você será capaz de:

  • 🔹 Analisar a alocação física de dados em disco e memória RAM (Data Pages de 8KB/16KB, Row Headers, Null Bitmaps e Data Alignment).
  • 🔹 Dominar os tamanhos exatos em bits e bytes dos tipos SQL (TINYINT 8b, SMALLINT 16b, INT 32b, BIGINT 64b, NUMERIC exato e VARCHAR).
  • 🔹 Evitar as 4 armadilhas clássicas de conversão entre Python e SQL (Overflow numérico, Imprecisão do Float, Truncamento de String e Timestamps ingênuos).
  • 🔹 Implementar tabelas híbridas com colunas JSONB nativas no SQLAlchemy 2.0.

No Modelo Relacional, a organização da informação é cirúrgica. Para que um banco de dados seja escalável e performático (como no MySQL 8.4 ou PostgreSQL 17), precisamos entender como cada "pedaço" de dado se encaixa na estrutura. 🛡️🧩

🏢 O Cenário Prático (Seu Desafio)

Você atua na TecProExpress. O setor de desenvolvimento reclamou que os IDs dos novos pacotes estão colidindo e gerando erros nas aplicações. Além disso, eles precisam de uma forma de salvar "detalhes técnicos variáveis" de eletrônicos (voltagem, cor, garantia) que mudam para cada produto, tudo sem criar mil colunas vazias.

"Seu desafio é dominar a engenharia de chaves primárias (Autoincrement/Identity) e explorar como armazenar flexibilidade no modelo relacional usando campos JSON nativos."


🧠 Fundamentos: Anatomia de um Registro

1. O Atributo (Campo)

Um Atributo é a menor unidade de informação. Ele precisa seguir as regras do seu Domínio:

  • Atomicidade: O valor deve ser indivisível (ex: separar a Rua do Bairro em vez de criar um "Endereço Completo").
  • Tipo de Dado: INT, VARCHAR, DATE (Limita o que o usuário pode preencher).

2. A Chave Primária (PK)

Nenhuma tabela profissional existe sem uma Chave Primária. Ela é o DNA da tupla, garantindo a integridade dos registros.

🔍 As Duas Leis da PK:

  1. Unicidade: O valor nunca se repete.
  2. Obrigatoriedade: O campo da PK é sempre NOT NULL (Não pode ser vazio).

🛢️ Anatomia dos Tipos de Dados SQL: Bits, Bytes e Armazenamento no SGBD

Diferente do Python (onde a memória é gerenciada em objetos dinâmicos no Heap), os Sistemas Gerenciadores de Bancos de Dados (PostgreSQL, MySQL, SQLite) precisam de definições rígidas e previsíveis de tipos para gravar dados de forma contígua em Páginas de Disco e Buffer Pool (geralmente de 8 KB no PostgreSQL ou 16 KB no MySQL InnoDB).

flowchart LR
    subgraph DISK_PAGE ["🛢️ Armazenamento Físico no SGBD (Páginas de 8KB / 16KB)"]
        direction TB
        HDR["Row Header + Null Bitmap (1 bit por coluna)"]
        T1["TINYINT: 8 bits (1 byte) ➔ -128 a 127"]
        T2["SMALLINT: 16 bits (2 bytes) ➔ -32.768 a 32.767"]
        T3["INTEGER: 32 bits (4 bytes) ➔ -2,14 bilhões a +2,14 bilhões"]
        T4["BIGINT: 64 bits (8 bytes) ➔ -9 quintilhões a +9 quintilhões"]
        T5["NUMERIC(10,2): Decimal exato BCD (sem dízima binária)"]
        T6["VARCHAR(n): 1-2 bytes de tamanho + bytes do texto"]
        
        HDR --> T1 --> T2 --> T3 --> T4 --> T5 --> T6
    end

Alocação Física no SGBD

📊 Tabela Mestra de Tipos SQL: Bits, Bytes e Limites Físicos

Tipo SQLBitsBytes em DiscoFaixa de Valores / LimiteComo o SGBD Armazena
BOOLEAN / BOOL8 bits (ou 1 bit)1 byte (ou bitfield)TRUE, FALSE, NULLArmazenado como byte simples ou em mapa de bits interno.
TINYINT8 bits1 byte-128 a 127 (ou 0 a 255 com UNSIGNED)1 byte puro em complemento de dois.
SMALLINT16 bits2 bytes-32.768 a 32.767Inteiro de 16 bits com alinhamento de 2 bytes.
INT / INTEGER32 bits4 bytes-2.147.483.648 a 2.147.483.647Inteiro padrão de 32 bits com alinhamento de 4 bytes.
BIGINT64 bits8 bytes-9.223.372.036.854.775.808 a +9.223.372.036.854.775.807Inteiro longo de 64 bits com alinhamento de 8 bytes.
REAL / FLOAT(24)32 bits4 bytes~6 a 7 dígitos de precisão (IEEE 754)Ponto flutuante binário simples de 32 bits.
DOUBLE PRECISION / FLOAT(53)64 bits8 bytes~15 a 17 dígitos de precisão (IEEE 754)Ponto flutuante binário duplo de 64 bits.
NUMERIC(p, s) / DECIMAL(p, s)Variável~4 a 16 bytes (conforme $p$)Exato, sem dízima binária (ex: 10, 2 = 8 dígitos inteiros + 2 decimais)Formato BCD (Binary-Coded Decimal) em blocos de 4 dígitos por 2 bytes.
CHAR(n)$n \times 8$ bits$n$ bytes fixosExatamente $n$ caracteresFixo em disco: preenche com espaços em branco à direita se o texto for menor.
VARCHAR(n)Variável1 a 2 bytes + tamanho realAté $n$ caracteresPrefixo de tamanho (1 byte se $n \le 255$, 2 bytes se $n > 255$) + caracteres reais.
TEXT / BLOBVariávelInline ou TOASTAté 1 GB (PostgreSQL) / 4 GB (MySQL)Se $> 2\text{ KB}$, comprimido e movido para fora da página principal (TOAST).
DATE24 a 32 bits3 a 4 bytesAno 0001 a 9999Número inteiro representando o deslocamento de dias em relação a uma época base.
TIMESTAMP64 bits8 bytesData + Hora com precisão de microsegundosInteiro de 64 bits em microssegundos desde 2000-01-01 (PostgreSQL).
JSONBVariávelBinário EstruturadoIlimitadoÁrvore binária indexada com busca direta de chaves sem reparsing.

🧠 Como o SGBD Gerencia a Memória RAM e Disco:

  1. Páginas de Dados (Data Pages): O SGBD lê e grava blocos inteiros de 8 KB (PostgreSQL) ou 16 KB (MySQL). Quanto menores forem seus tipos de dados (SMALLINT em vez de BIGINT), mais registros cabem por página, reduzindo drasticamente o I/O de disco.
  2. Null Bitmap (Mapa de Nulos): Se uma coluna aceita NULL e estiver vazia, o banco não gasta espaço na coluna; ele apenas ativa 1 bit no cabeçalho da linha (Row Header).
  3. Data Alignment & Padding (Alinhamento de Memória): Processadores de 64 bits leem memória em blocos múltiplos de 8 bytes. Colocar colunas na ordem (BIGINT, INT, SMALLINT) é mais eficiente em disco do que alternar (SMALLINT, BIGINT, SMALLINT), pois evita bytes de enchimento (padding).

🌉 A Ponte Objeto-Relacional: Tabela de Equivalências e Mapeamento

Abaixo, veja como os tipos viajam da modelagem conceitual até a memória do Python e a persistência no SGBD via SQLAlchemy 2.0:

Conceito / Domínio SQL (PostgreSQL / SQLite) Python POO (Memória RAM) ORM (SQLAlchemy 2.0) Tamanho no SQL
Identificador Curto SMALLINT int Mapped[int] = mapped_column(SmallInteger) 16 bits (2 bytes)
Chave Primária / ID Padrão INTEGER / INT / SERIAL int Mapped[int] = mapped_column(Integer, primary_key=True) 32 bits (4 bytes)
Contador de Alta Escala BIGINT / BIGSERIAL int Mapped[int] = mapped_column(BigInteger) 64 bits (8 bytes)
Texto Limitado VARCHAR(100) str Mapped[str] = mapped_column(String(100)) Variável (1B + texto)
Valor Monetário / Contábil NUMERIC(10,2) / DECIMAL Decimal (da lib decimal) Mapped[Decimal] = mapped_column(Numeric(10,2)) Exato (~5 a 8 bytes)
Data e Hora de Auditoria TIMESTAMP / DATETIME datetime Mapped[datetime] = mapped_column(DateTime) 64 bits (8 bytes)
Booleano / Indicador BOOLEAN bool Mapped[bool] = mapped_column(Boolean) 1 byte
Documento / Metadados JSON / JSONB dict / list Mapped[dict[str, Any]] = mapped_column(JSON) Variável
flowchart LR
    subgraph Dominio ["1. Domínio Real"]
        D["Entidade: Carro<br/>Atributo: Placa, Preço"]
    end
    subgraph Python ["2. Memória (Python POO)"]
        P["class Carro:<br/>placa: str (Heap)<br/>preco: Decimal"]
    end
    subgraph SQL ["3. Persistência (SQL DDL)"]
        S["CREATE TABLE carro (<br/>placa VARCHAR(10) PK,<br/>preco NUMERIC(10,2))"]
    end
    Dominio --> Python
    Python <== "ORM SQLAlchemy (Conversão Binária)" ==> SQL
    style Dominio fill:#e3f2fd,stroke:#1e88e5
    style Python fill:#fff8e1,stroke:#fbc02d
    style SQL fill:#e8f5e9,stroke:#43a047

⚠️ As 4 Grandes Armadilhas de Conversão Python ↔ SQL:

  1. Armadilha do Overflow Numérico: Em Python, x = 10**20 funciona perfeitamente (precisão arbitrária). Porém, se você tentar salvar esse valor em uma coluna INT (32 bits) ou BIGINT (64 bits), o SGBD abortará a transação com NumericValueOutOfRange.
  2. Armadilha do Dinheiro com Float: Nunca use float em Python para mapear colunas financeiras NUMERIC. Use sempre from decimal import Decimal para evitar dízimas binárias (ex: 0.1 + 0.2 = 0.30000000000000004).
  3. Armadilha do Truncamento de String: O Python expande strings conforme a memória RAM permitir, mas se o banco tiver VARCHAR(50), tentar gravar uma string de 51 caracteres causará StringDataRightTruncation.
  4. Armadilha do Fuso Horário em Timestamps: Salvar datetime.now() ingênuo (naive) em colunas TIMESTAMP WITHOUT TIME ZONE pode causar inconsistências graves em deploys na nuvem. Use sempre datetime.now(timezone.utc) com colunas TIMESTAMPTZ.

📖 Exemplo Guiado: Autoincremento Poliglota (DDL -> DML)

A solução para o problema de IDs colidindo na TecProExpress é deixar o próprio SGBD gerenciar a numeração automática das chaves primárias.

Muitos iniciantes não sabem que a sintaxe muda radicalmente entre os bancos.

🛠️ Código no MySQL 8.4 LTS

-- DDL
CREATE TABLE pacotes_mysql (
    id INT PRIMARY KEY AUTO_INCREMENT,
    destino VARCHAR(100)
);

-- DML (O ID é gerado sozinho)
INSERT INTO pacotes_mysql (destino) VALUES ('São Paulo');

🛠️ Código no PostgreSQL 17

-- DDL
CREATE TABLE pacotes_postgres (
    id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    destino VARCHAR(100)
);

-- DML
INSERT INTO pacotes_postgres (destino) VALUES ('Rio de Janeiro');

🔍 Detalhamento do Código:

  • AUTO_INCREMENT vs GENERATED ALWAYS AS IDENTITY: É essencial dominar ambos para atuar no mercado poliglota.
  • Note que, no INSERT, omitimos a coluna id de propósito, pois o SGBD é o responsável pela geração.

📐 Tipos de Dados e Restrições de Domínio

1. Power Tools de Domínio

Além dos tipos básicos (Texto, Número, Data), adicionamos inteligência com restrições (Constraints):

  • NOT NULL: Preenchimento obrigatório.
  • UNIQUE: Valor não repetido, mas permite NULL (ex: E-mail de marketing secundário).
  • CHECK: Valida lógicas (ex: CHECK (preco > 0)).

2. JSON vs JSONB (Arquitetura Híbrida)

A resposta para a TecProExpress armazenar "detalhes técnicos variáveis" sem criar mil colunas é utilizar o poder dos SGBDs modernos que suportam Documentos JSON dentro do modelo relacional:

  • MySQL JSON: Armazena e valida o texto estruturado.
  • Postgres JSONB: Armazena em binário indexado. É incrivelmente rápido para filtros internos.

🛠️ Prática Obrigatória: Carga Híbrida

Cenário: A tabela de produtos avançados da TecProExpress.

  1. Crie a tabela produto_hibrido com uma PK autoincremental e uma coluna chamada especificacoes do tipo JSON.
  2. Insira 2 produtos com detalhes estruturais completamente diferentes usando DML.

🚀 Script de Seed (Gabarito)

-- DDL (Sintaxe MySQL)
CREATE TABLE produto_hibrido (
    id INT PRIMARY KEY AUTO_INCREMENT,
    nome VARCHAR(100) NOT NULL,
    especificacoes JSON
);

-- DML
INSERT INTO produto_hibrido (nome, especificacoes) 
VALUES ('Notebook Gamer', '{"cpu": "i7", "ram": "16GB"}');

INSERT INTO produto_hibrido (nome, especificacoes) 
VALUES ('Cabo HDMI', '{"tamanho_metros": 2, "cor": "preto", "banhado_ouro": true}');


💻 Ponte Prática: Do SQL Manual ao SQLAlchemy 2.0 ORM

Como o SQLAlchemy 2.0 lida com colunas JSON nativas, autoincremento e mapeamento tipado em Python?

🔴 1. A Abordagem Manual (Serialização Manual de Strings JSON)

No modelo manual, o desenvolvedor precisa converter strings JSON na mão (json.loads/json.dumps), sem validação de integridade:

# ❌ ABORDAGEM COM SQL MANUAL: Conversão manual frágil
import sqlite3, json

conn = sqlite3.connect("catalogo_legado.db")
cursor = conn.cursor()
cursor.execute("CREATE TABLE IF NOT EXISTS prod (id INTEGER PRIMARY KEY, specs TEXT);")

# Inserção manual de string JSON:
dados_specs = {"voltagem": 220, "garantia_meses": 12}
cursor.execute("INSERT INTO prod (specs) VALUES (?);", (json.dumps(dados_specs),))
conn.commit()

🟢 2. A Abordagem com SQLAlchemy 2.0 (Coluna JSON Nativa e Tipagem)

Com o SQLAlchemy 2.0, usamos o tipo JSON nativo. O ORM converte dicionários Python diretamente em JSON no banco e restaura como dict na consulta:

# ✅ ABORDAGEM MODERNA: Mapeamento Relacional Híbrido (SQL + JSON)
from typing import Any
from sqlalchemy import create_engine, String, Integer, JSON, select
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, Session

class Base(DeclarativeBase):
    pass

class ProdutoHibridoModel(Base):
    """Tabela relacional com coluna JSON híbrida para especificações variáveis."""
    __tablename__ = "produtos_hibridos"

    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
    nome: Mapped[str] = mapped_column(String(100), nullable=False)
    especificacoes: Mapped[dict[str, Any]] = mapped_column(JSON, nullable=False)

    def __repr__(self) -> str:
        return f"Produto(id={self.id}, nome='{self.nome}', specs={self.especificacoes})"

🛠️ Mini-Projeto 06 (BD): Catálogo Híbrido de Produtos com JSON

Objetivo: Criar e consultar registros com estruturas de especificações heterogêneas utilizando a coluna JSON do SQLAlchemy 2.0.

📋 Pré-requisitos e Instalação

No terminal do seu ambiente virtual (PowerShell ou Bash), instale a biblioteca necessária:

pip install sqlalchemy

💻 Código Completo e Autocontido (miniprojeto_06_hibrido.py)

Crie o arquivo miniprojeto_06_hibrido.py e insira o código abaixo integralmente:

"""
Mini-Projeto 06: Catálogo Híbrido de Produtos com JSON
Curso: GTI - Banco de Dados Relacionais e Engenharia de Software
Stack: Python 3.11+ | SQLAlchemy 2.0 | SQLite
"""
import os
from typing import Any
from sqlalchemy import create_engine, String, Integer, JSON, select
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, Session

# 1. Definição Declarativa do Schema Híbrido
class Base(DeclarativeBase):
    pass

class ProdutoHibridoModel(Base):
    """Tabela relacional com coluna JSON híbrida para especificações variáveis."""
    __tablename__ = "produtos_hibridos"

    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
    nome: Mapped[str] = mapped_column(String(100), nullable=False)
    especificacoes: Mapped[dict[str, Any]] = mapped_column(JSON, nullable=False)

    def __repr__(self) -> str:
        return f"Produto(id={self.id}, nome='{self.nome}', specs={self.especificacoes})"

# 2. Ponto de Entrada Executável
if __name__ == "__main__":
    DB_FILE = "tecpro_produtos.db"

    # Reset preventivo para garantir idempotência em testes repetidos
    if os.path.exists(DB_FILE):
        os.remove(DB_FILE)

    engine = create_engine(f"sqlite:///{DB_FILE}", echo=False)
    Base.metadata.create_all(bind=engine)

    print("🧬 CATÁLOGO HÍBRIDO RELACIONAL + JSON (TECPROEXPRESS)")
    print("=" * 65)

    # 1. Inserindo produtos com especificações completamente distintas
    with Session(engine) as session:
        p1 = ProdutoHibridoModel(
            nome="Notebook Dell Latitude",
            especificacoes={"processador": "Core i7", "ram_gb": 16, "ssd_gb": 512, "voltagem": "Bivolt"}
        )
        p2 = ProdutoHibridoModel(
            nome="Cabo de Rede Furukawa Cat6",
            especificacoes={"comprimento_metros": 50, "blindagem": "STP", "cor": "Azul"}
        )
        p3 = ProdutoHibridoModel(
            nome="Sensor de Temperatura IoT",
            especificacoes={"bateria_duracao_anos": 3, "protocolo": "MQTT", "faixa_celsius": [-20, 70]}
        )

        session.add_all([p1, p2, p3])
        session.commit()
        print("✅ 3 Produtos híbridos inseridos com sucesso!")

    # 2. Consultando e acessando chaves JSON como objetos Python nativos
    print("\n🔍 Consultando catálogo e lendo atributos JSON diretamente:")
    with Session(engine) as session:
        produtos = session.scalars(select(ProdutoHibridoModel)).all()
        for p in produtos:
            print(f"\n📦 {p.nome} (ID: {p.id})")
            for chave, valor in p.especificacoes.items():
                print(f"   • {chave}: {valor}")
    print("=" * 65)

🚀 Como Executar

Execute o script no terminal:

python miniprojeto_06_hibrido.py

🖥️ Saída Esperada no Console

🧬 CATÁLOGO HÍBRIDO RELACIONAL + JSON (TECPROEXPRESS)
=================================================================
✅ 3 Produtos híbridos inseridos com sucesso!

🔍 Consultando catálogo e lendo atributos JSON diretamente:

📦 Notebook Dell Latitude (ID: 1)
   • processador: Core i7
   • ram_gb: 16
   • ssd_gb: 512
   • voltagem: Bivolt

📦 Cabo de Rede Furukawa Cat6 (ID: 2)
   • comprimento_metros: 50
   • blindagem: STP
   • cor: Azul

📦 Sensor de Temperatura IoT (ID: 3)
   • bateria_duracao_anos: 3
   • protocolo: MQTT
   • faixa_celsius: [-20, 70]
=================================================================

💡 Checkpoint de Lógica

Dica

Dica do Arquiteto: Evite usar BIGINT para tudo. No MySQL, se um campo nunca passará de 255 valores (ex: status de um pedido, idade), use TINYINT. Economia de espaço é sinônimo de performance em larga escala! 🚀🛡️



🧪 Quiz de Fixação e Autoavaliação — Capítulo 06

1. Por que valores monetários e financeiros NUNCA devem ser armazenados usando FLOAT ou DOUBLE?

  • A) Porque o banco rejeita salvar números com vírgula.
  • B) Porque FLOAT usa representação binária IEEE 754 com aproximações, acumulando dízimas e erros de centavos (ex: 0.1 + 0.2 = 0.30000000000000004). Deve-se usar sempre NUMERIC(p,s) / DECIMAL(p,s).
  • C) Porque FLOAT gasta mais memória que NUMERIC.
  • D) Porque FLOAT só aceita números inteiros.
💡 Ver Resposta e Justificativa

Resposta Correta: B
Justificativa: Bancos e sistemas fiscais exigem representação decimal exata (BCD) com NUMERIC(10,2) no banco e from decimal import Decimal no Python.


2. No armazenamento físico do SGBD, como o banco registra que uma coluna opcional contém valor NULL?

  • A) Gravando o texto 'NULL' ocupando 4 bytes na coluna.
  • B) Ativando exatamente 1 bit no mapa de bits (Null Bitmap) localizado no cabeçalho da linha, sem gastar nenhum byte extra de espaço na coluna.
  • C) Apagando a tabela inteira do disco.
  • D) Preenchendo a coluna com zeros binários.
💡 Ver Resposta e Justificativa

Resposta Correta: B
Justificativa: SGBDs são altamente eficientes: campos NULL não ocupam espaço na área de dados da linha; sua presença é sinalizada em um bitmask no cabeçalho da tupla.


3. Qual a diferença entre os tipos de texto CHAR(10) e VARCHAR(10)?

  • A) CHAR(10) é sempre fixo em 10 bytes (preenche com espaços se o texto tiver 3 letras); VARCHAR(10) gasta apenas o tamanho real das letras + 1 byte de cabeçalho de comprimento.
  • B) CHAR só aceita números e VARCHAR só aceita letras.
  • C) VARCHAR apaga o texto após 10 dias.
  • D) Não há nenhuma diferença prática.
💡 Ver Resposta e Justificativa

Resposta Correta: A
Justificativa: Use CHAR(n) apenas quando o tamanho for rigidamente fixo (ex: UF CHAR(2), Hash SHA-256 CHAR(64)). Para dados de tamanho variável (nomes, emails), use VARCHAR(n).


🎯 Laboratório Prático

Coloque este conhecimento em prática agora mesmo executando o roteiro autoguiado:
👉 ATIVIDADE 06: SQL DML (MANIPULAÇÃO DE DADOS)