C++: Prática: Banco de Dados Simples
Última atualização: 2026-08-26
Na aula 51, construímos um sistema de agenda.
Agora, vamos construir nosso segundo projeto prático abrangente — um banco de dados simples.
Este projeto usa muitos recursos avançados do C++, sendo um ótimo teste do que você aprendeu.
1. Requisitos do Projeto
(1) 1.1 Requisitos Funcionais
| Funcionalidade | Descrição |
|---|---|
| CREATE TABLE | Criar uma tabela |
| INSERT | Inserir dados |
| SELECT | Consultar dados |
| WHERE | Filtragem condicional |
| Index | Acelerar consultas |
| Transaction | Garantir ACID |
(2) 1.2 Design Simplificado
Como este é um projeto educacional, faremos uma versão simplificada:
- Suportar apenas tipos
intestring - Suportar apenas consultas de tabela única
- Índices implementados com
std::map - Dados armazenados em memória (persistência opcional)
2. Design da Estrutura da Tabela
(1) 2.1 Definição de Coluna
▶ Exemplo 2: Exemplo de Código (Dificuldade ⭐)
#include <iostream>
#include <string>
#include <variant>
#include <vector>
// Definição de coluna
struct Column {
std::string name;
enum Type { INT, STRING } type;
};
// Tipo de valor (usar variant para armazenar tipos diferentes)
using Value = std::variant<int, std::string>;
// Linha (uma linha de dados)
using Row = std::vector<Value>;
// Tabela
class Table {
private:
std::string name;
std::vector<Column> columns;
std::vector<Row> rows;
public:
Table(const std::string& name, const std::vector<Column>& columns)
: name(name), columns(columns) {}
// Inserir uma linha
void insert(const Row& row) {
if (row.size() != columns.size()) {
std::cerr << "Column count mismatch" << std::endl;
return;
}
rows.push_back(row);
}
// Consultar (simplificado: retornar todas as linhas)
std::vector<Row> select() {
return rows;
}
// Exibir estrutura da tabela
void describe() const {
std::cout << "Table name: " << name << std::endl;
std::cout << "Columns: " << std::endl;
for (const auto& col : columns) {
std::cout << " " << col.name << " (";
if (col.type == Column::INT) {
std::cout << "INT";
} else {
std::cout << "STRING";
}
std::cout << ")" << std::endl;
}
}
};
Saída:
Table name:
Columns:
(
INT
STRING
)
3. Implementação de Consultas
(1) 3.1 Cláusula WHERE
// Função predicado (para WHERE)
using Predicate = std::function<bool(const Row&)>;
// Consulta com condição
std::vector<Row> selectWhere(Predicate pred) {
std::vector<Row> result;
for (const auto& row : rows) {
if (pred(row)) {
result.push_back(row);
}
}
return result;
}
// Exemplo: encontrar linhas onde age > 18
auto predicate = [](const Row& row) {
int age = std::get<int>(row[1]); // Assume que coluna 2 é age
return age > 18;
};
auto result = table.selectWhere(predicate);
4. Implementação de Índice
(1) 4.1 Índice Simples
#include <map>
// Índice (simplificado: apenas indexar tipos int)
class Index {
private:
std::map<int, std::vector<int>> index; // Valor -> lista de números de linha
public:
void build(const std::vector<Row>& rows, int colIndex) {
index.clear();
for (int i = 0; i < rows.size(); i++) {
int key = std::get<int>(rows[i][colIndex]);
index[key].push_back(i);
}
}
std::vector<int> lookup(int key) {
auto it = index.find(key);
if (it != index.end()) {
return it->second;
}
return {};
}
};
5. Suporte a Transações
(1) 5.1 Transação Simplificada
#include <stack>
class Transaction {
private:
std::stack<std::vector<Row>> undoStack;
public:
// Iniciar transação (salvar estado atual)
void begin(Table& table) {
undoStack.push(table.rows);
}
// Commit (limpar pilha de undo)
void commit() {
while (!undoStack.empty()) {
undoStack.pop();
}
}
// Rollback (restaurar estado anterior)
void rollback(Table& table) {
if (!undoStack.empty()) {
table.rows = undoStack.top();
undoStack.pop();
}
}
};
6. Exemplo Completo
▶ Exemplo 1: Usando o Banco de Dados Simples (Dificuldade ⭐⭐⭐)
int main() {
// Criar tabela
Table t("users", {{"name", Column::STRING}, {"age", Column::INT}});
// Inserir dados
t.insert({"Alice", 25});
t.insert({"Bob", 30});
t.insert({"Charlie", 20});
// Consultar todos
auto rows = t.select();
std::cout << "All users:" << std::endl;
for (const auto& row : rows) {
std::cout << std::get<std::string>(row[0]) << ", "
<< std::get<int>(row[1]) << std::endl;
}
return 0;
}
Saída:
All users:
,
7. Direções de Extensão
(1) 7.1 Extensões de Funcionalidade
| Funcionalidade | Dificuldade | Descrição |
|---|---|---|
| Persistência | ⭐⭐ | Salvar em arquivo |
| JOIN | ⭐⭐⭐⭐ | Junções multi-tabela |
| SQL Parser | ⭐⭐⭐⭐⭐ | Analisar declarações SQL |
| Controle de Concorrência | ⭐⭐⭐⭐ | Segurança de threads |
❓ Exercícios
(1) Exercício Básico (Dificuldade ⭐⭐)
Adicione um recurso DELETE ao banco de dados.
(2) Exercício Intermediário (Dificuldade ⭐⭐⭐)
Adicione um recurso UPDATE ao banco de dados.
(3) Exercício Desafio (Dificuldade ⭐⭐⭐⭐)
Implemente um SQL parser simples que suporte SELECT * FROM table WHERE age > 18.
▶ Exemplo 3: Definição de estrutura de tabela de banco de dados (Dificuldade ⭐)
#include <iostream>
#include <string>
#include <vector>
#include <variant>
// Tipo de valor (suporta múltiplos tipos de dados)
using Value = std::variant<int, std::string, double>;
// Uma linha de dados
struct Row {
int id;
Value name;
Value age;
Value score;
void display() const {
std::cout << "ID: " << id << std::endl;
std::cout << "Name: " << std::get<std::string>(name) << std::endl;
std::cout << "Age: " << std::get<int>(age) << std::endl;
std::cout << "Score: " << std::get<double>(score) << std::endl;
}
};
int main() {
Row r = {1, std::string("Alice"), 20, 95.5};
r.display();
return 0;
}
Saída:
ID:
Name:
Age:
Score:
❓ Perguntas Frequentes
P: Quão rápida é a busca em map? R: map é implementado com uma árvore rubro-negra internamente, então busca, inserção e exclusão são todas O(log n). Para 1 milhão de registros, o pior caso requer apenas cerca de 20 comparações. P: Por que usar CSV para armazenamento em arquivo? R: O formato CSV é simples, legível por humanos e pode ser aberto diretamente no Excel. JSON suporta estruturas aninhadas mas é um pouco mais complexo de analisar. Para produção, SQLite é recomendado. P: Como prevenir injeção de SQL? R: Esta é uma implementação simples. Em ambientes de produção reais, sempre use consultas parametrizadas ou prepared statements. Nunca concatene diretamente a entrada do usuário em strings SQL.
📖 Resumo
| Ponto de Conhecimento | Aplicação |
|---|---|
| variant | Armazenar valores de tipos diferentes |
vector<Row> |
Armazenar dados de tabela |
| map | Implementar índices |
| Objetos de função | Implementar predicados WHERE |
- Banco de dados simples: dados em memória + persistência em arquivo
- Usar map para busca chave O(log n)
- Suportar interface de comandos CRUD
- Formato CSV ou JSON para armazenamento de dados
- Tratamento de exceções para estabilidade do programa
📝 Exercícios
-
Básico (Dificuldade ⭐): Execute o programa de banco de dados, crie uma tabela "students" (campos: matrícula, nome, idade, nota), insira 3 registros e consulte todos os registros.
-
Intermediário (Dificuldade ⭐⭐): Adicione um recurso de "consulta ordenada" — suporte
SELECT * FROM table ORDER BY age. Dica: usestd::sort+ expressão lambda para ordenar por um campo especificado. -
Desafio (Dificuldade ⭐⭐⭐): Implemente "junção multi-tabela" — crie duas tabelas (students, grades), suporte
SELECT students.name, grades.score FROM students JOIN grades ON students.id = grades.student_id. Você precisa projetar a lógica de JOIN e a mesclagem de resultados.
Próxima aula: Resumo do Curso e Roteiro Avançado (#53)