Consultas SELECT en PHP

Recuperar datos de MySQL es la operación más frecuente en cualquier aplicación PHP. PDO ofrece una API flexible y segura para ejecutar consultas parametrizadas que protegen contra inyecciones SQL. En este artículo aprenderás a usar sentencias preparadas, los diferentes modos de fetch disponibles y cómo construir consultas dinámicas de forma segura.

Requisitos previos

Necesitas una conexión PDO configurada como se explicó en el artículo anterior. Crea la tabla de prueba con el siguiente SQL:

CREATE DATABASE IF NOT EXISTS tienda;
USE tienda;
 
CREATE TABLE productos (
    id INT AUTO_INCREMENT PRIMARY KEY,
    nombre VARCHAR(100) NOT NULL,
    categoria VARCHAR(50) NOT NULL,
    precio DECIMAL(10, 2) NOT NULL,
    stock INT DEFAULT 0,
    activo TINYINT(1) DEFAULT 1,
    fecha_creacion DATETIME DEFAULT CURRENT_TIMESTAMP
);
 
INSERT INTO productos (nombre, categoria, precio, stock) VALUES
('Laptop HP Pavilion', 'Computadoras', 12999.99, 25),
('Mouse Logitech MX Master', 'Periféricos', 1599.00, 150),
('Teclado Mecánico Corsair K70', 'Periféricos', 2299.50, 80),
('Monitor Samsung 27"', 'Monitores', 6499.00, 40),
('Auriculares Sony WH-1000XM5', 'Audio', 5999.99, 60),
('Webcam Logitech C920', 'Periféricos', 1299.00, 200),
('SSD Samsung 1TB', 'Almacenamiento', 1899.00, 120),
('RAM Corsair 16GB DDR5', 'Componentes', 1499.00, 90);

Código completo

Este ejemplo ejecuta una consulta parametrizada con sentencia preparada:

<?php
$pdo = new PDO(
    'mysql:host=localhost;dbname=tienda;charset=utf8mb4',
    'root', 'tu_contraseña',
    [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
);
 
$categoria = 'Periféricos';
$stmt = $pdo->prepare('SELECT id, nombre, precio, stock FROM productos WHERE categoria = ? ORDER BY precio DESC');
$stmt->execute([$categoria]);
 
$productos = $stmt->fetchAll();
 
echo "Productos en categoría \"{$categoria}\":\n";
foreach ($productos as $p) {
    echo "  [{$p['id']}] {$p['nombre']} - \${$p['precio']} ({$p['stock']} en stock)\n";
}
echo "Total: " . count($productos) . " productos\n";

Salida esperada:

Productos en categoría "Periféricos":
  [3] Teclado Mecánico Corsair K70 - $2299.50 (80 en stock)
  [2] Mouse Logitech MX Master - $1599.00 (150 en stock)
  [6] Webcam Logitech C920 - $1299.00 (200 en stock)
Total: 3 productos

Explicación paso a paso

El método prepare() compila la consulta SQL en el servidor MySQL y devuelve un objeto PDOStatement. Los placeholders ? marcan dónde se insertarán los valores. El método execute() envía los valores al servidor, que los inserta de forma segura en la consulta compilada.

PDO soporta dos tipos de placeholders. Los posicionales usan ? y los valores se pasan en orden. Los nombrados usan :nombre y permiten pasar los valores como un array asociativo, lo que hace el código más legible:

<?php
// Placeholders posicionales
$stmt = $pdo->prepare('SELECT * FROM productos WHERE precio BETWEEN ? AND ? AND categoria = ?');
$stmt->execute([1000, 5000, 'Periféricos']);
 
// Placeholders nombrados
$stmt = $pdo->prepare('SELECT * FROM productos WHERE precio BETWEEN :min AND :max AND categoria = :cat');
$stmt->execute([':min' => 1000, ':max' => 5000, ':cat' => 'Periféricos']);

Modos de fetch

PDO ofrece varios modos para recuperar los resultados, y cada uno devuelve los datos en un formato diferente:

<?php
$stmt = $pdo->prepare('SELECT id, nombre, precio FROM productos WHERE id = ?');
$stmt->execute([1]);
 
// FETCH_ASSOC - Array asociativo (recomendado)
$producto = $stmt->fetch(PDO::FETCH_ASSOC);
echo $producto['nombre'];  // Laptop HP Pavilion
 
// FETCH_OBJ - Objeto anónimo
$stmt->execute([1]);
$producto = $stmt->fetch(PDO::FETCH_OBJ);
echo $producto->nombre;    // Laptop HP Pavilion
 
// FETCH_NUM - Array numérico
$stmt->execute([1]);
$producto = $stmt->fetch(PDO::FETCH_NUM);
echo $producto[1];          // Laptop HP Pavilion
 
// FETCH_CLASS - Instancia de una clase
class Producto {
    public int $id;
    public string $nombre;
    public float $precio;
}
 
$stmt->execute([1]);
$stmt->setFetchMode(PDO::FETCH_CLASS, Producto::class);
$producto = $stmt->fetch();
echo $producto->nombre;    // Laptop HP Pavilion

Obtener todas las filas vs una sola

<?php
// fetchAll() - Obtener todas las filas de una vez
$stmt = $pdo->prepare('SELECT nombre, precio FROM productos WHERE activo = 1');
$stmt->execute();
$todos = $stmt->fetchAll();
echo "Total: " . count($todos) . " productos\n";
 
// fetch() - Obtener una fila a la vez
$stmt = $pdo->prepare('SELECT nombre, precio FROM productos WHERE id = ?');
$stmt->execute([1]);
$producto = $stmt->fetch();
 
if ($producto) {
    echo "Producto: {$producto['nombre']}\n";
} else {
    echo "Producto no encontrado\n";
}
 
// Iterar fila por fila (eficiente para grandes conjuntos)
$stmt = $pdo->prepare('SELECT nombre, precio FROM productos WHERE activo = 1');
$stmt->execute();
 
while ($fila = $stmt->fetch()) {
    echo "{$fila['nombre']}: \${$fila['precio']}\n";
}

Obtener un solo valor

Cuando solo necesitas un valor escalar, como un conteo o un máximo, puedes usar fetchColumn():

<?php
$stmt = $pdo->prepare('SELECT COUNT(*) FROM productos WHERE categoria = ?');
$stmt->execute(['Periféricos']);
$total = $stmt->fetchColumn();
echo "Total en Periféricos: {$total}\n";

Caso práctico

Veamos cómo construir una función de búsqueda con paginación y filtros:

<?php
function buscarProductos(PDO $pdo, array $filtros = []): array {
    $sql = 'SELECT id, nombre, categoria, precio, stock FROM productos WHERE activo = 1';
    $params = [];
 
    if (!empty($filtros['busqueda'])) {
        $sql .= ' AND nombre LIKE :busqueda';
        $params[':busqueda'] = '%' . $filtros['busqueda'] . '%';
    }
 
    if (!empty($filtros['categoria'])) {
        $sql .= ' AND categoria = :categoria';
        $params[':categoria'] = $filtros['categoria'];
    }
 
    if (isset($filtros['precio_min'])) {
        $sql .= ' AND precio >= :precio_min';
        $params[':precio_min'] = $filtros['precio_min'];
    }
 
    if (isset($filtros['precio_max'])) {
        $sql .= ' AND precio <= :precio_max';
        $params[':precio_max'] = $filtros['precio_max'];
    }
 
    // Contar total
    $sqlCount = preg_replace('/SELECT .+ FROM/', 'SELECT COUNT(*) FROM', $sql, 1);
    $stmtCount = $pdo->prepare($sqlCount);
    $stmtCount->execute($params);
    $total = (int) $stmtCount->fetchColumn();
 
    // Paginación
    $pagina = max(1, $filtros['pagina'] ?? 1);
    $porPagina = min(100, max(1, $filtros['por_pagina'] ?? 10));
    $offset = ($pagina - 1) * $porPagina;
 
    $sql .= " ORDER BY nombre ASC LIMIT {$porPagina} OFFSET {$offset}";
 
    $stmt = $pdo->prepare($sql);
    $stmt->execute($params);
 
    return [
        'datos' => $stmt->fetchAll(),
        'total' => $total,
        'pagina' => $pagina,
        'total_paginas' => (int) ceil($total / $porPagina)
    ];
}
 
// Uso
$resultado = buscarProductos($pdo, [
    'categoria' => 'Periféricos',
    'precio_min' => 1000,
    'precio_max' => 3000,
    'pagina' => 1,
    'por_pagina' => 10
]);
 
echo "Mostrando " . count($resultado['datos']) . " de {$resultado['total']} resultados\n";
echo "Página {$resultado['pagina']} de {$resultado['total_paginas']}\n";
 
foreach ($resultado['datos'] as $p) {
    echo "  {$p['nombre']} ({$p['categoria']}) - \${$p['precio']}\n";
}

Salida esperada:

Mostrando 3 de 3 resultados
Página 1 de 1
  Mouse Logitech MX Master (Periféricos) - $1599.00
  Teclado Mecánico Corsair K70 (Periféricos) - $2299.50
  Webcam Logitech C920 (Periféricos) - $1299.00

Manejo de errores

Las consultas pueden fallar por errores de sintaxis o tablas inexistentes:

<?php
function consultaSegura(PDO $pdo, string $sql, array $params = []): array {
    try {
        $stmt = $pdo->prepare($sql);
        $stmt->execute($params);
        return $stmt->fetchAll();
 
    } catch (PDOException $e) {
        $codigo = $e->errorInfo[1] ?? $e->getCode();
 
        switch ($codigo) {
            case 1146:
                echo "La tabla especificada no existe\n";
                break;
            case 1064:
                echo "Error de sintaxis en la consulta SQL\n";
                break;
            case 1054:
                echo "Una de las columnas especificadas no existe\n";
                break;
            default:
                echo "Error en la consulta [{$codigo}]: {$e->getMessage()}\n";
                break;
        }
 
        return [];
    }
}

Ahora que dominas las consultas SELECT en PHP, en el siguiente artículo aprenderás a insertar nuevos registros en MySQL.

Escrito por Eduardo Lázaro