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.

Você vai aprender
  • 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, UPDATE e DELETE.
  • 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 guardarProblema
Variáveis e arraysSomem quando o programa (ou a página) termina.
Arquivo CSV ou JSONPara 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 dadosGuarda 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:

idnomeprecoestoque
1Caderno15.9030
2Caneta azul2.50100
3Mochila89.905
A tabela produtos: 4 colunas e 3 linhas.
TermoNa planilhaNo exemplo
Banco de dadosO arquivo inteiro, com várias abasA loja da Ana (produtos, clientes, vendas…)
TabelaUma abaprodutos
Coluna (ou campo)Uma coluna, com um títulonome, preco
Linha (ou registro)Uma linhaO Caderno, com todos os seus dados
Chave primáriaUm número que não se repeteid

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.

E os bancos "não relacionais"?

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 produtos com 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 o null do PHP); NOT NULL proí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

TipoGuardaExemploNo PHP vira
INTEGERNúmero inteiro30, -2int
REALNúmero com casas15.90float
TEXTTexto'Caderno'string
BLOBDados binários (o conteúdo de um arquivo)uma imagemstring
NULLAusência de valorNULLnull

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 id ficou 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.
No PHP de verdade, os valores não vão escritos dentro do SQL

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 WHERESignificaRepare
=igualUm sinal só (no PHP seria ==)
<> ou !=diferente
< > <= >=menor, maior, menor ou igual, maior ou igual
AND, OR, NOTe, ou, nãoEm palavras, não && e ||
BETWEEN 2 AND 20entre 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 NULLestá 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 sem WHERE muda TODAS as linhas

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:

SQLiteMySQL / MariaDBPostgreSQL
Como funcionaUma biblioteca dentro do PHP; o banco é um arquivoUm servidor: um programa sempre rodando, que atende pela redeUm servidor, como o MySQL
InstalarNada: vem com o PHPInstalar e ligar o servidorInstalar e ligar o servidor
Driver do PDOpdo_sqlite (sqlite:)pdo_mysql (mysql:)pdo_pgsql (pgsql:)
Usuários e senhasNãoSimSim
Vários escrevendo ao mesmo tempoUm de cada vezSimSim
Tipos das colunasFlexíveis (aceita texto em coluna de número, salvo tabelas STRICT)RígidosRígidos e muito variados (datas, JSON, listas…)
Id automáticoINTEGER PRIMARY KEYINT AUTO_INCREMENT PRIMARY KEYSERIAL ou GENERATED ALWAYS AS IDENTITY
Onde apareceCelulares, navegadores, programas de computador, sites pequenos, testesA maioria das hospedagens PHP, WordPress, lojas virtuaisSistemas grandes, dados complexos, empresas
Bom para aprender?Ótimo: zero instalaçãoSim, depois do básicoSim, 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

ComandoFazExemplo
CREATE TABLECria uma tabelaCREATE TABLE produtos (id INTEGER PRIMARY KEY, nome TEXT NOT NULL)
CREATE TABLE IF NOT EXISTSCria se ainda não existir
DROP TABLEApaga a tabela inteiraDROP TABLE produtos
INSERT INTO … VALUESInsere linhasINSERT INTO produtos (nome, preco) VALUES ('Lápis', 1.20)
SELECT … FROMConsultaSELECT nome, preco FROM produtos
WHEREFiltra as linhasWHERE preco < 10 AND estoque > 0
ORDER BY … [DESC]OrdenaORDER BY preco DESC
LIMITLimita a quantidadeLIMIT 3
COUNT SUM MIN MAX AVGContas sobre as linhasSELECT COUNT(*) FROM produtos
ASApelido de colunaSELECT COUNT(*) AS total
UPDATE … SET … WHEREAltera linhasUPDATE produtos SET preco = 17.50 WHERE id = 1
DELETE FROM … WHEREApaga linhasDELETE FROM produtos WHERE id = 5
PRIMARY KEYChave primária: valor único que identifica a linhaid INTEGER PRIMARY KEY
NOT NULL / DEFAULTObrigatório / valor padrãoestoque INTEGER NOT NULL DEFAULT 0

Exercícios

1. Todas as colunas

Complete para mostrar todas as colunas de todos os produtos:

SELECT FROM produtos

2. Os mais caros primeiro

Complete para ordenar do mais caro para o mais barato:

SELECT nome, preco FROM produtos ORDER BY

3. Só um produto

Complete para apagar só o produto de id 4:

DELETE FROM produtos

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 TABLE cria, INSERT insere, SELECT consulta, UPDATE altera, DELETE apaga.
  • WHERE escolhe as linhas; sem ele, UPDATE e DELETE atingem 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.