Programação Por Equipe DevBras

Capa do artigo: Uma consulta por tela: SELECT no MySQL com só as colunas que você mostra

Uma consulta por tela: SELECT no MySQL com só as colunas que você mostra

Escreva a consulta a partir da tela: colunas nomeadas, LIMIT, filtro por parâmetro, ORDER BY por lista branca e um índice que combine. Menos dados, menos vazamento.

A home do blog da escola de música leva 2,3 segundos para abrir. Ela mostra dez cards com título, data e nome do autor. A consulta por trás é SELECT * FROM artigos JOIN autores ..., que traz o corpo inteiro de cada artigo (40 KB em média) e, de brinde, o hash de senha do autor. São uns 400 KB saindo do banco para desenhar menos de 2 KB de HTML. A resposta curta: comece pela tela, não pela tabela; liste no SELECT só as colunas que aparecem, filtre e limite no banco, passe os valores como parâmetro e ordene apenas por colunas de uma lista que o código conhece.

Este guia mostra como escrever a consulta de cada tela e como confirmar com EXPLAIN que ela está barata. Os riscos de segurança do asterisco são o tema de SELECT * traz coluna que você não queria mostrar, e o problema da consulta sem limite está em consulta que funciona com dez linhas trava com dez mil.

Por que o asterisco pesa mais do que parece

Cada coluna trazida é lida do disco, transferida pela rede até o PHP e guardada na memória do processo, mesmo que o template nunca a use. Colunas de texto longo, como LONGTEXT com o corpo do artigo, são as mais caras. Em tabela pequena ninguém percebe. Com 50 mil artigos e a home sendo aberta mil vezes por hora, o banco passa o dia lendo texto que vai direto para o lixo.

Há também um efeito silencioso com JOIN. As tabelas artigos e autores têm, ambas, uma coluna id. Com SELECT * e PDO::FETCH_ASSOC, o array só pode ter uma chave id, e a última coluna com esse nome vence. O link "ler mais" passa a apontar para o id do autor, não do artigo. É o tipo de bug que só aparece quando os números deixam de coincidir.

Como escrever a consulta a partir da tela

  1. Abra o template e liste cada dado que ele imprime.
  2. Escreva o SELECT com essas colunas, com prefixo da tabela e apelido quando o nome se repete.
  3. Coloque o filtro e o LIMIT na consulta, nunca no PHP depois de buscar.
  4. Passe todo valor variável como parâmetro.
  5. Se o usuário escolhe a ordenação, traduza a escolha por um array de opções permitidas.
  6. Rode EXPLAIN e crie o índice que a consulta pede.

A lista da home

O card mostra título, data, nome do autor e link. Isso dá cinco colunas:

SELECT a.id, a.slug, a.titulo, a.publicado_em, u.nome AS autor
FROM artigos a
JOIN autores u ON u.id = a.autor_id
WHERE a.status = 'publicado'
ORDER BY a.publicado_em DESC
LIMIT 10 OFFSET 0;

Nenhum corpo de artigo, nenhum dado de login do autor. O apelido autor evita a colisão de nomes e deixa o template claro.

A página do artigo

O detalhe precisa do corpo, mas de um artigo só, buscado pelo slug:

SELECT a.id, a.titulo, a.corpo, a.publicado_em, u.nome AS autor
FROM artigos a
JOIN autores u ON u.id = a.autor_id
WHERE a.slug = ? AND a.status = 'publicado'
LIMIT 1;

Duas telas, duas consultas. Parece mais trabalho do que uma função genérica buscarArtigos() que serve para tudo, mas é justamente a função genérica que acaba trazendo tudo para todo mundo.

Parâmetros, inclusive no LIMIT

No PHP com PDO e MySQL, filtro e paginação entram como parâmetros. Há um detalhe com o LIMIT: com a emulação de prepared statements ligada (o padrão do driver MySQL no PDO), um valor passado como texto vira '10' entre aspas, e o MySQL recusa a consulta com erro de sintaxe. Use bindValue com PDO::PARAM_INT:

<?php
$porPagina = 10;
$pagina = max(1, (int) ($_GET['pagina'] ?? 1));

$ordens = [
    'recentes' => 'a.publicado_em DESC',
    'antigos'  => 'a.publicado_em ASC',
    'titulo'   => 'a.titulo ASC',
];
$ordem = $ordens[$_GET['ordem'] ?? ''] ?? $ordens['recentes'];

$sql = "SELECT a.id, a.slug, a.titulo, a.publicado_em, u.nome AS autor
        FROM artigos a
        JOIN autores u ON u.id = a.autor_id
        WHERE a.status = 'publicado'
        ORDER BY $ordem
        LIMIT :limite OFFSET :inicio";

$stmt = $pdo->prepare($sql);
$stmt->bindValue(':limite', $porPagina, PDO::PARAM_INT);
$stmt->bindValue(':inicio', ($pagina - 1) * $porPagina, PDO::PARAM_INT);
$stmt->execute();
$artigos = $stmt->fetchAll(PDO::FETCH_ASSOC);

A ordenação é o único pedaço interpolado, e só porque nome de coluna não pode ser parâmetro. Por isso ela nunca vem direto de $_GET: a pessoa escolhe uma chave, e o código entrega um trecho de SQL que ele mesmo escreveu. Qualquer outro valor cai no padrão. Para páginas muito profundas, o OFFSET fica lento e pode pular itens quando há inserções; a alternativa por cursor está em paginação que não pula.

Como confirmar com EXPLAIN

Coloque EXPLAIN antes da consulta da lista e olhe três colunas do resultado para a tabela artigos: type, key e rows. Se type for ALL e key vier vazio, o MySQL está lendo a tabela inteira. Se a coluna Extra mostrar Using filesort, ele está ordenando na mão depois de ler. Um índice composto que combine filtro e ordenação resolve os dois:

CREATE INDEX idx_artigos_status_publicado ON artigos (status, publicado_em);

Rode o EXPLAIN de novo. O esperado é ver o índice novo em key e um número em rows muito menor que o total da tabela. No caso da escola de música, a home caiu de 2,3 segundos para algo abaixo de 100 milissegundos com essas duas mudanças: colunas nomeadas e índice. Esses números são do exemplo; meça os seus antes e depois.

O que nunca sai na consulta de vitrine

Hash de senha, token de redefinição, CPF completo, e-mail de quem não é público. Se a tela não mostra, a consulta não busca. A regra protege contra o dia em que alguém faz json_encode($artigos) para uma API nova e publica tudo o que veio do banco.

Erros comuns

  • SELECT * na lista. Texto longo e dados sensíveis viajam em toda requisição sem nunca aparecer.
  • Asterisco com JOIN. Colunas com o mesmo nome se sobrescrevem no array e o link aponta para o registro errado.
  • Buscar tudo e paginar no PHP. O banco entrega dez mil linhas para a página mostrar dez.
  • Interpolar o ORDER BY vindo da URL. Abre injeção de SQL num lugar onde parâmetro não funciona.
  • Passar o LIMIT como texto no PDO. A consulta falha com erro de sintaxe por causa das aspas.
  • Uma função genérica para todas as telas. Ela acaba trazendo a união de todas as colunas que alguma tela já precisou.

Checklist

  • Cada tela tem uma consulta com colunas nomeadas, correspondendo ao que o template imprime.
  • Colunas com nome repetido em JOIN têm apelido.
  • Filtro e LIMIT estão no SQL, com valores como parâmetro.
  • LIMIT e OFFSET são ligados com PDO::PARAM_INT.
  • A ordenação vem de uma lista branca no código.
  • O EXPLAIN da consulta principal usa índice.
  • Nenhuma consulta de vitrine traz senha, token ou documento.

Abra a consulta da página mais acessada do seu sistema, ao lado do template dela, e risque cada coluna que o HTML não imprime. Quantas sobraram, e alguma das riscadas era um dado que você não gostaria de ver numa resposta de API?

Avalie este artigo

Referência: http://pt-br.html.net/tutorials/php/lesson20.php