PHP Banco de Dados: o Básico
Até aqui, os programas da lojinha da Ana guardavam os produtos em arrays, que somem quando o programa termina, ou em arquivos CSV e JSON, que funcionam, mas ficam difíceis quando a loja cresce. Os sites de verdade guardam os dados num banco de dados. Nesta aula você vai entender o que é um banco relacional (tabelas, linhas, colunas, chave primária) e aprender a língua que se fala com ele, o SQL, rodando cada comando de verdade.
- Por que usar um banco de dados em vez de arrays e arquivos.
- O que são tabela, linha, coluna e chave primária.
- Os cinco comandos básicos do SQL:
CREATE TABLE,INSERT,SELECT,UPDATEeDELETE. - Como filtrar (
WHERE), ordenar (ORDER BY), limitar (LIMIT) e contar (COUNT,SUM). - As diferenças entre SQLite, MySQL e PostgreSQL.
- O que significam os erros mais comuns:
no such column,UNIQUE constraint failed,NOT NULL constraint failed…
Os exemplos foram executados com o PHP 8.5 na máquina fictícia da Ana, na pasta ~/loja, usando o SQLite, um banco que já vem dentro do PHP (e dentro do editor do guia).
Por que um banco de dados?
A lojinha da Ana começou com uma lista de produtos num array. Funciona, até ela perceber os problemas:
| Onde guardar | Problema |
|---|---|
| Variáveis e arrays | Somem quando o programa (ou a página) termina. |
| Arquivo CSV ou JSON | Para achar um produto, o programa lê o arquivo inteiro. Para mudar um preço, reescreve o arquivo inteiro. Se dois clientes compram ao mesmo tempo, um pode apagar a gravação do outro. |
| Banco de dados | Guarda de forma permanente, acha um registro entre milhões rapidamente, muda só o que precisa, aguenta vários acessos ao mesmo tempo e confere regras ("todo produto tem preço"). |
Um banco de dados é um programa especializado em guardar e encontrar dados. O seu programa PHP não mexe nos arquivos do banco: ele faz pedidos ("guarde este produto", "quais produtos custam menos de 10 reais?") e o banco cuida do resto.
A ideia: uma planilha bem organizada
Se você já usou uma planilha (Excel, LibreOffice Calc, Google Planilhas), já conhece a ideia de um banco relacional. A lista de produtos da Ana, num banco, é uma tabela:
| id | nome | preco | estoque |
|---|---|---|---|
| 1 | Caderno | 15.90 | 30 |
| 2 | Caneta azul | 2.50 | 100 |
| 3 | Mochila | 89.90 | 5 |
produtos: 4 colunas e 3 linhas.| Termo | Na planilha | No exemplo |
|---|---|---|
| Banco de dados | O arquivo inteiro, com várias abas | A loja da Ana (produtos, clientes, vendas…) |
| Tabela | Uma aba | produtos |
| Coluna (ou campo) | Uma coluna, com um título | nome, preco |
| Linha (ou registro) | Uma linha | O Caderno, com todos os seus dados |
| Chave primária | Um número que não se repete | id |
A diferença para a planilha: o banco é rigoroso. Cada coluna tem um tipo (texto, número…) e regras (pode ficar vazia? tem valor padrão?), e cada tabela tem uma chave primária. Essas regras evitam a bagunça que toda planilha compartilhada acaba tendo.
Chave primária: o número que identifica a linha
Por que um id, se o produto já tem nome? Porque nomes se repetem e mudam: a Ana pode ter duas "Caneta azul" de marcas diferentes, ou renomear "Caderno" para "Caderno 96 folhas". A chave primária (primary key) é uma coluna cujo valor:
- nunca se repete na tabela (o banco recusa um id repetido);
- nunca muda depois de criado;
- é escolhido pelo próprio banco, em sequência (1, 2, 3…), na forma mais comum.
É como o número do RG, ou da matrícula na escola: duas Anas na mesma turma têm números diferentes.
"Relacional"?
O nome vem de relação, o termo matemático para tabela. E as tabelas se relacionam: uma tabela vendas pode guardar só o id do produto vendido (o número 3) em vez de repetir "Mochila, 89.90…" em cada venda. Esse id "emprestado" de outra tabela se chama chave estrangeira. Você vai usar isso nos projetos; nesta aula, uma tabela basta.
Existem bancos que não usam tabelas, como o MongoDB (guarda documentos parecidos com JSON) e o Redis (guarda pares chave-valor na memória). Eles são chamados de NoSQL e têm usos específicos. Para a grande maioria dos sites em PHP, o banco relacional é a escolha certa, e é o que este guia ensina.
SQL: a língua dos bancos
SQL (Structured Query Language, "linguagem de consulta estruturada"; diz-se "ésse-quê-éle" ou, em inglês, "síquel") é a língua que todos os bancos relacionais entendem. Ela é diferente do PHP: em vez de dizer como fazer, passo a passo, você diz o que quer, e o banco descobre como:
SELECT nome, preco FROM produtos WHERE preco < 10 ORDER BY preco;
Lido em voz alta: "selecione nome e preço da tabela produtos onde o preço for menor que 10, ordenado pelo preço". Algumas regras da escrita:
- As palavras do SQL (
SELECT,FROM,WHERE) funcionam em maiúsculas ou minúsculas. O costume é escrevê-las em MAIÚSCULAS, para destacar dos nomes de tabelas e colunas. - Textos vão entre aspas simples:
'Caderno'. Números, sem aspas:15.90(com ponto, nunca vírgula). - Um comando pode ocupar várias linhas. O
;no fim separa comandos; quando o PHP manda um comando por vez, ele é opcional.
Como vamos rodar o SQL
Para você se concentrar no SQL, a Ana escreveu um pequeno "console": o arquivo console.php abre o banco (o arquivo loja.db) e cria a função sql(), que roda um comando e mostra o resultado como tabela. Você não precisa entender o que está dentro dele agora: a próxima aula, Conectar com PDO, explica cada linha.
<?php
// Um "console" de SQL: roda um comando e mostra o resultado como uma tabela.
// (Como ele funciona por dentro é assunto da próxima aula, sobre PDO.)
$pdo = new PDO("sqlite:" . __DIR__ . "/loja.db");
function sql(string $comando): void
{
global $pdo;
echo "sql> $comando\n";
$stmt = $pdo->query($comando);
if ($stmt->columnCount() === 0) { // não era um SELECT
if (preg_match('/^\s*(INSERT|UPDATE|DELETE)/i', $comando)) {
echo "OK, ", $stmt->rowCount(), " linha(s) afetada(s)\n\n";
} else {
echo "OK\n\n";
}
return;
}
$linhas = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (count($linhas) === 0) {
echo "(nenhuma linha)\n\n";
return;
}
$colunas = array_keys($linhas[0]);
$largura = [];
foreach ($colunas as $c) {
$largura[$c] = mb_strlen($c);
foreach ($linhas as $l) {
$largura[$c] = max($largura[$c], mb_strlen($l[$c] ?? "NULL"));
}
}
$celulas = fn($valores) => rtrim(implode(" | ", array_map(
fn($c) => mb_str_pad($valores[$c] ?? "NULL", $largura[$c]), $colunas)));
echo $celulas(array_combine($colunas, $colunas)), "\n";
echo implode("-+-", array_map(fn($c) => str_repeat("-", $largura[$c]), $colunas)), "\n";
foreach ($linhas as $l) {
echo $celulas($l), "\n";
}
echo "(", count($linhas), " linha(s))\n\n";
}
Cada programa desta aula começa com require "console.php"; (veja include e require) e chama sql("…") com um comando. O console mostra o comando depois de sql> e, embaixo, a resposta do banco.
1. Criar a tabela: CREATE TABLE
<?php
require "console.php";
sql("CREATE TABLE produtos (
id INTEGER PRIMARY KEY,
nome TEXT NOT NULL,
preco REAL NOT NULL,
estoque INTEGER NOT NULL DEFAULT 0
)");
$ php criar.php
sql> CREATE TABLE produtos (
id INTEGER PRIMARY KEY,
nome TEXT NOT NULL,
preco REAL NOT NULL,
estoque INTEGER NOT NULL DEFAULT 0
)
OK
$ ls -l loja.db
-rw-r--r-- 1 ana ana 8192 out 16 21:07 loja.db « o banco inteiro mora neste arquivo
- CREATE TABLE produtos ( … )
- "Crie a tabela
produtoscom as colunas…". Cada coluna é nome, tipo e regras, separadas por vírgula. - id INTEGER PRIMARY KEY
- A coluna
idé um número inteiro e é a chave primária. No SQLite, essa combinação faz o banco numerar sozinho (1, 2, 3…) quando você não informa o id. - nome TEXT NOT NULL
- Texto, obrigatório.
NULLé o "vazio" do banco (como onulldo PHP);NOT NULLproíbe deixar vazio. - preco REAL NOT NULL
- Número com casas decimais (real), obrigatório.
- estoque INTEGER NOT NULL DEFAULT 0
- Inteiro, obrigatório, mas com valor padrão 0: se você não informar, vale 0.
Os tipos do SQLite
| Tipo | Guarda | Exemplo | No PHP vira |
|---|---|---|---|
INTEGER | Número inteiro | 30, -2 | int |
REAL | Número com casas | 15.90 | float |
TEXT | Texto | 'Caderno' | string |
BLOB | Dados binários (o conteúdo de um arquivo) | uma imagem | string |
NULL | Ausência de valor | NULL | null |
Datas, no SQLite, costumam ser guardadas como texto no formato '2026-10-16 14:30:00' (veja Datas e Horas). Outros bancos têm mais tipos (VARCHAR, DECIMAL, DATE); você vai vê-los em MySQL e MariaDB.
2. Inserir linhas: INSERT
<?php
require "console.php";
sql("INSERT INTO produtos (nome, preco, estoque) VALUES ('Caderno', 15.90, 30)");
sql("INSERT INTO produtos (nome, preco, estoque) VALUES ('Caneta azul', 2.50, 100)");
sql("INSERT INTO produtos (nome, preco, estoque) VALUES
('Mochila', 89.90, 5),
('Lápis', 1.20, 200),
('Estojo', 9.90, 12)");
sql("INSERT INTO produtos (nome, preco) VALUES ('Borracha', 0.90)");
$ php inserir.php
sql> INSERT INTO produtos (nome, preco, estoque) VALUES ('Caderno', 15.90, 30)
OK, 1 linha(s) afetada(s)
sql> INSERT INTO produtos (nome, preco, estoque) VALUES ('Caneta azul', 2.50, 100)
OK, 1 linha(s) afetada(s)
sql> INSERT INTO produtos (nome, preco, estoque) VALUES
('Mochila', 89.90, 5),
('Lápis', 1.20, 200),
('Estojo', 9.90, 12)
OK, 3 linha(s) afetada(s)
sql> INSERT INTO produtos (nome, preco) VALUES ('Borracha', 0.90)
OK, 1 linha(s) afetada(s)
- INSERT INTO produtos
- "Insira na tabela
produtos…" - (nome, preco, estoque)
- …nestas colunas, nesta ordem. O
idficou de fora: o banco escolhe. - VALUES ('Caderno', 15.90, 30)
- …estes valores, na mesma ordem das colunas. Texto entre aspas simples; números sem aspas.
- VALUES (…), (…), (…)
- Vários produtos num comando só, separados por vírgula: "3 linha(s) afetada(s)".
- (nome, preco) VALUES ('Borracha', 0.90)
- Sem o estoque: vale o
DEFAULT 0.
Aqui os valores estão fixos no código, escritos por você, para aprender o SQL. Quando os dados vierem de um formulário, eles nunca devem ser colados dentro do texto do SQL: isso abre a porta para o ataque SQL injection. O jeito certo (prepare e ?) está em CRUD com PDO e Prepared Statements.
3. Consultar: SELECT
O comando mais usado do SQL. Ele não muda nada: só pergunta.
<?php
require "console.php";
sql("SELECT * FROM produtos");
sql("SELECT nome, preco FROM produtos");
$ php consultar.php sql> SELECT * FROM produtos id | nome | preco | estoque ---+-------------+-------+-------- 1 | Caderno | 15.9 | 30 2 | Caneta azul | 2.5 | 100 3 | Mochila | 89.9 | 5 4 | Lápis | 1.2 | 200 5 | Estojo | 9.9 | 12 6 | Borracha | 0.9 | 0 (6 linha(s)) sql> SELECT nome, preco FROM produtos nome | preco ------------+------ Caderno | 15.9 Caneta azul | 2.5 Mochila | 89.9 Lápis | 1.2 Estojo | 9.9 Borracha | 0.9 (6 linha(s))
- SELECT
- "Selecione…"
- *
- …todas as colunas. Ou escreva só as que quer:
nome, preco. - FROM produtos
- …da tabela
produtos.
Repare: os ids vieram em sequência (1 a 6) sem você escrever nenhum, a Borracha ganhou estoque 0 (o DEFAULT), e os preços aparecem como 15.9: um número REAL não guarda "quantos zeros você digitou". Mostrar R$ 15,90 é trabalho do PHP, com number_format.
Filtrar: WHERE
O WHERE ("onde") escolhe quais linhas vêm. Só passam as linhas em que a condição é verdadeira:
<?php
require "console.php";
sql("SELECT nome, preco FROM produtos WHERE preco < 10");
sql("SELECT nome, estoque FROM produtos WHERE estoque >= 30 AND preco > 2");
sql("SELECT * FROM produtos WHERE nome = 'Mochila'");
sql("SELECT nome FROM produtos WHERE nome LIKE 'C%'");
sql("SELECT nome FROM produtos WHERE nome = 'Régua'");
$ php filtrar.php sql> SELECT nome, preco FROM produtos WHERE preco < 10 nome | preco ------------+------ Caneta azul | 2.5 Lápis | 1.2 Estojo | 9.9 Borracha | 0.9 (4 linha(s)) sql> SELECT nome, estoque FROM produtos WHERE estoque >= 30 AND preco > 2 nome | estoque ------------+-------- Caderno | 30 Caneta azul | 100 (2 linha(s)) sql> SELECT * FROM produtos WHERE nome = 'Mochila' id | nome | preco | estoque ---+---------+-------+-------- 3 | Mochila | 89.9 | 5 (1 linha(s)) sql> SELECT nome FROM produtos WHERE nome LIKE 'C%' nome ----------- Caderno Caneta azul (2 linha(s)) sql> SELECT nome FROM produtos WHERE nome = 'Régua' (nenhuma linha) « não existe: não é erro, só uma resposta vazia
No WHERE | Significa | Repare |
|---|---|---|
= | igual | Um sinal só (no PHP seria ==) |
<> ou != | diferente | |
< > <= >= | menor, maior, menor ou igual, maior ou igual | |
AND, OR, NOT | e, ou, não | Em palavras, não && e || |
BETWEEN 2 AND 20 | entre 2 e 20 (incluindo as pontas) | |
IN (1, 3, 5) | é um destes | |
LIKE 'C%' | texto que combina com o modelo | % = qualquer coisa; _ = um caractere |
IS NULL, IS NOT NULL | está vazio / não está | = NULL não funciona |
Missão: a Ana quer saber quais produtos estão acabando. Mude a consulta para mostrar nome e estoque só dos produtos com estoque menor que 20. Devem vir 3 linhas. Depois, brinque: troque o < 20 por outras condições da tabela acima.
<?php
require "console.php";
sql("SELECT nome, estoque FROM produtos");
<?php
// Um "console" de SQL para o editor: cria a lojinha da Ana na memória
// e mostra o resultado de cada comando como uma tabela.
// (Como ele funciona por dentro é assunto da próxima aula, sobre PDO.)
$pdo = new PDO("sqlite::memory:");
$pdo->exec("CREATE TABLE produtos (
id INTEGER PRIMARY KEY,
nome TEXT NOT NULL,
preco REAL NOT NULL,
estoque INTEGER NOT NULL DEFAULT 0
)");
$pdo->exec("INSERT INTO produtos (nome, preco, estoque) VALUES
('Caderno', 15.90, 30), ('Caneta azul', 2.50, 100), ('Mochila', 89.90, 5),
('Lápis', 1.20, 200), ('Estojo', 9.90, 12), ('Borracha', 0.90, 0)");
function sql(string $comando): void
{
global $pdo;
echo "sql> $comando\n";
$stmt = $pdo->query($comando);
if ($stmt->columnCount() === 0) { // não era um SELECT
if (preg_match('/^\s*(INSERT|UPDATE|DELETE)/i', $comando)) {
echo "OK, ", $stmt->rowCount(), " linha(s) afetada(s)\n\n";
} else {
echo "OK\n\n";
}
return;
}
$linhas = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (count($linhas) === 0) {
echo "(nenhuma linha)\n\n";
return;
}
$colunas = array_keys($linhas[0]);
$largura = [];
foreach ($colunas as $c) {
$largura[$c] = mb_strlen($c);
foreach ($linhas as $l) {
$largura[$c] = max($largura[$c], mb_strlen($l[$c] ?? "NULL"));
}
}
$celulas = fn($valores) => rtrim(implode(" | ", array_map(
fn($c) => mb_str_pad($valores[$c] ?? "NULL", $largura[$c]), $colunas)));
echo $celulas(array_combine($colunas, $colunas)), "\n";
echo implode("-+-", array_map(fn($c) => str_repeat("-", $largura[$c]), $colunas)), "\n";
foreach ($linhas as $l) {
echo $celulas($l), "\n";
}
echo "(", count($linhas), " linha(s))\n\n";
}
<?php
require "console.php";
sql("SELECT nome, estoque FROM produtos WHERE estoque < 20");
Ordenar e limitar: ORDER BY e LIMIT
<?php
require "console.php";
sql("SELECT nome, preco FROM produtos ORDER BY preco");
sql("SELECT nome, preco FROM produtos ORDER BY preco DESC LIMIT 3");
$ php ordenar.php sql> SELECT nome, preco FROM produtos ORDER BY preco nome | preco ------------+------ Borracha | 0.9 Lápis | 1.2 Caneta azul | 2.5 Estojo | 9.9 Caderno | 15.9 Mochila | 89.9 (6 linha(s)) sql> SELECT nome, preco FROM produtos ORDER BY preco DESC LIMIT 3 nome | preco --------+------ Mochila | 89.9 Caderno | 15.9 Estojo | 9.9 (3 linha(s))
- ORDER BY preco
- Ordena pelo preço, do menor para o maior (
ASC, o padrão). - DESC
- Do maior para o menor (descending, "descendente").
- LIMIT 3
- Só as 3 primeiras linhas. Junto com o
ORDER BY … DESC: "os 3 mais caros".
Sem ORDER BY, a ordem das linhas não é garantida. Se a ordem importa, peça.
Contar e somar
<?php
require "console.php";
sql("SELECT COUNT(*) FROM produtos");
sql("SELECT COUNT(*) AS total, SUM(estoque) AS itens, MIN(preco) AS menor, MAX(preco) AS maior FROM produtos");
sql("SELECT nome, preco * estoque AS valor_em_estoque FROM produtos ORDER BY valor_em_estoque DESC");
$ php contas.php sql> SELECT COUNT(*) FROM produtos COUNT(*) -------- 6 (1 linha(s)) sql> SELECT COUNT(*) AS total, SUM(estoque) AS itens, MIN(preco) AS menor, MAX(preco) AS maior FROM produtos total | itens | menor | maior ------+-------+-------+------ 6 | 347 | 0.9 | 89.9 (1 linha(s)) sql> SELECT nome, preco * estoque AS valor_em_estoque FROM produtos ORDER BY valor_em_estoque DESC nome | valor_em_estoque ------------+----------------- Caderno | 477 Mochila | 449.5 Caneta azul | 250 Lápis | 240 Estojo | 118.8 Borracha | 0 (6 linha(s))
- COUNT(*)
- Conta as linhas.
- SUM, MIN, MAX, AVG
- Soma, menor, maior e média de uma coluna.
- AS total
- Dá um apelido à coluna do resultado (sem ele, o título é
COUNT(*)). - preco * estoque
- Contas linha por linha: quanto dinheiro está parado em cada produto.
4. Alterar: UPDATE
<?php
require "console.php";
sql("UPDATE produtos SET preco = 17.50 WHERE id = 1");
sql("UPDATE produtos SET estoque = estoque + 20 WHERE nome = 'Borracha'");
sql("SELECT * FROM produtos WHERE id IN (1, 6)");
$ php atualizar.php sql> UPDATE produtos SET preco = 17.50 WHERE id = 1 OK, 1 linha(s) afetada(s) sql> UPDATE produtos SET estoque = estoque + 20 WHERE nome = 'Borracha' OK, 1 linha(s) afetada(s) sql> SELECT * FROM produtos WHERE id IN (1, 6) id | nome | preco | estoque ---+----------+-------+-------- 1 | Caderno | 17.5 | 30 6 | Borracha | 0.9 | 20 (2 linha(s))
- UPDATE produtos
- "Altere a tabela
produtos…" - SET preco = 17.50
- "…pondo 17.50 na coluna
preco…". Várias colunas:SET preco = 17.50, estoque = 40. - SET estoque = estoque + 20
- Usa o valor atual: "o estoque passa a ser o estoque mais 20" (chegou mercadoria).
- WHERE id = 1
- "…só na linha de id 1."
UPDATE produtos SET preco = 17.50, sem WHERE, deixa todos os produtos custando 17,50. O banco não pergunta "tem certeza?". Escreva o WHERE primeiro, e filtre pela chave primária (WHERE id = 1) sempre que puder: ela garante uma linha só.
5. Apagar: DELETE
<?php
require "console.php";
sql("DELETE FROM produtos WHERE id = 5");
sql("DELETE FROM produtos WHERE id = 5");
sql("SELECT id, nome FROM produtos");
$ php apagar.php sql> DELETE FROM produtos WHERE id = 5 OK, 1 linha(s) afetada(s) sql> DELETE FROM produtos WHERE id = 5 OK, 0 linha(s) afetada(s) « já não existia: zero linhas, sem erro sql> SELECT id, nome FROM produtos id | nome ---+------------ 1 | Caderno 2 | Caneta azul 3 | Mochila 4 | Lápis 6 | Borracha (5 linha(s))
- DELETE FROM produtos
- "Apague linhas da tabela
produtos…" - WHERE id = 5
- "…a de id 5" (o Estojo). Sem
WHERE: apaga todas as linhas (a tabela continua existindo, vazia).
O id 5 deixou um "buraco" na sequência, e é assim mesmo: ids não são reaproveitados para não confundir registros antigos com novos. Para apagar a tabela inteira (estrutura e dados), o comando é outro: DROP TABLE produtos.
Missão: a Mochila (id 3) entrou em promoção: mude o preço dela para 79.90. O Estojo (id 5) saiu de linha: apague-o. O SELECT do fim deve mostrar 5 produtos, com a Mochila a 79.9.
<?php
require "console.php";
// 1. UPDATE: Mochila (id 3) por 79.90
// 2. DELETE: Estojo (id 5)
sql("SELECT * FROM produtos");
<?php
// Um "console" de SQL para o editor: cria a lojinha da Ana na memória
// e mostra o resultado de cada comando como uma tabela.
// (Como ele funciona por dentro é assunto da próxima aula, sobre PDO.)
$pdo = new PDO("sqlite::memory:");
$pdo->exec("CREATE TABLE produtos (
id INTEGER PRIMARY KEY,
nome TEXT NOT NULL,
preco REAL NOT NULL,
estoque INTEGER NOT NULL DEFAULT 0
)");
$pdo->exec("INSERT INTO produtos (nome, preco, estoque) VALUES
('Caderno', 15.90, 30), ('Caneta azul', 2.50, 100), ('Mochila', 89.90, 5),
('Lápis', 1.20, 200), ('Estojo', 9.90, 12), ('Borracha', 0.90, 0)");
function sql(string $comando): void
{
global $pdo;
echo "sql> $comando\n";
$stmt = $pdo->query($comando);
if ($stmt->columnCount() === 0) { // não era um SELECT
if (preg_match('/^\s*(INSERT|UPDATE|DELETE)/i', $comando)) {
echo "OK, ", $stmt->rowCount(), " linha(s) afetada(s)\n\n";
} else {
echo "OK\n\n";
}
return;
}
$linhas = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (count($linhas) === 0) {
echo "(nenhuma linha)\n\n";
return;
}
$colunas = array_keys($linhas[0]);
$largura = [];
foreach ($colunas as $c) {
$largura[$c] = mb_strlen($c);
foreach ($linhas as $l) {
$largura[$c] = max($largura[$c], mb_strlen($l[$c] ?? "NULL"));
}
}
$celulas = fn($valores) => rtrim(implode(" | ", array_map(
fn($c) => mb_str_pad($valores[$c] ?? "NULL", $largura[$c]), $colunas)));
echo $celulas(array_combine($colunas, $colunas)), "\n";
echo implode("-+-", array_map(fn($c) => str_repeat("-", $largura[$c]), $colunas)), "\n";
foreach ($linhas as $l) {
echo $celulas($l), "\n";
}
echo "(", count($linhas), " linha(s))\n\n";
}
<?php
require "console.php";
sql("UPDATE produtos SET preco = 79.90 WHERE id = 3");
sql("DELETE FROM produtos WHERE id = 5");
sql("SELECT * FROM produtos");
SQLite, MySQL ou PostgreSQL?
O SQL é quase o mesmo em todos os bancos relacionais, mas cada um tem seu jeito de funcionar. Os três que você mais vai encontrar com PHP:
| SQLite | MySQL / MariaDB | PostgreSQL | |
|---|---|---|---|
| Como funciona | Uma biblioteca dentro do PHP; o banco é um arquivo | Um servidor: um programa sempre rodando, que atende pela rede | Um servidor, como o MySQL |
| Instalar | Nada: vem com o PHP | Instalar e ligar o servidor | Instalar e ligar o servidor |
| Driver do PDO | pdo_sqlite (sqlite:) | pdo_mysql (mysql:) | pdo_pgsql (pgsql:) |
| Usuários e senhas | Não | Sim | Sim |
| Vários escrevendo ao mesmo tempo | Um de cada vez | Sim | Sim |
| Tipos das colunas | Flexíveis (aceita texto em coluna de número, salvo tabelas STRICT) | Rígidos | Rígidos e muito variados (datas, JSON, listas…) |
| Id automático | INTEGER PRIMARY KEY | INT AUTO_INCREMENT PRIMARY KEY | SERIAL ou GENERATED ALWAYS AS IDENTITY |
| Onde aparece | Celulares, navegadores, programas de computador, sites pequenos, testes | A maioria das hospedagens PHP, WordPress, lojas virtuais | Sistemas grandes, dados complexos, empresas |
| Bom para aprender? | Ótimo: zero instalação | Sim, depois do básico | Sim, depois do básico |
Este guia usa o SQLite em todos os exemplos e missões: ele roda no seu computador e no editor do guia sem instalar nada. Como o PHP conversa com todos pelo mesmo PDO, o que você aprender vale para os outros; as diferenças estão em MySQL e MariaDB.
Quando dá errado
Nos erros abaixo, o PHP mostra o rastro (stack trace) passando pelo console.php, porque é lá dentro que o comando roda. A linha que interessa é a mensagem do banco, no começo, e a linha #1, que aponta o seu arquivo.
no such column: Mochila (faltaram as aspas)
$ php erro-aspas.php
sql> SELECT * FROM produtos WHERE nome = Mochila
Fatal error: Uncaught PDOException: SQLSTATE[HY000]: General error: 1 no such column: Mochila in /home/ana/loja/console.php:10
Stack trace:
#0 /home/ana/loja/console.php(10): PDO->query('SELECT * FROM p...')
#1 /home/ana/loja/erro-aspas.php(4): sql('SELECT * FROM p...')
#2 {main}
thrown in /home/ana/loja/console.php on line 10
O que significa: sem aspas, Mochila é lido como nome de coluna, e não existe coluna com esse nome ("não existe a coluna Mochila").
Como consertar: texto sempre entre aspas simples: WHERE nome = 'Mochila'. Se a coluna realmente se chama assim, confira a grafia: a mesma mensagem aparece com SELECT nome, valor quando a coluna é preco (no such column: valor).
unrecognized token (aspa sem par)
$ php erro-faltou-aspa.php
sql> INSERT INTO produtos (nome, preco) VALUES ('Agenda, 25.00)
Fatal error: Uncaught PDOException: SQLSTATE[HY000]: General error: 1 unrecognized token: "'Agenda, 25.00)" in /home/ana/loja/console.php:10
Stack trace:
#0 /home/ana/loja/console.php(10): PDO->query('INSERT INTO pro...')
#1 /home/ana/loja/erro-faltou-aspa.php(4): sql('INSERT INTO pro...')
#2 {main}
thrown in /home/ana/loja/console.php on line 10
O que significa: "símbolo não reconhecido". A aspa abriu em 'Agenda e nunca fechou, então o banco leu tudo até o fim como um texto sem final.
Como consertar: feche a aspa: ('Agenda', 25.00). Cada texto, um par de aspas.
UNIQUE constraint failed: produtos.id
$ php erro-chave.php
sql> INSERT INTO produtos (id, nome, preco) VALUES (1, 'Agenda', 25.00)
Fatal error: Uncaught PDOException: SQLSTATE[23000]: Integrity constraint violation: 19 UNIQUE constraint failed: produtos.id in /home/ana/loja/console.php:10
Stack trace:
#0 /home/ana/loja/console.php(10): PDO->query('INSERT INTO pro...')
#1 /home/ana/loja/erro-chave.php(4): sql('INSERT INTO pro...')
#2 {main}
thrown in /home/ana/loja/console.php on line 10
O que significa: "a regra de valor único falhou em produtos.id". Já existe o id 1 (o Caderno), e a chave primária não se repete. É o banco protegendo os seus dados.
Como consertar: não informe o id: deixe o banco escolher (INSERT INTO produtos (nome, preco) VALUES (…)).
NOT NULL constraint failed: produtos.preco
$ php erro-nulo.php
sql> INSERT INTO produtos (nome) VALUES ('Agenda')
Fatal error: Uncaught PDOException: SQLSTATE[23000]: Integrity constraint violation: 19 NOT NULL constraint failed: produtos.preco in /home/ana/loja/console.php:10
Stack trace:
#0 /home/ana/loja/console.php(10): PDO->query('INSERT INTO pro...')
#1 /home/ana/loja/erro-nulo.php(4): sql('INSERT INTO pro...')
#2 {main}
thrown in /home/ana/loja/console.php on line 10
O que significa: a coluna preco é NOT NULL e não tem DEFAULT; o INSERT não a preencheu. (O estoque também ficou de fora, mas tem DEFAULT 0, então não reclamou.)
Como consertar: inclua a coluna e o valor: INSERT INTO produtos (nome, preco) VALUES ('Agenda', 25.00).
table produtos already exists
$ php erro-tabela-existe.php
sql> CREATE TABLE produtos (id INTEGER PRIMARY KEY, nome TEXT)
Fatal error: Uncaught PDOException: SQLSTATE[HY000]: General error: 1 table produtos already exists in /home/ana/loja/console.php:10
Stack trace:
#0 /home/ana/loja/console.php(10): PDO->query('CREATE TABLE pr...')
#1 /home/ana/loja/erro-tabela-existe.php(4): sql('CREATE TABLE pr...')
#2 {main}
thrown in /home/ana/loja/console.php on line 10
O que significa: "a tabela produtos já existe". O CREATE TABLE roda uma vez; depois, a tabela fica no arquivo loja.db.
Como consertar: use CREATE TABLE IF NOT EXISTS produtos (…), que só cria se ainda não existir. Para recomeçar do zero: DROP TABLE produtos (apaga tudo!) ou apague o arquivo loja.db.
Missão final: a loja vai ter cadastro de clientes. Crie a tabela clientes com as colunas id (chave primária), nome (texto obrigatório) e cidade (texto). Insira dois clientes: Ana, de Recife, e Bruno, de Belém. Por fim, mostre todos com SELECT. O resultado deve terminar com (2 linha(s)).
<?php require "console.php"; // 1. CREATE TABLE clientes (...) // 2. INSERT INTO clientes ... // 3. SELECT ...
<?php
// Um "console" de SQL para o editor: cria a lojinha da Ana na memória
// e mostra o resultado de cada comando como uma tabela.
// (Como ele funciona por dentro é assunto da próxima aula, sobre PDO.)
$pdo = new PDO("sqlite::memory:");
$pdo->exec("CREATE TABLE produtos (
id INTEGER PRIMARY KEY,
nome TEXT NOT NULL,
preco REAL NOT NULL,
estoque INTEGER NOT NULL DEFAULT 0
)");
$pdo->exec("INSERT INTO produtos (nome, preco, estoque) VALUES
('Caderno', 15.90, 30), ('Caneta azul', 2.50, 100), ('Mochila', 89.90, 5),
('Lápis', 1.20, 200), ('Estojo', 9.90, 12), ('Borracha', 0.90, 0)");
function sql(string $comando): void
{
global $pdo;
echo "sql> $comando\n";
$stmt = $pdo->query($comando);
if ($stmt->columnCount() === 0) { // não era um SELECT
if (preg_match('/^\s*(INSERT|UPDATE|DELETE)/i', $comando)) {
echo "OK, ", $stmt->rowCount(), " linha(s) afetada(s)\n\n";
} else {
echo "OK\n\n";
}
return;
}
$linhas = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (count($linhas) === 0) {
echo "(nenhuma linha)\n\n";
return;
}
$colunas = array_keys($linhas[0]);
$largura = [];
foreach ($colunas as $c) {
$largura[$c] = mb_strlen($c);
foreach ($linhas as $l) {
$largura[$c] = max($largura[$c], mb_strlen($l[$c] ?? "NULL"));
}
}
$celulas = fn($valores) => rtrim(implode(" | ", array_map(
fn($c) => mb_str_pad($valores[$c] ?? "NULL", $largura[$c]), $colunas)));
echo $celulas(array_combine($colunas, $colunas)), "\n";
echo implode("-+-", array_map(fn($c) => str_repeat("-", $largura[$c]), $colunas)), "\n";
foreach ($linhas as $l) {
echo $celulas($l), "\n";
}
echo "(", count($linhas), " linha(s))\n\n";
}
<?php
require "console.php";
sql("CREATE TABLE clientes (
id INTEGER PRIMARY KEY,
nome TEXT NOT NULL,
cidade TEXT
)");
sql("INSERT INTO clientes (nome, cidade) VALUES ('Ana', 'Recife'), ('Bruno', 'Belém')");
sql("SELECT * FROM clientes");
Referência
| Comando | Faz | Exemplo |
|---|---|---|
CREATE TABLE | Cria uma tabela | CREATE TABLE produtos (id INTEGER PRIMARY KEY, nome TEXT NOT NULL) |
CREATE TABLE IF NOT EXISTS | Cria se ainda não existir | |
DROP TABLE | Apaga a tabela inteira | DROP TABLE produtos |
INSERT INTO … VALUES | Insere linhas | INSERT INTO produtos (nome, preco) VALUES ('Lápis', 1.20) |
SELECT … FROM | Consulta | SELECT nome, preco FROM produtos |
WHERE | Filtra as linhas | WHERE preco < 10 AND estoque > 0 |
ORDER BY … [DESC] | Ordena | ORDER BY preco DESC |
LIMIT | Limita a quantidade | LIMIT 3 |
COUNT SUM MIN MAX AVG | Contas sobre as linhas | SELECT COUNT(*) FROM produtos |
AS | Apelido de coluna | SELECT COUNT(*) AS total |
UPDATE … SET … WHERE | Altera linhas | UPDATE produtos SET preco = 17.50 WHERE id = 1 |
DELETE FROM … WHERE | Apaga linhas | DELETE FROM produtos WHERE id = 5 |
PRIMARY KEY | Chave primária: valor único que identifica a linha | id INTEGER PRIMARY KEY |
NOT NULL / DEFAULT | Obrigatório / valor padrão | estoque INTEGER NOT NULL DEFAULT 0 |
Exercícios
1. Todas as colunas
Complete para mostrar todas as colunas de todos os produtos:
2. Os mais caros primeiro
Complete para ordenar do mais caro para o mais barato:
3. Só um produto
Complete para apagar só o produto de id 4:
4. Para que serve a chave primária?
5. O que acontece com UPDATE produtos SET estoque = 0?
6. Qual consulta busca o produto chamado Caderno?
7. Qual destas afirmações sobre o SQLite está correta?
Resumo
- Um banco de dados guarda dados de forma permanente, encontra rápido e confere regras. O seu programa faz pedidos a ele.
- Num banco relacional, os dados ficam em tabelas, com colunas (campos, com tipo) e linhas (registros).
- A chave primária (
id INTEGER PRIMARY KEY) identifica cada linha e nunca se repete. - SQL:
CREATE TABLEcria,INSERTinsere,SELECTconsulta,UPDATEaltera,DELETEapaga. WHEREescolhe as linhas; sem ele,UPDATEeDELETEatingem todas.- Texto entre aspas simples; números com ponto, sem aspas; comparação com
=. - SQLite é um arquivo, sem instalação; MySQL e PostgreSQL são servidores. O PHP fala com todos pelo PDO.
Próxima aula: Conectar com PDO, para entender o que o console.php faz e escrever os seus próprios programas com banco.