Cómo resolver el Problema del N+1 en PHP y MySQL

Qué es el problema del N+1

Se trata de un problema de optimización de consultas a una base de datos.

Imagina que tienes una base de datos con dos tablas:

  • Users
  • PhoneNumbers

Donde cada usuario puede tener muchos números telefónicos (Relación 1:N).

Ahora, imagina que tienes que mostrar un listado de todos los usuarios junto con sus números de teléfono.

Una forma de resolverlo sería utilizar una consulta tipo:

SELECT * FROM users;

Y luego:

foreach ($users as $user) {
      $sql = "SELECT * FROM phone_numbers WHERE user_id = {$user->getId()};";

     ...
}

Lo que sucede aquí es que realizamos una consulta para obtener todos los usuarios y luego, una más por cada uno.

De ahí que el número total de consultas que se realizan es N (Número de usuarios) + 1.

Esto puede representar un gran problema cuando N es grande… especialmente cuando el tiempo que tarda cada consulta es significativo (Por ejemplo porque la base de datos está en un servidor remoto y hay que contar con los tiempos de transferencia de red).

Solución basada en JOIN

Una primera solución sería utilizar un join:

SELECT * FROM users u INNER JOIN phone_numbers p ON p.user_id = u.id;

Aquí el problema es que el resultado tendrá mucha información duplicada (Todos los datos del usuario estarán repetidos en cada fila), lo cual puede traernos inconvenientes con la memoria.

Solución de dos consultas

Una solución más eficiente se basa en la realización de dos consultas.

Los pasos son los siguientes:

  1. Buscar los objetos principales
  2. Obtener los Ids de dichos objetos
  3. Buscar los objetos relacionados para aquellos Ids obtenidos en 2
  4. Mostrar el resultado combinado
SELECT * FROM user;
$ids = array_map(function(User $u) { return $u->getId(); }, $users );
$phone_numbers = $db->query("SELECT * FROM phone_numbers WHERE user_id IN ('.implode(', ', $ids).')");

foreach ( $users as $user ) {
     $str = $user->getName() . implode(',', array_filter($phone_numbers, function($p) use ($user) {
              return $p['user_id'] == $user->getId();
     }));
}

En esta solución la cantidad de consultas realizadas siempre es fija: 2.

Puede parecer algo más laborioso del lado del PHP, pero ten en cuenta que los tiempos de procesamiento siempre son significativamente inferiores respecto de los de comunicación vía red u otro tipo de operaciones de entrada/salida, con lo cual, vale la pena el esfuerzo extra 😉

mchojrin

Por mchojrin

Ayudo a desarrolladores PHP a afinar sus habilidades técnicas y avanzar en sus carreras

¿Te quedó alguna duda? Publica aca tu pregunta

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.