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?
orm
select-n-plus-1
Lars A. Brekken
fuente
fuente
Respuestas:
Digamos que tiene una colección de
Car
objetos (filas de la base de datos), y cada unoCar
tiene una colección deWheel
objetos (también filas). En otras palabras,Car
→Wheel
es 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:
Y luego para cada uno
Car
: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:
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.
fuente
SELECT * from Wheel;
), en lugar de N + 1. Con una N grande, el impacto en el rendimiento puede ser muy significativo.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.
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:
y mesas con una estructura similar. Una sola consulta para la dirección "22 Valley St" puede devolver:
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:
con una consulta separada como
y resultando en un conjunto de datos separado como
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.
fuente
Proveedor con una relación de uno a muchos con el Producto. Un proveedor tiene (suministra) muchos productos.
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)
Resultado:
¡Este es un problema de selección N + 1!
fuente
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.
fuente
JOIN
algoritmos 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.Nos alejamos del ORM en Django debido a este problema. Básicamente, si lo intentas y haces
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
Esto devolverá algo como
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
Vea esta página web para una implementación de plegado en abanico para python.
fuente
select_related
, lo que está destinado a resolver esto; de hecho, sus documentos comienzan con un ejemplo similar a sup.car.colour
ejemplo.select_related()
yprefetch_related()
en Django ahora.select_related()
y amigo no parece hacer ninguna de las extrapolaciones obviamente útiles de una unión comoLEFT 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.¿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 :
Vamos a crear las siguientes 4
post
filas:Y también crearemos 4
post_comment
registros secundarios:Problema de consulta N + 1 con SQL simple
Si selecciona el
post_comments
uso de esta consulta SQL:Y, más tarde, decide buscar el asociado
post
title
para cada unopost_comment
:Va a desencadenar el problema de consulta N + 1 porque, en lugar de una consulta SQL, ejecutó 5 (1 + 4):
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í:
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
post
ypost_comments
a las siguientes entidades:Las asignaciones JPA se ven así:
FetchType.EAGER
El uso
FetchType.EAGER
implí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.EAGER
Además estrategia también es propensa a problemas de consulta N + 1.Desafortunadamente, las asociaciones
@ManyToOne
y se@OneToOne
usanFetchType.EAGER
de manera predeterminada, por lo que si sus asignaciones se ven así:Está utilizando la
FetchType.EAGER
estrategia y, cada vez que olvida usarlaJOIN FETCH
al cargar algunasPostComment
entidades con una consulta JPQL o Criteria API:Va a desencadenar el problema de consulta N + 1:
Observe las instrucciones SELECT adicionales que se ejecutan porque la
post
asociación debe recuperarse antes de devolver elList
dePostComment
las entidades.A diferencia del plan de recuperación predeterminado, que está utilizando al llamar al
find
método de laEnrityManager
, 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
post
asociación en absoluto, no tiene suerte al usarlaFetchType.EAGER
porque no hay forma de evitarla. Por eso es mejor usarFetchType.LAZY
por defecto.Pero, si desea utilizar la
post
asociación, puede utilizarJOIN FETCH
para evitar el problema de consulta N + 1:Esta vez, Hibernate ejecutará una sola declaración SQL:
FetchType.LAZY
Incluso si cambia a usar
FetchType.LAZY
explícitamente para todas las asociaciones, aún puede toparse con el problema N + 1.Esta vez, la
post
asociación se mapea así:Ahora, cuando busca las
PostComment
entidades:Hibernate ejecutará una sola declaración SQL:
Pero, si después, va a hacer referencia a la
post
asociación con carga lenta:Obtendrá el problema de consulta N + 1:
Debido a que la
post
asociació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 FETCH
cláusula a la consulta JPQL:Y, al igual que en el
FetchType.EAGER
ejemplo, esta consulta JPQL generará una sola declaración SQL.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 el
db-util
proyecto de código abierto.Primero, debe agregar la siguiente dependencia de Maven:
Después, solo tiene que usar la
SQLStatementCountValidator
utilidad para afirmar las declaraciones SQL subyacentes que se generan:En caso de que esté utilizando
FetchType.EAGER
y ejecute el caso de prueba anterior, obtendrá el siguiente error de caso de prueba:fuente
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.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.
fuente
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.
fuente
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.
fuente
Es mucho más rápido emitir 1 consulta que devuelve 100 resultados que emitir 100 consultas que cada una devuelve 1 resultado.
fuente
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
fuente
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
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.
fuente
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:
fuente
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
OneToMany
columnas, 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.
Ahora, para cada
OneToMany
columna, simplemente haga unSELECT
en la tabla de identificadoresINNER JOIN
de la tabla secundaria con unWHERE 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.
fuente
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.
fuente