Actualizar el stock de miles de productos sin tumbar MySQL
Un compañero cargaba 800.000 productos desde Python y quería actualizar stock y precio a diario. Rekire, Gustavo y un experto en bases de datos del grupo coincidieron: solo lo que cambia, nunca un UPDATE por línea y, si hay módulos que dependen del stock, con la clase StockAvailable. Aprendes las dos vías y sus trampas.
Un compañero de la comunidad montaba la carga de un catálogo de 800.000 productos, con unas ocho características cada uno, y buscaba la mejor forma de actualizar a diario stock y precios. Rekire contó que mantiene un sistema que actualiza a diario un millón de productos. La conclusión del hilo: actualiza solo lo que ha cambiado, en bloque y no fila a fila, y decide si necesitas que salten los hooks de PrestaShop.
El error que tumba el servidor
Un especialista en bases de datos del grupo lo dijo claro: si haces un foreach con un UPDATE por cada línea, revientas. El problema no es MySQL en sí, sino lanzar consultas sin parar: las tablas quedan bloqueadas todo el rato, el búfer no llega a trabajar y la tienda se arrastra mientras dura el proceso. Rekire lo vivió en un cliente en plena transición: los actualizadores antiguos cargaban tanto MySQL que había momentos en que todo iba lento.
Las reglas que salieron:
- Compara antes de escribir. Detecta qué productos han cambiado y toca solo esos. El sistema de Rekire hace eso: altas, bajas y modificaciones, y nada más.
- Agrupa. Muchos valores por sentencia o una tabla temporal y un único
UPDATEconJOIN, en lugar de miles de sentencias sueltas. - Lee por tramos. Gustavo se encontró con lo mismo al exportar todo el catálogo a CSV: meterlo todo en un array dispara la memoria. Lo resolvió leyendo los productos por tramos.
- Ejecútalo por consola, con cron, y no desde una URL que pueda cortar el servidor web a los pocos minutos.
Vía 1: la clase StockAvailable (si tienes módulos que escuchan el stock)
Gustavo prefiere no tocar la base de datos directamente. Usa la clase de stock de PrestaShop, y no Product::update(), que tarda muchísimo más. Antes de escribir comprueba el stock actual y solo actualiza si no coincide:
$actual = (int) StockAvailable::getQuantityAvailableByProduct($idProduct, $idProductAttribute);
if ($actual !== $nueva) {
StockAvailable::setQuantity($idProduct, $idProductAttribute, $nueva);
}La ventaja: se ejecutan los hooks de cambio de cantidad. Si tienes el módulo de alertas de stock (el aviso de «vuelve a estar disponible»), un conector de marketplace o cualquier módulo que reaccione al stock, por esta vía se enteran. Por SQL directo, no.
Vía 2: SQL en bloque (si nada depende de los hooks)
Es mucho más rápida. Cargas el stock nuevo en una tabla temporal y haces un único UPDATE que solo toca lo que difiere:
CREATE TEMPORARY TABLE stock_nuevo (
id_product INT UNSIGNED NOT NULL,
id_product_attribute INT UNSIGNED NOT NULL,
quantity INT NOT NULL,
PRIMARY KEY (id_product, id_product_attribute)
);
-- aquí cargas los datos, con muchos valores por INSERT
UPDATE ps_stock_available sa
JOIN stock_nuevo n
ON n.id_product = sa.id_product
AND n.id_product_attribute = sa.id_product_attribute
SET sa.quantity = n.quantity,
sa.physical_quantity = n.quantity + sa.reserved_quantity
WHERE sa.id_shop = 1
AND sa.quantity <> n.quantity;Tres trampas de esta vía:
- Cantidad física y reservada. Desde 1.7 la tabla guarda también
physical_quantityyreserved_quantity(lo que está en pedidos sin servir). Si solo cambiasquantity, descuadras lo físico; por eso el ejemplo las actualiza juntas. - Multitienda. Si compartes stock entre tiendas, las filas van con
id_shop = 0y el grupo de tiendas; adapta el filtro. - La fila del producto padre. En productos con combinaciones, la fila con
id_product_attribute = 0guarda la suma de todas. Por SQL tienes que recalcularla tú.
UPDATE ps_stock_available p
JOIN (
SELECT id_product, id_shop, SUM(quantity) AS total
FROM ps_stock_available
WHERE id_product_attribute <> 0
GROUP BY id_product, id_shop
) c ON c.id_product = p.id_product AND c.id_shop = p.id_shop
SET p.quantity = c.total,
p.physical_quantity = c.total + p.reserved_quantity
WHERE p.id_product_attribute = 0;Cambia ps_ por el prefijo de tus tablas y prueba siempre en una copia antes de lanzarlo en producción.
Y el precio, aparte
El precio va en ps_product y también en ps_product_shop, que es la que lee la tienda. Si lo actualizas por SQL, actualiza las dos. Y recuerda que los hooks de producto no saltarán: un feed o un buscador externo no se enterará hasta su siguiente lectura completa.
Si tu proveedor tiene API, en Rekire hacemos conectores que traen stock, tarifas e imágenes y solo envían los cambios, como el conector de Makito.
Esto salió en @PrestashopEsp, la mayor comunidad PrestaShop de España en Telegram. Ver la conversación