🔍 Optimizacion de queries SQL — rendimiento avanzado

Actualizado: 2024-12-01

Las queries de base de datos son el cuello de botella numero uno en PrestaShop. Con las tecnicas correctas se puede reducir el tiempo de carga de una pagina de 2-3 segundos a menos de 300ms.

#Diagnosticar queries lentas

Activar log de queries SQL lentas
php
<?php

// ── Activar _PS_DEBUG_SQL_ para ver todas las queries ──
// En config/config.inc.php (solo en desarrollo):
define('_PS_DEBUG_SQL_', true);
// Las queries aparecen en el Symfony Profiler y en la consola

// ── Medir el tiempo de una query especifica ──
$start  = microtime(true);
$result = Db::getInstance()->executeS('SELECT ...');
$elapsed = microtime(true) - $start;
if ($elapsed > 0.1) { // > 100ms
    PrestaShopLogger::addLog(
        'Slow query: ' . round($elapsed * 1000, 2) . 'ms',
        2, null, 'MyModule'
    );
}

// ── MySQL Slow Query Log ──
// En my.cnf:
// slow_query_log = 1
// slow_query_log_file = /var/log/mysql/slow.log
// long_query_time = 0.5  # Queries > 500ms

// ── EXPLAIN para analizar el plan de ejecucion ──
$explain = Db::getInstance()->executeS(
    'EXPLAIN SELECT p.id_product, pl.name FROM ps_product p
    LEFT JOIN ps_product_lang pl ON p.id_product = pl.id_product AND pl.id_lang = 1
    WHERE p.active = 1'
);
// Buscar: type='ALL' (table scan), Extra='Using filesort' (ordenacion lenta)

#Patron N+1 — deteccion y solucion

Detectar y resolver el problema N+1
php
<?php

// ═══════════════════════════════════════════════
// ❌ N+1 QUERIES — Patron comun en hooks de listado
// ═══════════════════════════════════════════════

public function hookDisplayProductListItem(array $params): string
{
    $product = $params['product'];

    // ❌ N queries — una por producto en el listado
    $reviews = Db::getInstance()->executeS(
        'SELECT * FROM ps_mymodule_review WHERE id_product = ' . (int) $product['id_product']
    );

    // Si el listado tiene 40 productos = 40 queries adicionales!
    return $this->display(__FILE__, 'product-reviews.tpl');
}

// ══════════════════════════════════════════════
// ✅ SOLUCION — Pre-cargar datos en un solo batch
// ══════════════════════════════════════════════

public function hookActionProductListOverride(array &$params): void
{
    // Este hook recibe TODOS los productos del listado a la vez
    $products = $params['products'];
    $ids      = array_column($products, 'id_product');

    // UNA sola query para todos los productos
    $reviewsRaw = Db::getInstance()->executeS(
        'SELECT id_product, COUNT(*) as count, AVG(rating) as avg_rating
        FROM ps_mymodule_review
        WHERE id_product IN (' . implode(',', array_map('intval', $ids)) . ')
        GROUP BY id_product'
    ) ?: [];

    // Indexar por id_product para acceso O(1)
    $reviews = array_column($reviewsRaw, null, 'id_product');

    // Asignar a cada producto
    foreach ($params['products'] as &$product) {
        $product['mymodule_reviews'] = $reviews[$product['id_product']] ?? [
            'count' => 0, 'avg_rating' => 0
        ];
    }
}

#Indices efectivos

Crear indices compuestos efectivos
sql
-- ── Regla de los 3 pasos para indices ──
-- 1. Indices en columnas WHERE
-- 2. Indices en columnas JOIN
-- 3. Indices en columnas ORDER BY

-- ── Indice simple ──
ALTER TABLE `ps_mymodule_item` ADD INDEX `idx_active` (`active`);

-- ── Indice compuesto (columnas de mayor a menor selectividad) ──
ALTER TABLE `ps_mymodule_item`
    ADD INDEX `idx_active_category_sort` (`active`, `id_category`, `sort_order`);
-- Cubre la query: WHERE active=1 AND id_category=5 ORDER BY sort_order

-- ── Indice de cobertura (covering index) ──
-- Incluye todas las columnas que necesita la query
-- Evita acceso a la tabla principal (Using index en EXPLAIN)
ALTER TABLE `ps_mymodule_item`
    ADD INDEX `idx_cover` (`active`, `id_category`, `price`, `id_mymodule_item`);
-- Esta query NO necesita acceder a la tabla:
-- SELECT id_mymodule_item, price FROM ps_mymodule_item
-- WHERE active=1 AND id_category=5

-- ── Ver indices actuales ──
SHOW INDEX FROM `ps_mymodule_item`;

-- ── Eliminar indice duplicado o innecesario ──
ALTER TABLE `ps_mymodule_item` DROP INDEX `idx_redundante`;

#Paginacion eficiente

Paginacion con cursor vs OFFSET
php
<?php

// ── Paginacion clasica con OFFSET (lenta en paginas altas) ──
// OFFSET 10000 = MySQL lee 10000 filas y descarta las primeras
$page   = 500; // Pagina 500
$limit  = 20;
$offset = ($page - 1) * $limit; // OFFSET 9980 — muy lento

$sql = 'SELECT * FROM ps_product WHERE active=1 ORDER BY id_product LIMIT ' . $limit . ' OFFSET ' . $offset;

// ── Paginacion con cursor (rapida incluso en paginas altas) ──
// En lugar de OFFSET, usar el ultimo ID de la pagina anterior
$lastId = (int) Tools::getValue('last_id', 0);

$query = (new DbQuery())
    ->select('id_product, reference, price')
    ->from('product')
    ->where('active = 1')
    ->where($lastId > 0 ? 'id_product > ' . $lastId : '1=1')
    ->orderBy('id_product ASC')
    ->limit($limit);

$products = Db::getInstance()->executeS($query) ?: [];
$nextCursor = !empty($products) ? end($products)['id_product'] : null;

// El cliente pasa ?last_id={nextCursor} para la siguiente pagina
// Funciona en tiempo constante independientemente de la pagina

#Queries de solo lectura con esclavo

Usar replica de lectura para queries pesadas
php
<?php

// ── Si hay un servidor esclavo (replica) configurado en PS ──
// BO → Parametros Avanzados → Base de datos → Servidores de la BD

// Obtener la instancia de lectura (esclavo si disponible)
$dbSlave  = Db::getInstance(_PS_USE_SQL_SLAVE_);

// Usar el esclavo para queries de lectura pesadas
$products = $dbSlave->executeS(
    (new DbQuery())
        ->select('p.id_product, pl.name, p.price')
        ->from('product', 'p')
        ->leftJoin('product_lang', 'pl', 'p.id_product = pl.id_product AND pl.id_lang = 1')
        ->where('p.active = 1')
        ->orderBy('p.date_add DESC')
        ->limit(100)
);

// ── SIEMPRE usar el maestro para escrituras ──
$dbMaster = Db::getInstance(); // Sin flag = maestro
$dbMaster->insert('mymodule_log', ['message' => pSQL('Query completed')]);

// ── Si no hay esclavo, getInstance() devuelve el maestro de todas formas ──
// El codigo es el mismo — no hay que detectar si hay esclavo
Descargar en Markdown Pensado para pegar en ChatGPT, Claude u otra IA. Incluye solo el contenido de esta pagina.