Guardar y extraer imágenes en MySQL con PHP 8: BLOB, PDO y alternativas modernas

Artículo original de junio de 2005. Actualizado en mayo de 2026 por David Carrero. El original usaba mysql_connect() (eliminado en PHP 7.0, diciembre de 2015) y imagecreatefromgif() con salida de buffer para convertir la imagen. Esta versión usa PHP 8 con PDO y el flujo estándar de subida de archivos con move_uploaded_file().

¿Tiene sentido guardar imágenes en MySQL?

La primera pregunta que hay que responder antes de escribir una línea de código es si realmente conviene almacenar las imágenes en la base de datos. No es una respuesta única para todos los casos, así que conviene entender los escenarios donde tiene sentido y donde no.

Criterio

BLOB en MySQL

Ficheros en disco / CDN

Facilidad de backup

El backup de la BD incluye las imágenes automáticamente

Requiere backup separado del sistema de ficheros

Transacciones ACID

La imagen y sus metadatos siempre están sincronizados

Posible inconsistencia si falla la BD pero se guardó el fichero

Rendimiento

Más lento: la imagen pasa por MySQL y PHP antes de llegar al navegador

El servidor web sirve el fichero directamente, sin PHP

Escalabilidad

La BD crece rápido; no funciona bien con imágenes grandes o muchas

Compatible con CDN y almacenamiento en la nube (S3, etc.)

Caché HTTP

Requiere cabeceras manuales en PHP

El servidor web gestiona ETags, Last-Modified, etc. de forma nativa

Casos de uso típico

Avatares pequeños, miniaturas, imágenes asociadas a registros críticos

Fotos de alta resolución, galerías, assets de sitio

Para imágenes pequeñas y en número limitado (avatares de usuario, logos de empresa, miniaturas de producto), guardarlas en MySQL simplifica el backup y elimina la posibilidad de que exista un registro sin imagen. Para galerías grandes o imágenes de alta resolución, el sistema de ficheros o un servicio de almacenamiento en la nube es la opción correcta.

Tipos BLOB en MySQL

MySQL ofrece cuatro variantes de BLOB según el tamaño máximo que necesites:

Tipo

Tamaño máximo

Uso habitual

TINYBLOB

255 bytes

Iconos, datos muy pequeños

BLOB

65.535 bytes (~64 KB)

Imágenes pequeñas, thumbnails

MEDIUMBLOB

16.777.215 bytes (~16 MB)

Imágenes estándar, PDFs pequeños

LONGBLOB

4.294.967.295 bytes (~4 GB)

Vídeos, archivos grandes

Para imágenes JPEG o PNG normales, MEDIUMBLOB es la elección habitual. Recuerda también ajustar max_allowed_packet en my.cnf si las imágenes superan el valor por defecto (por defecto 64 MB en MySQL 8).

Crear la base de datos y la tabla

CREATE DATABASE IF NOT EXISTS bd_galeria
  CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

USE bd_galeria;

CREATE TABLE banners (
    id          INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    nombre      VARCHAR(255) NOT NULL,
    descripcion TEXT,
    mime_type   VARCHAR(50) NOT NULL,
    tamanio     INT UNSIGNED NOT NULL,
    imagen      MEDIUMBLOB NOT NULL,
    fecha       DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    INDEX (fecha)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

El campo mime_type guarda el tipo MIME real de la imagen (image/jpeg, image/png, image/webp). Lo necesitamos para enviar la cabecera Content-Type correcta cuando el navegador pide la imagen.

Subir y guardar una imagen en MySQL con PHP 8

El formulario HTML es el mismo que en cualquier subida de archivos: enctype="multipart/form-data" y un campo type="file":

<form method="post" action="guardar.php" enctype="multipart/form-data">
  <label>
    Descripción:
    <input type="text" name="descripcion" maxlength="200">
  </label>
  <label>
    Imagen (JPG, PNG, WebP, GIF — máx. 2 MB):
    <input type="file" name="imagen" accept="image/*">
  </label>
  <button type="submit">Guardar</button>
</form>
<?php
// guardar.php — PHP 8, PDO

declare(strict_types=1);

define('MAX_BYTES', 2 * 1024 * 1024); // 2 MB

$tiposPermitidos = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'];

// ??? Validaciones ?????????????????????????????????????????????????????????????
$archivo = $_FILES['imagen'] ?? null;

if (!$archivo || $archivo['error'] !== UPLOAD_ERR_OK) {
    die('Error al subir el archivo.');
}

if ($archivo['size'] > MAX_BYTES) {
    die('La imagen supera el tamaño máximo de 2 MB.');
}

// Validar tipo MIME real (no confiar en la extensión)
$finfo    = new finfo(FILEINFO_MIME_TYPE);
$mimeReal = $finfo->file($archivo['tmp_name']);

if (!in_array($mimeReal, $tiposPermitidos, true)) {
    die("Tipo de archivo no permitido: $mimeReal");
}

// ??? Leer el contenido binario ?????????????????????????????????????????????????
$contenido = file_get_contents($archivo['tmp_name']);
if ($contenido === false) {
    die('No se pudo leer el archivo temporal.');
}

// ??? Insertar en MySQL ?????????????????????????????????????????????????????????
$dsn = 'mysql:host=localhost;dbname=bd_galeria;charset=utf8mb4';
$pdo = new PDO($dsn, 'usuario', 'contraseña', [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);

$stmt = $pdo->prepare(
    "INSERT INTO banners (nombre, descripcion, mime_type, tamanio, imagen)
     VALUES (:nombre, :desc, :mime, :tam, :img)"
);

// PDO::PARAM_LOB indica que el valor es un dato binario largo
$stmt->bindValue(':nombre', $archivo['name'],  PDO::PARAM_STR);
$stmt->bindValue(':desc',   $_POST['descripcion'] ?? '', PDO::PARAM_STR);
$stmt->bindValue(':mime',   $mimeReal,         PDO::PARAM_STR);
$stmt->bindValue(':tam',    $archivo['size'],  PDO::PARAM_INT);
$stmt->bindParam(':img',    $contenido,        PDO::PARAM_LOB);
$stmt->execute();

echo 'Imagen guardada con ID: ' . $pdo->lastInsertId();
?>

La diferencia clave respecto al código de 2005 es PDO::PARAM_LOB en el bindParam del dato binario. PDO gestiona correctamente el escapado del contenido binario sin necesitar addslashes() ni mysql_escape_string(), funciones que podían corromper datos binarios en ciertos conjuntos de caracteres.

Extraer y servir la imagen desde MySQL

<?php
// imagen.php?id=42
// Sirve la imagen al navegador con el Content-Type correcto

declare(strict_types=1);

$id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
if (!$id || $id <= 0) {
    http_response_code(400);
    exit('ID no válido.');
}

$dsn = 'mysql:host=localhost;dbname=bd_galeria;charset=utf8mb4';
$pdo = new PDO($dsn, 'usuario', 'contraseña', [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);

$stmt = $pdo->prepare(
    "SELECT nombre, mime_type, tamanio, imagen FROM banners WHERE id = ?"
);
$stmt->execute([$id]);
$fila = $stmt->fetch(PDO::FETCH_ASSOC);

if (!$fila) {
    http_response_code(404);
    exit('Imagen no encontrada.');
}

// Cabeceras de caché básica: el navegador puede guardar la imagen 1 hora
header('Content-Type: '   . $fila['mime_type']);
header('Content-Length: ' . $fila['tamanio']);
header('Cache-Control: max-age=3600, public');
header('ETag: "' . md5((string)$id) . '"');

echo $fila['imagen'];
exit;
?>

Para incrustar la imagen en HTML usa simplemente:

<img src="imagen.php?id=42" alt="Banner promocional">

Listar las imágenes del repositorio

<?php
// listar.php — muestra la galería con miniaturas
$pdo = new PDO('mysql:host=localhost;dbname=bd_galeria;charset=utf8mb4',
               'usuario', 'contraseña',
               [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);

$stmt = $pdo->query(
    "SELECT id, nombre, descripcion, mime_type,
            tamanio, fecha
     FROM banners
     ORDER BY fecha DESC"
);

echo '<ul class="galeria">';
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $img) {
    $tamKB = number_format($img['tamanio'] / 1024, 1);
    echo '<li>';
    echo '<img src="imagen.php?id=' . (int)$img['id'] . '" alt="' . htmlspecialchars($img['descripcion']) . '" width="120">';
    echo '<p>' . htmlspecialchars($img['nombre']) . " — $tamKB KB</p>";
    echo '</li>';
}
echo '</ul>';
?>

Alternativa: servir imágenes con URI de datos (para imágenes muy pequeñas)

Para imágenes pequeñas (iconos, avatares de menos de ~10 KB) se pueden embeber directamente en el HTML como URI de datos, evitando una petición HTTP adicional:

<?php
// En la consulta de listado, recupera también el campo imagen
$b64  = base64_encode($img['imagen']);
$mime = $img['mime_type'];
echo '<img src="data:' . $mime . ';base64,' . $b64 . '" alt="...">';
?>

Solo recomendable para imágenes muy pequeñas. Para imágenes normales, el script imagen.php separado permite que el navegador las cachee.

Para almacenar otros tipos de archivo binario (PDFs, ZIPs, documentos) usando el mismo principio BLOB, revisa Repositorio de archivos binarios en MySQL con PHP 8. Para la subida segura de archivos al sistema de ficheros, consulta Trabajando con ficheros en PHP 8.

Imagen: Pexels / Vladyslav Dukhin

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP