¿Cuál es el problema de "N + 1 selecciona" en ORM (mapeo relacional de objetos)?

1598

El "problema de selección de N + 1" generalmente se expresa como un problema en las discusiones de mapeo relacional de objetos (ORM), y entiendo que tiene algo que ver con tener que hacer muchas consultas a la base de datos para algo que parece simple en el objeto mundo.

¿Alguien tiene una explicación más detallada del problema?

Lars A. Brekken
fuente
2
Este es un gran enlace con una buena explicación sobre cómo entender el problema n + 1 . También cubre las soluciones para contrarrestar este problema: architects.dzone.com/articles/how-identify-and-resilve-n1
aces.
Hay algunas publicaciones útiles que hablan sobre este problema y la posible solución. Problemas comunes de aplicación y cómo solucionarlos: el problema Select N + 1 , la
viñeta
Para todos los que buscan una solución a este problema, encontré una publicación que lo describe. stackoverflow.com/questions/32453989/…
damndemon
2
Teniendo en cuenta las respuestas, ¿no debería calificarse de problema 1 + N? Como esto parece ser una terminología, no estoy, específicamente, preguntando a OP.
user1418717

Respuestas:

1018

Digamos que tiene una colección de Carobjetos (filas de la base de datos), y cada uno Cartiene una colección de Wheelobjetos (también filas). En otras palabras, CarWheeles una relación de 1 a muchos.

Ahora, supongamos que necesita recorrer todos los autos y, para cada uno, imprimir una lista de las ruedas. La ingenua implementación de O / R haría lo siguiente:

SELECT * FROM Cars;

Y luego para cada uno Car:

SELECT * FROM Wheel WHERE CarId = ?

En otras palabras, tiene una selección para los Autos, y luego N selecciones adicionales, donde N es el número total de autos.

Alternativamente, uno podría obtener todas las ruedas y realizar las búsquedas en la memoria:

SELECT * FROM Wheel

Esto reduce el número de viajes de ida y vuelta a la base de datos de N + 1 a 2. La mayoría de las herramientas ORM le brindan varias formas de evitar selecciones de N + 1.

Referencia: Java Persistence with Hibernate , capítulo 13.

Matt Solnit
fuente
140
Para aclarar sobre "Esto es malo", puede obtener todas las ruedas con 1 select ( SELECT * from Wheel;), en lugar de N + 1. Con una N grande, el impacto en el rendimiento puede ser muy significativo.
tucuxi
212
@tucuxi Me sorprende que hayas recibido tantos votos positivos por estar equivocado. Una base de datos es muy buena con respecto a los índices, ya que la consulta de un CarID específico volvería muy rápido. Pero si tienes todas las ruedas una vez, deberías buscar CarID en tu aplicación, que no está indexada, esto es más lento. A menos que tenga problemas importantes de latencia, llegar a su base de datos y n + 1 es realmente más rápido, y sí, lo comparé con una gran variedad de código del mundo real.
Ariel
74
@ariel La forma 'correcta' es obtener todas las ruedas, ordenadas por CarId (1 selección), y si se requieren más detalles que el CarId, haga una segunda consulta para todos los automóviles (2 consultas en total). Imprimir cosas ahora es óptimo, y no se requieren índices ni almacenamiento secundario (puede iterar sobre los resultados, no es necesario descargarlos todos). Hiciste una evaluación comparativa de lo incorrecto. Si todavía está seguro de sus puntos de referencia, ¿le importaría publicar un comentario más largo (o una respuesta completa) que explique su experimento y los resultados?
tucuxi
92
"Hibernate (no estoy familiarizado con los otros marcos ORM) le ofrece varias formas de manejarlo". y de esta manera son?
Tima
58
@Ariel Intente ejecutar sus puntos de referencia con servidores de bases de datos y aplicaciones en máquinas separadas. En mi experiencia, los viajes de ida y vuelta a la base de datos cuestan más en gastos generales que la consulta misma. Entonces, sí, las consultas son realmente rápidas, pero son los viajes de ida y vuelta los que causan estragos. Convertí "WHERE Id = const " a "WHERE Id IN ( const , const , ...)" y obtuve un aumento de órdenes de magnitud.
Hans
110
SELECT 
table1.*
, table2.*
INNER JOIN table2 ON table2.SomeFkId = table1.SomeId

Eso le da un conjunto de resultados donde las filas secundarias en la tabla2 causan duplicación al devolver los resultados de la tabla1 para cada fila secundaria en la tabla2. Los mapeadores de O / R deben diferenciar las instancias de table1 en función de un campo de clave único, luego usar todas las columnas de table2 para llenar instancias secundarias.

SELECT table1.*

SELECT table2.* WHERE SomeFkId = #

El N + 1 es donde la primera consulta llena el objeto primario y la segunda consulta llena todos los objetos secundarios para cada uno de los objetos primarios únicos devueltos.

Considerar:

class House
{
    int Id { get; set; }
    string Address { get; set; }
    Person[] Inhabitants { get; set; }
}

class Person
{
    string Name { get; set; }
    int HouseId { get; set; }
}

y mesas con una estructura similar. Una sola consulta para la dirección "22 Valley St" puede devolver:

Id Address      Name HouseId
1  22 Valley St Dave 1
1  22 Valley St John 1
1  22 Valley St Mike 1

El O / RM debe llenar una instancia de Inicio con ID = 1, Dirección = "22 Valley St" y luego llenar la matriz Habitantes con instancias de Personas para Dave, John y Mike con solo una consulta.

Una consulta N + 1 para la misma dirección utilizada anteriormente daría como resultado:

Id Address
1  22 Valley St

con una consulta separada como

SELECT * FROM Person WHERE HouseId = 1

y resultando en un conjunto de datos separado como

Name    HouseId
Dave    1
John    1
Mike    1

y el resultado final es el mismo que el anterior con la consulta única.

Las ventajas de la selección única es que obtiene todos los datos por adelantado, que pueden ser lo que finalmente desea. Las ventajas de N + 1 es que la complejidad de la consulta se reduce y puede usar la carga diferida donde los conjuntos de resultados secundarios solo se cargan a la primera solicitud.

cfeduke
fuente
44
La otra ventaja de n + 1 es que es más rápido porque la base de datos puede devolver los resultados directamente desde un índice. Hacer la unión y luego ordenar requiere una tabla temporal, que es más lenta. La única razón para evitar n + 1 es si tiene mucha latencia hablando con su base de datos.
Ariel
17
Unirse y ordenar puede ser bastante rápido (porque se unirá en campos indexados y posiblemente ordenados). ¿Qué tan grande es tu 'n + 1'? ¿Cree seriamente que el problema n + 1 solo se aplica a las conexiones de bases de datos de alta latencia?
tucuxi
99
@ariel: su consejo de que N + 1 es el "más rápido" es incorrecto, aunque sus puntos de referencia puedan ser correctos. ¿Cómo es eso posible? Ver en.wikipedia.org/wiki/Anecdotal_evidence , y también mi comentario en la otra respuesta a esta pregunta.
whitneyland el
77
@Ariel - Creo que lo entendí bien :). Solo estoy tratando de señalar que su resultado solo se aplica a un conjunto de condiciones. Fácilmente podría construir un contraejemplo que mostrara lo contrario. ¿Tiene sentido?
whitneyland
13
Para reiterar, el problema SELECT N + 1 es, en esencia: tengo 600 registros para recuperar. ¿Es más rápido obtener los 600 en una consulta, o 1 a la vez en 600 consultas? A menos que esté en MyISAM y / o tenga un esquema mal normalizado / mal indexado (en cuyo caso, el ORM no es el problema), un db sintonizado correctamente devolverá las 600 filas en 2 ms, mientras devuelve las filas individuales en aproximadamente 1 ms cada uno. Entonces, a menudo vemos que N + 1 toma cientos de milisegundos donde una unión solo toma un par
Perros
64

Proveedor con una relación de uno a muchos con el Producto. Un proveedor tiene (suministra) muchos productos.

***** Table: Supplier *****
+-----+-------------------+
| ID  |       NAME        |
+-----+-------------------+
|  1  |  Supplier Name 1  |
|  2  |  Supplier Name 2  |
|  3  |  Supplier Name 3  |
|  4  |  Supplier Name 4  |
+-----+-------------------+

***** Table: Product *****
+-----+-----------+--------------------+-------+------------+
| ID  |   NAME    |     DESCRIPTION    | PRICE | SUPPLIERID |
+-----+-----------+--------------------+-------+------------+
|1    | Product 1 | Name for Product 1 |  2.0  |     1      |
|2    | Product 2 | Name for Product 2 | 22.0  |     1      |
|3    | Product 3 | Name for Product 3 | 30.0  |     2      |
|4    | Product 4 | Name for Product 4 |  7.0  |     3      |
+-----+-----------+--------------------+-------+------------+

Factores

  • Modo diferido para el proveedor establecido en "verdadero" (predeterminado)

  • El modo de recuperación utilizado para consultar en el Producto es Seleccionar

  • Modo de recuperación (predeterminado): se accede a la información del proveedor

  • El almacenamiento en caché no juega un papel por primera vez

  • Se accede al proveedor

El modo Fetch es Seleccionar Fetch (predeterminado)

// It takes Select fetch mode as a default
Query query = session.createQuery( "from Product p");
List list = query.list();
// Supplier is being accessed
displayProductsListWithSupplierName(results);

select ... various field names ... from PRODUCT
select ... various field names ... from SUPPLIER where SUPPLIER.id=?
select ... various field names ... from SUPPLIER where SUPPLIER.id=?
select ... various field names ... from SUPPLIER where SUPPLIER.id=?

Resultado:

  • 1 declaración selecta para producto
  • N declaraciones selectas para el proveedor

¡Este es un problema de selección N + 1!

Summy
fuente
3
¿Se supone que es 1 selección para el proveedor y luego N selecciona para el producto?
bencampbell_14
@bencampbell_ Sí, inicialmente sentí lo mismo. Pero luego, con su ejemplo, es un producto para muchos proveedores.
Mohd Faizan Khan
38

No puedo comentar directamente sobre otras respuestas, porque no tengo suficiente reputación. Pero vale la pena señalar que el problema esencialmente solo surge porque, históricamente, muchos dbms han sido bastante pobres cuando se trata de manejar combinaciones (MySQL es un ejemplo particularmente notable). Entonces, n + 1, a menudo, ha sido notablemente más rápido que una unión. Y luego hay formas de mejorar en n + 1 pero aún sin necesidad de una unión, que es a lo que se refiere el problema original.

Sin embargo, MySQL ahora es mucho mejor de lo que solía ser cuando se trata de uniones. Cuando aprendí MySQL, usé muchas combinaciones. Luego descubrí lo lentos que son y cambié a n + 1 en el código. Pero, recientemente, he estado regresando a las uniones, porque MySQL ahora es mucho mejor para manejarlas que cuando comencé a usarlo.

En estos días, una unión simple en un conjunto de tablas correctamente indexadas rara vez es un problema, en términos de rendimiento. Y si da un golpe de rendimiento, entonces el uso de pistas de índice a menudo las resuelve.

Esto es discutido aquí por uno del equipo de desarrollo de MySQL:

http://jorgenloland.blogspot.co.uk/2013/02/dbt-3-q3-6-x-performance-in-mysql-5610.html

Entonces, el resumen es: si ha estado evitando uniones en el pasado debido al rendimiento abismal de MySQL con ellos, intente nuevamente con las últimas versiones. Probablemente se sorprenderá gratamente.

Mark Goodge
fuente
77
Llamar a las primeras versiones de MySQL un DBMS relacional es bastante difícil ... Si las personas que se encontraran con esos problemas hubieran estado utilizando una base de datos real, no habrían encontrado ese tipo de problemas. ;-)
Craig
2
Curiosamente, muchos de estos tipos de problemas se resolvieron en MySQL con la introducción y posterior optimización del motor INNODB, pero aún se encontrará con personas que intentan promocionar MYISAM porque piensan que es más rápido.
Craig
55
Para su información, uno de los 3 JOINalgoritmos comunes utilizados en RDBMS se llama bucles anidados. Básicamente es un N + 1 seleccionado debajo del capó. La única diferencia es que el DB tomó una decisión inteligente para usarlo basado en estadísticas e índices, en lugar de que el código del cliente lo obligue a seguir ese camino categóricamente.
Brandon
2
@ Brandon ¡Sí! Al igual que las sugerencias JOIN e INDEX, forzar una determinada ruta de ejecución en todos los casos rara vez superará a la base de datos. La base de datos casi siempre es muy buena para elegir el enfoque óptimo para obtener los datos. Tal vez en los primeros días de dbs necesitabas 'formular' tu pregunta de una manera peculiar para convencer a la base de datos, pero después de décadas de ingeniería de clase mundial, ahora puedes obtener el mejor rendimiento al hacerle a tu base de datos una pregunta relacional y dejarla resuelva cómo buscar y reunir esos datos por usted.
Perros
3
La base de datos no solo utiliza índices y estadísticas, sino que todas las operaciones también son E / S locales, muchas de las cuales a menudo operan en caché altamente eficiente en lugar de en disco. Los programadores de bases de datos dedican mucha atención a la optimización de este tipo de cosas.
Craig
27

Nos alejamos del ORM en Django debido a este problema. Básicamente, si lo intentas y haces

for p in person:
    print p.car.colour

El ORM devolverá felizmente a todas las personas (generalmente como instancias de un objeto Persona), pero luego deberá consultar la tabla del automóvil para cada Persona.

Un enfoque simple y muy efectivo para esto es algo que yo llamo " fanfolding ", que evita la absurda idea de que los resultados de la consulta de una base de datos relacional deberían corresponder a las tablas originales de las que se compone la consulta.

Paso 1: selección amplia

  select * from people_car_colour; # this is a view or sql function

Esto devolverá algo como

  p.id | p.name | p.telno | car.id | car.type | car.colour
  -----+--------+---------+--------+----------+-----------
  2    | jones  | 2145    | 77     | ford     | red
  2    | jones  | 2145    | 1012   | toyota   | blue
  16   | ashby  | 124     | 99     | bmw      | yellow

Paso 2: objetivar

Aspire los resultados en un creador de objetos genéricos con un argumento para dividir después del tercer elemento. Esto significa que el objeto "jones" no se realizará más de una vez.

Paso 3: renderizar

for p in people:
    print p.car.colour # no more car queries

Vea esta página web para una implementación de plegado en abanico para python.

rorycl
fuente
10
Estoy tan contento de haber tropezado con tu publicación, porque pensé que me estaba volviendo loco. cuando me enteré del problema N + 1, mi pensamiento inmediato fue: bueno, ¿por qué no creas una vista que contiene toda la información que necesitas y la sacas de esa vista? Has validado mi posición. gracias Señor.
un desarrollador
14
Nos alejamos del ORM en Django debido a este problema. ¿Eh? Django tiene select_related, lo que está destinado a resolver esto; de hecho, sus documentos comienzan con un ejemplo similar a su p.car.colourejemplo.
Adrian17
8
Esta es una vieja respuesta, tenemos select_related()y prefetch_related()en Django ahora.
Mariusz Jamro
1
Frio. Pero select_related()y amigo no parece hacer ninguna de las extrapolaciones obviamente útiles de una unión como LEFT OUTER JOIN. El problema no es un problema de interfaz, sino un problema relacionado con la extraña idea de que los objetos y los datos relacionales son asignables ... en mi opinión.
rorycl
26

Como esta es una pregunta muy común, escribí este artículo , en el que se basa esta respuesta.

¿Cuál es el problema de consulta N + 1?

El problema de la consulta N + 1 ocurre cuando el marco de acceso a datos ejecuta N sentencias SQL adicionales para obtener los mismos datos que podrían haberse recuperado al ejecutar la consulta SQL primaria.

Cuanto mayor sea el valor de N, más consultas se ejecutarán, mayor será el impacto en el rendimiento. Y, a diferencia del registro de consulta lento que puede ayudarlo a encontrar consultas de ejecución lenta, el problema de N + 1 no se detectará porque cada consulta adicional individual se ejecuta lo suficientemente rápido como para no activar el registro de consulta lenta.

El problema es ejecutar una gran cantidad de consultas adicionales que, en general, toman suficiente tiempo para ralentizar el tiempo de respuesta.

Consideremos que tenemos las siguientes tablas de base de datos post y post_comments que forman una relación de tabla de uno a muchos :

Las tablas <code> post </code> y <code> post_comments </code>

Vamos a crear las siguientes 4 postfilas:

INSERT INTO post (title, id)
VALUES ('High-Performance Java Persistence - Part 1', 1)

INSERT INTO post (title, id)
VALUES ('High-Performance Java Persistence - Part 2', 2)

INSERT INTO post (title, id)
VALUES ('High-Performance Java Persistence - Part 3', 3)

INSERT INTO post (title, id)
VALUES ('High-Performance Java Persistence - Part 4', 4)

Y también crearemos 4 post_commentregistros secundarios:

INSERT INTO post_comment (post_id, review, id)
VALUES (1, 'Excellent book to understand Java Persistence', 1)

INSERT INTO post_comment (post_id, review, id)
VALUES (2, 'Must-read for Java developers', 2)

INSERT INTO post_comment (post_id, review, id)
VALUES (3, 'Five Stars', 3)

INSERT INTO post_comment (post_id, review, id)
VALUES (4, 'A great reference book', 4)

Problema de consulta N + 1 con SQL simple

Si selecciona el post_commentsuso de esta consulta SQL:

List<Tuple> comments = entityManager.createNativeQuery("""
    SELECT
        pc.id AS id,
        pc.review AS review,
        pc.post_id AS postId
    FROM post_comment pc
    """, Tuple.class)
.getResultList();

Y, más tarde, decide buscar el asociado post titlepara cada uno post_comment:

for (Tuple comment : comments) {
    String review = (String) comment.get("review");
    Long postId = ((Number) comment.get("postId")).longValue();

    String postTitle = (String) entityManager.createNativeQuery("""
        SELECT
            p.title
        FROM post p
        WHERE p.id = :postId
        """)
    .setParameter("postId", postId)
    .getSingleResult();

    LOGGER.info(
        "The Post '{}' got this review '{}'",
        postTitle,
        review
    );
}

Va a desencadenar el problema de consulta N + 1 porque, en lugar de una consulta SQL, ejecutó 5 (1 + 4):

SELECT
    pc.id AS id,
    pc.review AS review,
    pc.post_id AS postId
FROM post_comment pc

SELECT p.title FROM post p WHERE p.id = 1
-- The Post 'High-Performance Java Persistence - Part 1' got this review
-- 'Excellent book to understand Java Persistence'

SELECT p.title FROM post p WHERE p.id = 2
-- The Post 'High-Performance Java Persistence - Part 2' got this review
-- 'Must-read for Java developers'

SELECT p.title FROM post p WHERE p.id = 3
-- The Post 'High-Performance Java Persistence - Part 3' got this review
-- 'Five Stars'

SELECT p.title FROM post p WHERE p.id = 4
-- The Post 'High-Performance Java Persistence - Part 4' got this review
-- 'A great reference book'

Arreglar el problema de consulta N + 1 es muy fácil. Todo lo que necesita hacer es extraer todos los datos que necesita en la consulta SQL original, así:

List<Tuple> comments = entityManager.createNativeQuery("""
    SELECT
        pc.id AS id,
        pc.review AS review,
        p.title AS postTitle
    FROM post_comment pc
    JOIN post p ON pc.post_id = p.id
    """, Tuple.class)
.getResultList();

for (Tuple comment : comments) {
    String review = (String) comment.get("review");
    String postTitle = (String) comment.get("postTitle");

    LOGGER.info(
        "The Post '{}' got this review '{}'",
        postTitle,
        review
    );
}

Esta vez, solo se ejecuta una consulta SQL para obtener todos los datos que nos interesan más.

Problema de consulta N + 1 con JPA e Hibernate

Al usar JPA e Hibernate, hay varias formas en que puede desencadenar el problema de consulta N + 1, por lo que es muy importante saber cómo puede evitar estas situaciones.

Para los siguientes ejemplos, considere que estamos asignando las tablas posty post_commentsa las siguientes entidades:

Entidades <code> Post </code> y <code> PostComment </code>

Las asignaciones JPA se ven así:

@Entity(name = "Post")
@Table(name = "post")
public class Post {

    @Id
    private Long id;

    private String title;

    //Getters and setters omitted for brevity
}

@Entity(name = "PostComment")
@Table(name = "post_comment")
public class PostComment {

    @Id
    private Long id;

    @ManyToOne
    private Post post;

    private String review;

    //Getters and setters omitted for brevity
}

FetchType.EAGER

El uso FetchType.EAGERimplícito o explícito para sus asociaciones JPA es una mala idea porque va a obtener muchos más datos que necesita. Más, elFetchType.EAGERAdemás estrategia también es propensa a problemas de consulta N + 1.

Desafortunadamente, las asociaciones @ManyToOney se @OneToOneusan FetchType.EAGERde manera predeterminada, por lo que si sus asignaciones se ven así:

@ManyToOne
private Post post;

Está utilizando la FetchType.EAGERestrategia y, cada vez que olvida usarla JOIN FETCHal cargar algunas PostCommententidades con una consulta JPQL o Criteria API:

List<PostComment> comments = entityManager
.createQuery("""
    select pc
    from PostComment pc
    """, PostComment.class)
.getResultList();

Va a desencadenar el problema de consulta N + 1:

SELECT 
    pc.id AS id1_1_, 
    pc.post_id AS post_id3_1_, 
    pc.review AS review2_1_ 
FROM 
    post_comment pc

SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 1
SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 2
SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 3
SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 4

Observe las instrucciones SELECT adicionales que se ejecutan porque la postasociación debe recuperarse antes de devolver el ListdePostComment las entidades.

A diferencia del plan de recuperación predeterminado, que está utilizando al llamar al findmétodo de la EnrityManager, una consulta de JPQL o Criteria API define un plan explícito que Hibernate no puede cambiar al inyectar un JOIN FETCH automáticamente. Por lo tanto, debe hacerlo manualmente.

Si no necesitaba la postasociación en absoluto, no tiene suerte al usarla FetchType.EAGERporque no hay forma de evitarla. Por eso es mejor usarFetchType.LAZY por defecto.

Pero, si desea utilizar la postasociación, puede utilizar JOIN FETCHpara evitar el problema de consulta N + 1:

List<PostComment> comments = entityManager.createQuery("""
    select pc
    from PostComment pc
    join fetch pc.post p
    """, PostComment.class)
.getResultList();

for(PostComment comment : comments) {
    LOGGER.info(
        "The Post '{}' got this review '{}'", 
        comment.getPost().getTitle(), 
        comment.getReview()
    );
}

Esta vez, Hibernate ejecutará una sola declaración SQL:

SELECT 
    pc.id as id1_1_0_, 
    pc.post_id as post_id3_1_0_, 
    pc.review as review2_1_0_, 
    p.id as id1_0_1_, 
    p.title as title2_0_1_ 
FROM 
    post_comment pc 
INNER JOIN 
    post p ON pc.post_id = p.id

-- The Post 'High-Performance Java Persistence - Part 1' got this review 
-- 'Excellent book to understand Java Persistence'

-- The Post 'High-Performance Java Persistence - Part 2' got this review 
-- 'Must-read for Java developers'

-- The Post 'High-Performance Java Persistence - Part 3' got this review 
-- 'Five Stars'

-- The Post 'High-Performance Java Persistence - Part 4' got this review 
-- 'A great reference book'

Para obtener más detalles sobre por qué debe evitar la FetchType.EAGERestrategia de recuperación, consulte también este artículo .

FetchType.LAZY

Incluso si cambia a usar FetchType.LAZYexplícitamente para todas las asociaciones, aún puede toparse con el problema N + 1.

Esta vez, la postasociación se mapea así:

@ManyToOne(fetch = FetchType.LAZY)
private Post post;

Ahora, cuando busca las PostCommententidades:

List<PostComment> comments = entityManager
.createQuery("""
    select pc
    from PostComment pc
    """, PostComment.class)
.getResultList();

Hibernate ejecutará una sola declaración SQL:

SELECT 
    pc.id AS id1_1_, 
    pc.post_id AS post_id3_1_, 
    pc.review AS review2_1_ 
FROM 
    post_comment pc

Pero, si después, va a hacer referencia a la postasociación con carga lenta:

for(PostComment comment : comments) {
    LOGGER.info(
        "The Post '{}' got this review '{}'", 
        comment.getPost().getTitle(), 
        comment.getReview()
    );
}

Obtendrá el problema de consulta N + 1:

SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 1
-- The Post 'High-Performance Java Persistence - Part 1' got this review 
-- 'Excellent book to understand Java Persistence'

SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 2
-- The Post 'High-Performance Java Persistence - Part 2' got this review 
-- 'Must-read for Java developers'

SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 3
-- The Post 'High-Performance Java Persistence - Part 3' got this review 
-- 'Five Stars'

SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 4
-- The Post 'High-Performance Java Persistence - Part 4' got this review 
-- 'A great reference book'

Debido a que la postasociación se obtiene perezosamente, se ejecutará una instrucción SQL secundaria al acceder a la asociación perezosa para generar el mensaje de registro.

Nuevamente, la solución consiste en agregar una JOIN FETCHcláusula a la consulta JPQL:

List<PostComment> comments = entityManager.createQuery("""
    select pc
    from PostComment pc
    join fetch pc.post p
    """, PostComment.class)
.getResultList();

for(PostComment comment : comments) {
    LOGGER.info(
        "The Post '{}' got this review '{}'", 
        comment.getPost().getTitle(), 
        comment.getReview()
    );
}

Y, al igual que en el FetchType.EAGERejemplo, esta consulta JPQL generará una sola declaración SQL.

Incluso si está utilizando FetchType.LAZYy no hace referencia a la asociación secundaria de una @OneToOnerelación JPA bidireccional , aún puede desencadenar el problema de consulta N + 1.

Para obtener más detalles sobre cómo puede superar el problema de consulta N + 1 generado por las @OneToOneasociaciones, consulte este artículo .

Cómo detectar automáticamente el problema de consulta N + 1

Si desea detectar automáticamente un problema de consulta N + 1 en su capa de acceso a datos, este artículo explica cómo puede hacerlo utilizando eldb-util proyecto de código abierto.

Primero, debe agregar la siguiente dependencia de Maven:

<dependency>
    <groupId>com.vladmihalcea</groupId>
    <artifactId>db-util</artifactId>
    <version>${db-util.version}</version>
</dependency>

Después, solo tiene que usar la SQLStatementCountValidatorutilidad para afirmar las declaraciones SQL subyacentes que se generan:

SQLStatementCountValidator.reset();

List<PostComment> comments = entityManager.createQuery("""
    select pc
    from PostComment pc
    """, PostComment.class)
.getResultList();

SQLStatementCountValidator.assertSelectCount(1);

En caso de que esté utilizando FetchType.EAGERy ejecute el caso de prueba anterior, obtendrá el siguiente error de caso de prueba:

SELECT 
    pc.id as id1_1_, 
    pc.post_id as post_id3_1_, 
    pc.review as review2_1_ 
FROM 
    post_comment pc

SELECT p.id as id1_0_0_, p.title as title2_0_0_ FROM post p WHERE p.id = 1

SELECT p.id as id1_0_0_, p.title as title2_0_0_ FROM post p WHERE p.id = 2


-- SQLStatementCountMismatchException: Expected 1 statement(s) but recorded 3 instead!

Para obtener más detalles sobre el db-utilproyecto de código abierto, consulte este artículo .

Vlad Mihalcea
fuente
Pero ahora tienes un problema con la paginación. Si tiene 10 autos, cada uno con 4 ruedas y desea paginar autos con 5 autos por página. Así que básicamente tienes SELECT cars, wheels FROM cars JOIN wheels LIMIT 0, 5. Pero lo que obtienes son 2 autos con 5 ruedas (primer auto con las 4 ruedas y segundo auto con solo 1 rueda), porque LIMIT limitará todo el conjunto de resultados, no solo la cláusula raíz.
CappY
2
Tengo un artículo para eso también.
Vlad Mihalcea
Gracias por el articulo Lo leeré. Mediante desplazamiento rápido, vi que la solución es la función de ventana, pero son bastante nuevas en MariaDB, por lo que el problema persiste en versiones anteriores. :)
CappY
@VladMihalcea, señalé ya sea de su artículo o de la publicación cada vez que se refiere al caso ManyToOne al explicar el problema N + 1. Pero, en realidad, las personas están más interesadas en el caso OneToMany relacionado con el tema N + 1. ¿Podría por favor referirse y explicar el caso de OneToMany?
JJ Beam
18

Supongamos que tiene EMPRESA y EMPLEADO. La EMPRESA tiene muchos EMPLEADOS (es decir, el EMPLEADO tiene un campo ID_COMPAÑÍA).

En algunas configuraciones de O / R, cuando tiene un objeto de empresa asignado y accede a sus objetos de empleado, la herramienta de O / R hará una selección para cada empleado, mientras que si solo estuviera haciendo cosas en SQL directo, podría select * from employees where company_id = XX . Por lo tanto, N (# de empleados) más 1 (empresa)

Así es como funcionaban las versiones iniciales de EJB Entity Beans. Creo que cosas como Hibernate han eliminado esto, pero no estoy muy seguro. La mayoría de las herramientas generalmente incluyen información sobre su estrategia de mapeo.

davetron5000
fuente
18

Aquí hay una buena descripción del problema.

Ahora que comprende el problema, generalmente se puede evitar haciendo una búsqueda de combinación en su consulta. Básicamente, esto obliga a buscar el objeto cargado de forma diferida para que los datos se recuperen en una consulta en lugar de n + 1 consultas. Espero que esto ayude.

Joe Dean
fuente
17

Consulte la publicación de Ayende sobre el tema: Combatir el problema Seleccionar N + 1 en NHibernate .

Básicamente, cuando se utiliza un ORM como NHibernate o EntityFramework, si tiene una relación de uno a muchos (detalle maestro) y desea enumerar todos los detalles por cada registro maestro, debe realizar llamadas de consulta N + 1 al base de datos, siendo "N" el número de registros maestros: 1 consulta para obtener todos los registros maestros y N consultas, una por registro maestro, para obtener todos los detalles por registro maestro.

Más llamadas de consulta a la base de datos → más tiempo de latencia → disminución del rendimiento de la aplicación / base de datos.

Sin embargo, los ORM tienen opciones para evitar este problema, principalmente utilizando JOIN.

Nathan
fuente
3
las combinaciones no son una buena solución (a menudo), porque pueden dar como resultado un producto cartesiano, lo que significa que el número de filas de resultados es el número de resultados de la tabla raíz multiplicado por el número de resultados en cada tabla secundaria. particularmente malo en múltiples niveles de jerarquía. Seleccionar 20 "blogs" con 100 "publicaciones" en cada una y 10 "comentarios" en cada publicación dará como resultado 20000 filas de resultados. NHibernate tiene soluciones alternativas, como el "tamaño de lote" (seleccione elementos secundarios con cláusula en identificadores principales) o "subseleccionar".
Erik Hart
14

Es mucho más rápido emitir 1 consulta que devuelve 100 resultados que emitir 100 consultas que cada una devuelve 1 resultado.

jj_
fuente
13

En mi opinión, el artículo escrito en Hibernate Pitfall: Por qué las relaciones deberían ser perezosas es exactamente opuesto al problema real de N + 1.

Si necesita una explicación correcta, consulte Hibernate - Capítulo 19: Mejora del rendimiento - Obtener estrategias

Seleccionar recuperación (el valor predeterminado) es extremadamente vulnerable a N + 1 selecciona problemas, por lo que es posible que deseemos habilitar la recuperación de unión

Anoop Isaac
fuente
2
Leí la página de hibernación. No dice cuál es realmente el problema de selección de N + 1 . Pero dice que puedes usar combinaciones para arreglarlo.
Ian Boyd el
3
Se requiere el tamaño de lote para la recuperación selectiva, para seleccionar objetos secundarios para varios padres en una declaración select. Subseleccionar podría ser otra alternativa. Las uniones pueden ser realmente malas si tiene múltiples niveles de jerarquía y se crea un producto cartesiano.
Erik Hart
10

El enlace provisto tiene un ejemplo muy simple del problema n + 1. Si lo aplicas a Hibernate, básicamente se trata de lo mismo. Cuando consulta un objeto, la entidad se carga pero cualquier asociación (a menos que se configure de otra manera) se cargará de forma diferida. Por lo tanto, una consulta para los objetos raíz y otra consulta para cargar las asociaciones para cada uno de estos. 100 objetos devueltos significa una consulta inicial y luego 100 consultas adicionales para obtener la asociación para cada uno, n + 1.

http://pramatr.com/2009/02/05/sql-n-1-selects-explained/


fuente
9

Un millonario tiene N autos. Desea obtener todas las (4) ruedas.

Una (1) consulta carga todos los automóviles, pero para cada (N) automóvil se envía una consulta por separado para cargar las ruedas.

Costos:

Suponga que los índices encajan en el carnero.

Análisis y planificación de consultas 1 + N + búsqueda de índice Y 1 + N + (N * 4) acceso a la placa para cargar la carga útil.

Suponga que los índices no encajan en el ram.

Costos adicionales en el peor de los casos 1 + N acceso a la placa para el índice de carga.

Resumen

El cuello de la botella es el acceso a la placa (aproximadamente 70 veces por segundo acceso aleatorio en el disco duro) Una selección de unión ansiosa también accedería a la placa 1 + N + (N * 4) veces para la carga útil. Entonces, si los índices se ajustan a la memoria RAM, no hay problema, es lo suficientemente rápido porque solo intervienen las operaciones de memoria RAM.

hans wurst
fuente
9

El problema de selección N + 1 es un problema, y ​​tiene sentido detectar tales casos en pruebas unitarias. He desarrollado una pequeña biblioteca para verificar el número de consultas ejecutadas por un método de prueba dado o simplemente un bloque de código arbitrario - JDBC Sniffer

Simplemente agregue una regla JUnit especial a su clase de prueba y coloque una anotación con el número esperado de consultas en sus métodos de prueba:

@Rule
public final QueryCounter queryCounter = new QueryCounter();

@Expectation(atMost = 3)
@Test
public void testInvokingDatabase() {
    // your JDBC or JPA code
}
bedrin
fuente
5

El problema, como otros han dicho con más elegancia, es que tienes un producto cartesiano de las columnas OneToMany o estás haciendo selecciones N + 1. Posible conjunto de resultados gigantesco o hablador con la base de datos, respectivamente.

Me sorprende que esto no se mencione, pero así es como he solucionado este problema ... Hago una tabla de ID semi-temporal . También hago esto cuando tienes la IN ()limitación de la cláusula .

Esto no funciona para todos los casos (probablemente ni siquiera la mayoría), pero funciona particularmente bien si tiene muchos objetos secundarios de manera que el producto cartesiano se salga de control (es decir, muchas OneToManycolumnas, el número de resultados será un multiplicación de las columnas) y es más un lote como trabajo.

Primero inserta sus identificadores de objeto principal como lote en una tabla de identificadores. Este batch_id es algo que generamos en nuestra aplicación y que conservamos.

INSERT INTO temp_ids 
    (product_id, batch_id)
    (SELECT p.product_id, ? 
    FROM product p ORDER BY p.product_id
    LIMIT ? OFFSET ?);

Ahora, para cada OneToManycolumna, simplemente haga un SELECTen la tabla de identificadores INNER JOINde la tabla secundaria con un WHERE batch_id=(o viceversa). Solo debe asegurarse de ordenar por la columna de id, ya que facilitará la fusión de las columnas de resultados (de lo contrario, necesitará un HashMap / Table para todo el conjunto de resultados que puede no ser tan malo).

Luego, limpie periódicamente la tabla de identificadores.

Esto también funciona particularmente bien si el usuario selecciona unos 100 elementos distintos para algún tipo de procesamiento masivo. Ponga los 100 identificadores distintos en la tabla temporal.

Ahora el número de consultas que está haciendo es por el número de columnas OneToMany.

Adam Gent
fuente
1

Tome el ejemplo de Matt Solnit, imagine que define una asociación entre Car y Wheels como LAZY y necesita algunos campos de Wheels. Esto significa que después de la primera selección, hibernate hará "Select * from Wheels donde car_id =: id" PARA CADA automóvil.

Esto hace la primera selección y más 1 selección por cada automóvil N, por eso se llama problema n + 1.

Para evitar esto, haga que la asociación busque como entusiasta, para que hibernar cargue datos con una unión.

Pero atención, si muchas veces no accede a las Ruedas asociadas, es mejor mantenerlo PERFECTO o cambiar el tipo de búsqueda con Criterios.

martins.tuga
fuente
1
Una vez más, las uniones no son una buena solución, especialmente cuando se pueden cargar más de 2 niveles de jerarquía. Marque "subseleccionar" o "tamaño de lote" en su lugar; el último cargará hijos por ID de padres en la cláusula "in", como "select ... from wheels donde car_id in (1,3,4,6,7,8,11,13)".
Erik Hart