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 |
| 255 bytes | Iconos, datos muy pequeños |
| 65.535 bytes (~64 KB) | Imágenes pequeñas, thumbnails |
| 16.777.215 bytes (~16 MB) | Imágenes estándar, PDFs pequeños |
| 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
