O "N + 1 seleciona o problema" geralmente é declarado como um problema nas discussões de mapeamento objeto-relacional (ORM), e eu entendo que isso tem algo a ver com ter que fazer muitas consultas ao banco de dados para algo que parece simples no objeto mundo.
Alguém tem uma explicação mais detalhada do problema?
orm
select-n-plus-1
Lars A. Brekken
fonte
fonte
Respostas:
Digamos que você tenha uma coleção de
Car
objetos (linhas do banco de dados) e cadaCar
um tenha uma coleção deWheel
objetos (também linhas). Em outras palavras,Car
→Wheel
é um relacionamento de 1 para muitos.Agora, digamos que você precise percorrer todos os carros e, para cada um, imprimir uma lista das rodas. A implementação ingênua de O / R faria o seguinte:
E então para cada um
Car
:Em outras palavras, você tem uma seleção para os carros e, em seguida, N seleciona mais, onde N é o número total de carros.
Como alternativa, é possível obter todas as rodas e realizar as pesquisas na memória:
Isso reduz o número de viagens de ida e volta ao banco de dados de N + 1 para 2. A maioria das ferramentas ORM oferece várias maneiras de impedir a seleção de N + 1.
Referência: Persistência de Java com Hibernate , capítulo 13.
fonte
SELECT * from Wheel;
), em vez de N + 1. Com um N grande, o impacto no desempenho pode ser muito significativo.Isso gera um conjunto de resultados em que as linhas filho na tabela2 causam duplicação retornando os resultados da tabela1 para cada linha filha na tabela2. Os mapeadores de O / R devem diferenciar instâncias da tabela1 com base em um campo-chave exclusivo e, em seguida, usar todas as colunas da tabela2 para preencher instâncias filhas.
O N + 1 é onde a primeira consulta preenche o objeto primário e a segunda consulta preenche todos os objetos filhos de cada um dos objetos primários exclusivos retornados.
Considerar:
e tabelas com uma estrutura semelhante. Uma única consulta para o endereço "22 Valley St" pode retornar:
O O / RM deve preencher uma instância de Home com ID = 1, Endereço = "22 Valley St" e, em seguida, preencher a matriz de Habitantes com instâncias de Pessoas para Dave, John e Mike com apenas uma consulta.
Uma consulta N + 1 para o mesmo endereço usado acima resultaria em:
com uma consulta separada como
e resultando em um conjunto de dados separado, como
e o resultado final é o mesmo que acima com a consulta única.
As vantagens da seleção única são que você obtém todos os dados antecipadamente, o que pode ser o que você deseja. As vantagens do N + 1 são a complexidade da consulta reduzida e você pode usar o carregamento lento, em que os conjuntos de resultados filhos são carregados somente na primeira solicitação.
fonte
Fornecedor com uma relação de um para muitos com o Produto. Um fornecedor possui (fornece) muitos produtos.
Fatores:
Modo lento para o fornecedor definido como "verdadeiro" (padrão)
O modo de busca usado para consultar o Produto é Selecionar
Modo de busca (padrão): as informações do fornecedor são acessadas
O armazenamento em cache não desempenha um papel pela primeira vez
O fornecedor é acessado
O modo de busca é Selecionar busca (padrão)
Resultado:
Este é o problema de seleção N + 1!
fonte
Não posso comentar diretamente sobre outras respostas, porque não tenho reputação suficiente. Mas vale a pena notar que o problema basicamente só surge porque, historicamente, muitos dbms são muito ruins quando se trata de lidar com junções (o MySQL é um exemplo particularmente notável). Portanto, n + 1 tem sido, com frequência, notavelmente mais rápido que uma junção. Além disso, existem maneiras de melhorar o n + 1, mas ainda sem a necessidade de associação, e é com isso que o problema original se relaciona.
No entanto, o MySQL agora é muito melhor do que costumava ser quando se trata de junções. Quando eu aprendi o MySQL, usei muito o joins. Então eu descobri como eles são lentos e, em vez disso, mudei para n + 1 no código. Mas, recentemente, eu voltei para as junções, porque o MySQL agora é muito melhor em lidar com elas do que era quando eu comecei a usá-las.
Atualmente, uma junção simples em um conjunto de tabelas indexado corretamente raramente é um problema, em termos de desempenho. E se isso causa um impacto no desempenho, o uso de dicas de índice geralmente as resolve.
Isso é discutido aqui por uma equipe de desenvolvimento do MySQL:
http://jorgenloland.blogspot.co.uk/2013/02/dbt-3-q3-6-x-performance-in-mysql-5610.html
Portanto, o resumo é: Se você estava evitando junções no passado por causa do péssimo desempenho do MySQL com elas, tente novamente nas versões mais recentes. Você provavelmente ficará agradavelmente surpreendido.
fonte
JOIN
algoritmos comuns usados no RDBMS 'é chamado de loops aninhados. Fundamentalmente, é um N + 1 selecionado sob o capô. A única diferença é que o banco de dados fez uma escolha inteligente para usá-lo com base em estatísticas e índices, em vez de o código do cliente forçar esse caminho categoricamente.Afastamo-nos do ORM no Django por causa desse problema. Basicamente, se você tentar fazer
O ORM retornará felizmente todas as pessoas (normalmente como instâncias de um objeto Pessoa), mas precisará consultar a tabela de carros para cada Pessoa.
Uma abordagem simples e muito eficaz para isso é algo que eu chamo de " dobragem de fãs ", que evita a idéia sem sentido de que os resultados da consulta de um banco de dados relacional devem ser mapeados de volta para as tabelas originais das quais a consulta é composta.
Etapa 1: ampla seleção
Isso retornará algo como
Etapa 2: Objetivar
Coloque os resultados em um criador de objeto genérico com um argumento para dividir após o terceiro item. Isso significa que o objeto "jones" não será criado mais de uma vez.
Etapa 3: renderizar
Consulte esta página da Web para obter uma implementação de dobragem em ventilador para python.
fonte
select_related
, que tem o objetivo de resolver isso - de fato, seus documentos começam com um exemplo semelhante ao seup.car.colour
exemplo.select_related()
eprefetch_related()
agora no Django.select_related()
e friend não parecem fazer nenhuma das extrapolações obviamente úteis de uma junção comoLEFT OUTER JOIN
. O problema não é um problema de interface, mas um problema relacionado à estranha idéia de que objetos e dados relacionais são mapeáveis .... na minha opinião.Qual é o problema de consulta N + 1
O problema de consulta N + 1 ocorre quando a estrutura de acesso a dados executa N instruções SQL adicionais para buscar os mesmos dados que poderiam ter sido recuperados ao executar a consulta SQL primária.
Quanto maior o valor de N, mais consultas serão executadas, maior o impacto no desempenho. E, diferentemente do log de consultas lentas que pode ajudá-lo a encontrar consultas de execução lenta, o problema N + 1 não será pontual, pois cada consulta adicional individual é executada com rapidez suficiente para não acionar o log de consultas lentas.
O problema está executando um grande número de consultas adicionais que, em geral, levam tempo suficiente para diminuir o tempo de resposta.
Vamos considerar que temos as seguintes tabelas de banco de dados post e post_comments que formam um relacionamento de tabela um para muitos :
Vamos criar as 4
post
linhas a seguir :E também criaremos 4
post_comment
registros filhos:Problema de consulta N + 1 com SQL simples
Se você selecionar
post_comments
usando esta consulta SQL:E, mais tarde, você decide buscar o associado
post
title
para cada umpost_comment
:Você acionará o problema de consulta N + 1 porque, em vez de uma consulta SQL, você executou 5 (1 + 4):
Corrigir o problema de consulta N + 1 é muito fácil. Tudo o que você precisa fazer é extrair todos os dados necessários na consulta SQL original, assim:
Desta vez, apenas uma consulta SQL é executada para buscar todos os dados que estamos mais interessados em usar.
Problema de consulta N + 1 com JPA e Hibernate
Ao usar o JPA e o Hibernate, há várias maneiras de disparar o problema de consulta N + 1; portanto, é muito importante saber como evitar essas situações.
Para os próximos exemplos, considere que estamos mapeando as tabelas
post
epost_comments
para as seguintes entidades:Os mapeamentos JPA são assim:
FetchType.EAGER
Usar de
FetchType.EAGER
forma implícita ou explícita para suas associações JPA é uma péssima idéia, pois você irá buscar muito mais dados necessários.FetchType.EAGER
Além disso , a estratégia também é propensa a problemas de consulta N + 1.Infelizmente, as associações
@ManyToOne
e@OneToOne
usamFetchType.EAGER
por padrão, portanto, se seus mapeamentos se parecerem com isso:Você está usando a
FetchType.EAGER
estratégia e, toda vez que se esquecer de usarJOIN FETCH
ao carregar algumasPostComment
entidades com uma consulta à API JPQL ou Critérios:Você vai acionar o problema de consulta N + 1:
Observe as instruções SELECT adicionais que são executadas porque a
post
associação precisa ser buscada antes do retornoList
dasPostComment
entidades.Diferentemente do plano de busca padrão, que você está usando ao chamar o
find
método daEnrityManager
, uma consulta à API JPQL ou Critérios define um plano explícito que o Hibernate não pode alterar injetando um JOIN FETCH automaticamente. Então, você precisa fazer isso manualmente.Se você não precisou da
post
associação, está sem sorte ao usá-laFetchType.EAGER
porque não há como evitar a busca. É por isso que é melhor usarFetchType.LAZY
por padrão.Mas, se você quiser usar a
post
associação, poderá usarJOIN FETCH
para evitar o problema de consulta N + 1:Desta vez, o Hibernate executará uma única instrução SQL:
FetchType.LAZY
Mesmo se você mudar para o uso
FetchType.LAZY
explícito em todas as associações, ainda poderá encontrar o problema N + 1.Desta vez, a
post
associação é mapeada da seguinte maneira:Agora, quando você busca as
PostComment
entidades:O Hibernate irá executar uma única instrução SQL:
Mas, se depois, você fará referência à
post
associação preguiçosa :Você receberá o problema de consulta N + 1:
Como a
post
associação é buscada preguiçosamente, uma instrução SQL secundária será executada ao acessar a associação preguiçosa para criar a mensagem de log.Novamente, a correção consiste em adicionar uma
JOIN FETCH
cláusula à consulta JPQL:E, como no
FetchType.EAGER
exemplo, essa consulta JPQL gerará uma única instrução SQL.Como detectar automaticamente o problema de consulta N + 1
Se você deseja detectar automaticamente o problema de consulta N + 1 em sua camada de acesso a dados, este artigo explica como fazer isso usando o
db-util
projeto de código aberto.Primeiro, você precisa adicionar a seguinte dependência do Maven:
Depois, você só precisa usar o
SQLStatementCountValidator
utilitário para afirmar as instruções SQL subjacentes que são geradas:Caso você esteja usando
FetchType.EAGER
e execute o caso de teste acima, você receberá a seguinte falha do caso de teste:fonte
SELECT cars, wheels FROM cars JOIN wheels LIMIT 0, 5
. Mas o que você recebe são 2 carros com 5 rodas (primeiro carro com todas as 4 rodas e segundo carro com apenas 1 roda), porque LIMIT limitará todo o conjunto de resultados, não apenas a cláusula raiz.Suponha que você tenha EMPRESA e EMPREGADO. EMPRESA possui muitos COLABORADORES (ou seja, COLABORADOR possui um campo EMPRESA_ID).
Em algumas configurações de O / R, quando você tem um objeto Empresa mapeado e acessa seus objetos Employee, a ferramenta de O / R faz uma seleção para cada funcionário, enquanto que, se você estivesse apenas fazendo coisas no SQL direto, poderia
select * from employees where company_id = XX
. Assim, N (número de funcionários) mais 1 (empresa)É assim que as versões iniciais do EJB Entity Beans funcionaram. Acredito que coisas como o Hibernate acabaram com isso, mas não tenho muita certeza. A maioria das ferramentas geralmente inclui informações sobre sua estratégia de mapeamento.
fonte
Aqui está uma boa descrição do problema
Agora que você entende o problema, ele geralmente pode ser evitado fazendo uma busca de junção na sua consulta. Isso basicamente força a busca do objeto carregado lento, para que os dados sejam recuperados em uma consulta, em vez de n + 1 consultas. Espero que isto ajude.
fonte
Verifique a publicação de Ayende sobre o tópico: Combate ao problema Selecionar N + 1 no NHibernate .
Basicamente, ao usar um ORM como NHibernate ou EntityFramework, se você tiver um relacionamento um para muitos (detalhes mestre) e quiser listar todos os detalhes por cada registro mestre, precisará fazer chamadas de consulta N + 1 para o banco de dados, "N" sendo o número de registros mestre: 1 consulta para obter todos os registros mestre e N consultas, uma por registro mestre, para obter todos os detalhes por registro mestre.
Mais chamadas de consulta ao banco de dados → mais tempo de latência → diminuição no desempenho do aplicativo / banco de dados.
No entanto, os ORMs têm opções para evitar esse problema, principalmente usando JOINs.
fonte
É muito mais rápido emitir 1 consulta que retorna 100 resultados do que emitir 100 consultas que retornam 1 resultado.
fonte
Na minha opinião, o artigo escrito em Hibernate Pitfall: Por que os relacionamentos devem ser preguiçosos é exatamente o oposto do verdadeiro problema de N + 1.
Se você precisar de uma explicação correta, consulte Hibernate - Capítulo 19: Melhorando o Desempenho - Buscando Estratégias
fonte
O link fornecido tem um exemplo muito simples do problema n + 1. Se você aplicá-lo ao Hibernate, está basicamente falando da mesma coisa. Quando você consulta um objeto, a entidade é carregada, mas quaisquer associações (a menos que configuradas de outra forma) serão carregadas com preguiça. Portanto, uma consulta para os objetos raiz e outra consulta para carregar as associações para cada um deles. 100 objetos retornados significam uma consulta inicial e, em seguida, 100 consultas adicionais para obter a associação para cada um, n + 1.
http://pramatr.com/2009/02/05/sql-n-1-selects-explained/
fonte
Um milionário tem N carros. Você deseja obter todas as (4) rodas.
Uma (1) consulta carrega todos os carros, mas para cada (N) uma consulta separada é enviada para carregar as rodas.
Custos:
Suponha que os índices se encaixam no ram.
Análise e planejamento de consulta 1 + N + pesquisa de índice E 1 + N + (N * 4) acesso à placa para carga útil.
Suponha que os índices não se encaixam no ram.
Custos adicionais, na pior das hipóteses, acessos à placa 1 + N para o índice de carregamento.
Sumário
O gargalo da garrafa é o acesso à placa (acesso aleatório de 70 vezes por segundo no disco rígido) Um seletor de junção ansioso também acessaria a placa 1 + N + (N * 4) vezes para carga útil. Portanto, se os índices se encaixam no RAM - não há problema, é rápido o suficiente, porque apenas as operações de RAM estão envolvidas.
fonte
O problema de seleção do N + 1 é um problema e faz sentido detectar esses casos em testes de unidade. Eu desenvolvi uma pequena biblioteca para verificar o número de consultas executadas por um determinado método de teste ou apenas um bloco de código arbitrário - JDBC Sniffer
Basta adicionar uma regra JUnit especial à sua classe de teste e colocar anotações com o número esperado de consultas nos seus métodos de teste:
fonte
O problema, como outros declararam de maneira mais elegante, é que você possui um produto cartesiano das colunas OneToMany ou está executando o N + 1 Selects. Possível conjunto de resultados gigantesco ou tagarela com o banco de dados, respectivamente.
Estou surpreso que isso não tenha sido mencionado, mas é assim que eu resolvi esse problema ... Eu faço uma tabela de IDs semi-temporária . Eu também faço isso quando você tem a
IN ()
limitação da cláusula .Isso não funciona em todos os casos (provavelmente nem na maioria), mas funciona particularmente bem se você tiver muitos objetos filhos, de modo que o produto cartesiano fique fora de controle (ou seja, em muitas
OneToMany
colunas, o número de resultados será um multiplicação das colunas) e seu trabalho é mais parecido com um lote.Primeiro, insira seus IDs de objeto pai como lote em uma tabela de IDs. Esse batch_id é algo que geramos em nosso aplicativo e nos apegamos.
Agora, para cada
OneToMany
coluna, basta fazer umSELECT
na tabela de IDs na tabelaINNER JOIN
filho com umWHERE batch_id=
(ou vice-versa). Você só quer ter a ordem da coluna id, pois isso facilitará a mesclagem das colunas de resultados (caso contrário, você precisará de um HashMap / Table para todo o conjunto de resultados, o que pode não ser tão ruim).Então você apenas limpa periodicamente a tabela de IDs.
Isso também funciona particularmente bem se o usuário selecionar, digamos 100 itens distintos para algum tipo de processamento em massa. Coloque os 100 IDs distintos na tabela temporária.
Agora, o número de consultas que você está fazendo é pelo número de colunas OneToMany.
fonte
Tomemos o exemplo de Matt Solnit, imagine que você define uma associação entre Car e Wheels como LAZY e precisa de alguns campos de Wheels. Isso significa que após a primeira seleção, o hibernate fará "Select * from Wheels, onde car_id =: id" PARA CADA Carro.
Isso torna a primeira seleção e mais 1 seleção por cada carro N, por isso é chamado de problema n + 1.
Para evitar isso, faça a associação buscar como ansiosa, para que o hibernate carregue dados com uma associação.
Mas atenção, se muitas vezes você não acessar o Wheels associado, é melhor mantê-lo LAZY ou alterar o tipo de busca com os Critérios.
fonte