Optimización de consultas SQL

En nuestro listado de productos con categoría, probablemente nos hará falta mostrar el listado de productos filtrado por categoría. En esta lección trabajaremos en esta característica y veremos cómo mejorar el tiempo de respuesta de la base de datos mediante el uso de claves foráneas e índices.

Esta lección incluye un video premium

Regístrate para ver este video y cientos de lecciones exclusivas.

Mira el código en GitHub: actual, resultado, comparación.

Dos tablas se relacionan por medio de un campo que es común para ambas. Por ejemplo, la tabla products contiene el campo category_id.

Aunque se puede establecer esta relación sin el uso de una clave foránea, cuando queramos filtrar productos por categoría, el manejador de base de datos utilizará algoritmos que no son muy eficientes y como resultado tardará más tiempo. Vamos a hacer una prueba de esto:

Abre la siguiente URL en el navegador: http://127.0.0.1:8000/productos?categoria=1 (cambia 127.0.0.1:8000 si estás usando otro puerto o has creado un virtual host).

Aún podremos ver los productos sin filtros, como podemos comprobar en la consulta SQL que se está ejecutando:

#
select `title`, `slug`, `category_id`, `image`
 from `products`
 limit 21
 offset 0

simplePaginate trae un resultado extra, esto se hace para comprobar si existe una página siguiente o no sin necesidad de contar los resultados.

De vuelta a routes/web.php modifiquemos la consulta para agregar la condición para filtrar nuestros productos por categoría:

<?php

$products = Product::query()
   ->select(['title', 'slug', 'category_id', 'image'])
   ->with('category:id,title,slug')
   ->when($request->input('categoria'), function ($q, $categoryId) {
       $q->where('category_id', $categoryId);
   })
   ->simplePaginate();

El método when nos permite modificar una consulta de manera condicional, como aprendimos en la lección Nuevos métodos disponibles en Fluent y Eloquent ORM en Laravel 5.2.

La ejecución de esta condición where requerirá que el manejador de base de datos haga una lectura de todas las filas por cada tabla participante (lo cual se conoce como Full Table Scans); además deberá ir comparando cada combinación para poder encontrar los registros que se deben incluir en el resultado final.

En la medida que aumente el número de registros de las tablas, aumentará proporcionalmente el tiempo requerido y esto podría crear un cuello de botella en tu aplicación.

Antes de pasar a la sección siguiente, recarga el navegador y fíjate el tiempo que toma esta consulta. Nota que el tiempo puede variar dependiendo del caché. Intenta hacer búsquedas con diferentes categorías. También puedes ejecutar la consulta directamente desde tu administrador de base de datos:

#
select `title`, `slug`, `category_id`, `image`
 from `products`
 where `category_id` = '45'
 limit 21
 offset 0

Agregar una clave foránea mediante el sistema de migraciones de Laravel

Para solucionar esto, simplemente basta con agregar una clave foránea al momento de crear la tabla mediante el sistema de migraciones de Laravel:

<?php

// ...

class CreateProductsTable extends Migration
{
    public function up()
    {
        Schema::create('products', function (Blueprint $table) {
            $table->bigIncrements('id');

            $table->unsignedBigInteger('category_id');
            $table->foreign('category_id')
                ->references('id')
                ->on('product_categories');

            // ...
        });
    }

    // ...
}

A partir de Laravel 7, esta sintaxis se simplifica bastante, como estudiamos en Nuevos métodos id() y foreignId() para las migraciones de Laravel 7.

Reiniciemos todas las migraciones y los seeders utilizando php artisan migrate:fresh --seed y algo de paciencia.

También es posible agregar una clave foránea a una tabla ya existente – en caso de que tu sistema ya se encuentre en producción:

Migración para agregar una clave foránea a una tabla ya existente

Vamos a deshacer el cambio previo a la migración para crear la tabla de productos y a re-ejecutar php artisan migrate:fresh --seed

Luego generemos una nueva migración en la consola:

#
php artisan make:migration add_foreign_key_to_products_table --table=products

Agreguemos el siguiente código al método up de la nueva migración:

<?php
//...
Schema::table('products', function (Blueprint $table) {
    $table->foreign('category_id')
        ->references('id')
        ->on('product_categories');
});

Y para el rollback en el método down agreguemos lo siguiente:

Schema::table('products', function (Blueprint $table) {
    $table->dropForeign(['category_id']);
});

Verifiquemos el SQL que será ejecutado utilizando el comando php artisan migrate --pretend:

#
alter table `products`
 add constraint `products_category_id_foreign`
 foreign key (`category_id`)
 references `product_categories` (`id`)

Ejecutemos la nueva migración mediante el comando php artisan migrate

Si regresamos al navegador y recargamos la página, podremos ver que las consultas sobre la tabla products con filtro de categoría ahora se ejecutan mucho más rápido. Esto es porque el manejador de base de datos utilizará el índice que se creó con la clave foránea y de esta manera no hace falta comparar toda la tabla para encontrar los registros correspondientes.

Con esto podemos comprobar que, además de proveer integridad en la información, las claves foráneas llevan implícitas la creación de índices que permiten optimizar los tiempos de respuestas de las consultas.

Más adelante volveremos a este tema de forma más avanzada.

Aprende más sobre bases de datos y SQL con nuestro libro digital disponible en Leanpub con 40% de descuento.

Ver más

Regístrate hoy en Styde y obtén acceso a todo nuestro contenido.

Lección anterior Tip de optimización con Eloquent #4: Utiliza paginación