O que é o “problema N + 1 selecciona” em ORM (Object-relacional Mapeamento)?

StackOverflow https://stackoverflow.com/questions/97197

  •  01-07-2019
  •  | 
  •  

Pergunta

O "problema N + 1 seleciona" é geralmente indicado como um problema de mapeamento objeto-relacional (ORM) discussões, e eu entendo que ele tem algo fazer para com ter que fazer um monte de consultas de banco de dados para algo que parece simples no mundo do objeto.

Alguém tem uma explicação mais detalhada do problema?

Foi útil?

Solução

Vamos dizer que você tem uma coleção de objetos Car (linhas de banco de dados), e cada Car tem uma coleção de objetos Wheel (também linhas). Em outras palavras, Car -> Wheel é um relacionamento 1-to-many

.

Agora, vamos dizer que você precisa para percorrer todos os carros, e para cada um, imprimir uma lista das rodas. A implementação O / R ingênuo faria o seguinte:

SELECT * FROM Cars;

E, em seguida, para cada Car:

SELECT * FROM Wheel WHERE CarId = ?

Em outras palavras, você tem uma escolha para os carros, e em seguida, seleciona N adicionais, onde N é o número total de carros.

Como alternativa, pode-se obter todas as rodas e realizar as pesquisas na memória:

SELECT * FROM Wheel

Isto reduz o número de de ida e volta para a base de dados a partir de N + 1 a 2. A maioria das ferramentas ORM dar-lhe várias formas de prevenir a N + 1 seleciona.

Referência: Java Persistence com Hibernate , capítulo 13.

Outras dicas

SELECT 
table1.*
, table2.*
INNER JOIN table2 ON table2.SomeFkId = table1.SomeId

Que você recebe um conjunto de resultados onde as linhas criança em causa a duplicação table2 por retornar os resultados tabela1 para cada linha infantil em table2. R mapeadores O / deve diferenciar casos tabela1 com base em um campo de chave única, em seguida, usar todas as colunas Quadro2 Médias a instâncias criança preenchido.

SELECT table1.*

SELECT table2.* WHERE SomeFkId = #

O N + 1 é onde a primeira consulta preenche o objecto principal e os segundos povoa consulta todos os objectos subordinados para cada um dos objectos primários únicos devolvidos.

Considere o seguinte:

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

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

e mesas com uma estrutura semelhante. A única consulta para o endereço "22 Valley St" pode retornar:

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

O O / RM deve preencher uma instância de casa com ID = 1, endereço = "22 Valley St" e, em seguida, preencher a matriz Habitantes com instâncias Pessoas para Dave, John, e Mike com apenas uma consulta.

A N + 1 de consulta para o mesmo endereço utilizado acima resultaria em:

Id Address
1  22 Valley St

com uma consulta separada como

SELECT * FROM Person WHERE HouseId = 1

e resultando em um conjunto de dados separado, como

Name    HouseId
Dave    1
John    1
Mike    1

e sendo o mesmo que acima, com a única consulta o resultado final.

As vantagens para seleção única é que você obter todos os dados na frente que pode ser o que você finalmente desejar. As vantagens para N + 1 é consulta a complexidade é reduzida e você pode usar o carregamento lento, onde os conjuntos de resultados criança só são carregados após a primeira solicitação.

Fornecedor com um relacionamento um-para-muitos com o produto. Um fornecedor tem (suprimentos) muitos produtos.

***** 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      |
+-----+-----------+--------------------+-------+------------+

Fatores:

  • modo preguiçoso por set Fornecedor para “true” (padrão)

  • modo usado para consultar sobre o produto Fetch é Select

  • Modo (padrão) Fetch: Informações do fornecedor é acessado

  • Caching não desempenhar um papel para a primeira vez que o

  • Fornecedor é acessado

Obter modo é Select Fetch (padrão)

// 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 instrução de seleção para o produto
  • N declarações select para Fornecedor

Esta é N + 1 seleciona problema!

Eu não posso comentar diretamente sobre outras respostas, porque eu não tenho reputação suficiente. Mas vale a pena notar que o problema essencialmente apenas surge porque, historicamente, um monte de dbms ter sido muito pobre quando se trata de manipulação de junta (MySQL é um exemplo particularmente notável). Então n + 1 tem, muitas vezes, sido notavelmente mais rápido do que uma junção. E depois há maneiras de melhorar em n + 1, mas ainda sem a necessidade uma junção, que é o que o problema original se refere a.

No entanto, o MySQL é agora muito melhor do que costumava ser quando se trata de junta. Quando eu aprendi MySQL, eu costumava se junta um monte. Então eu descobri quão lento eles são, e mudou para n + 1 no código em vez disso. Mas, recentemente, eu estive de volta à junta-se em movimento, porque MySQL é agora um pedaço de um monte melhor em lidar com eles do que era quando eu comecei a usá-lo.

Estes dias, uma junção simples em um conjunto devidamente indexado de mesas raramente é um problema, em termos de desempenho. E se ele dá um acerto de desempenho, então o uso de dicas de índice muitas vezes resolve-los.

Esta é discutido aqui por um da equipe de desenvolvimento do MySQL:

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

Assim, o resumo é: Se você está evitando junta-se no passado por causa do péssimo desempenho do MySQL com eles, então tente novamente nas últimas versões. Você provavelmente vai ser agradavelmente surpreendido.

Nós nos afastamos do ORM no Django devido a este problema. Basicamente, se você tentar fazer

for p in person:
    print p.car.colour

O ORM todo o prazer voltar todas as pessoas (normalmente como instâncias de um objeto Person), mas, em seguida, ele terá de consultar a tabela carro para cada pessoa.

Um simples e muito eficaz abordagem para isso é algo chamada I " dobragem em leque ", o que evita a idéia absurda de que os resultados da consulta de uma base de dados relacional deve mapear de volta para as tabelas originais a partir do qual a consulta é composta.

Passo 1: Ampla escolha

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

Isso irá retornar 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

Passo 2: Objectify

sugar os resultados em um criador objeto genérico com um argumento para divisão após o terceiro item. Isto significa que o objeto "jones" não vai ser feita mais de uma vez.

Passo 3: Renda

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

desta página web para uma implementação de dobragem em leque para python.

Suponha que você tem a empresa eo empregado. Empresa tem muitos funcionários (ou seja funcionário tem um company_id campo).

Em algumas configurações O / R, quando você tem um objeto Empresa mapeados e ir para acessar seus objetos Employee, a ferramenta O / R vai fazer uma escolha para todos os funcionários, wheras se estivesse apenas fazendo as coisas em SQL reta, você poderia select * from employees where company_id = XX. Assim N (número de empregados) mais 1 (empresa)

Esta é a forma como as versões iniciais de Beans EJB de entidade funcionou. Eu acredito que coisas como Hibernate ter eliminado a isso, mas eu não estou muito certo. A maioria das ferramentas geralmente incluem informações como a sua estratégia para o mapeamento.

Aqui está uma boa descrição do problema - https://web.archive.org/web/20160310145416/http://www.realsolve.co.uk/site/tech/hib-tip -pitfall.php? name = why-preguiçoso

Agora que você entende o problema que geralmente pode ser evitada fazendo uma junção buscar em sua consulta. Este basicamente obriga a busca do objecto carregado preguiçoso assim os dados são recuperados em uma consulta em vez de n + 1 consultas. Espero que isso ajude.

Em minha opinião, o artigo escrito em Hibernate Pitfall: relacionamentos por que deve ser preguiçoso é exatamente o oposto de N verdadeira + 1 questão é

.

Se precisar de explicação correta consulte Hibernate - Capítulo 19: Melhorar o desempenho - Buscando Estratégias

Select fetching (o padrão) é extremamente vulnerável a n + 1 selecciona problemas, então podemos querer permitir join

Verifique Ayende post sobre o tema: Combate Selecionar N + 1 Problema Em NHibernate

Basicamente, ao usar um ORM como NHibernate ou EntityFramework, se você tem um um-para-muitos (mestre-detalhe) relacionamento, e deseja listar todos os detalhes por cada registro mestre, você tem que fazer N + 1 query chamadas para o banco de dados, "N" é o número de registros mestre:. 1 consulta para obter todos os registros mestre e consultas N, um por registro mestre, para obter todos os detalhes por registro mestre

Mais chamadas de consulta de banco de dados -> mais tempo de latência -> diminuiu application / desempenho do banco de dados

.

No entanto, ORM tem opções para evitar esse problema, usando principalmente "junta".

A questão + 1 query N acontece quando você se esqueça de buscar uma associação e, em seguida, você precisa para acessá-lo:

List<PostComment> comments = entityManager.createQuery(
    "select pc " +
    "from PostComment pc " +
    "where pc.review = :review", PostComment.class)
.setParameter("review", review)
.getResultList();

LOGGER.info("Loaded {} comments", comments.size());

for(PostComment comment : comments) {
    LOGGER.info("The post title is '{}'", comment.getPost().getTitle());
}

que gera as seguintes instruções SQL:

SELECT pc.id AS id1_1_, pc.post_id AS post_id3_1_, pc.review AS review2_1_
FROM   post_comment pc
WHERE  pc.review = 'Excellent!'

INFO - Loaded 3 comments

SELECT pc.id AS id1_0_0_, pc.title AS title2_0_0_
FROM   post pc
WHERE  pc.id = 1

INFO - The post title is 'Post nr. 1'

SELECT pc.id AS id1_0_0_, pc.title AS title2_0_0_
FROM   post pc
WHERE  pc.id = 2

INFO - The post title is 'Post nr. 2'

SELECT pc.id AS id1_0_0_, pc.title AS title2_0_0_
FROM   post pc
WHERE  pc.id = 3

INFO - The post title is 'Post nr. 3'

Em primeiro lugar, o Hibernate executa a consulta JPQL, e uma lista de entidades PostComment é buscado.

Em seguida, para cada PostComment, a propriedade post associado é usado para gerar uma mensagem de registo contendo a título Post.

Porque a associação post não é inicializada, o Hibernate deve buscar a entidade Post com uma consulta secundária e para entidades PostComment N, N mais consultas vão ser executada (daí o N + 1 problema de consulta).

Primeiro, você precisa logging SQL adequada e monitoramento de modo que você pode manchar esta questão.

Em segundo lugar, esse tipo de problema é melhor ser pego por testes de integração. Você pode usar um automática JUnit assert para validar a contagem esperada de instruções SQL gerado . A db-unidade do projeto já oferece essa funcionalidade, e é open source.

Quando você identificou o problema da consulta N + 1, você precisa usar um JOIN FETCH para que associações criança são buscados em uma consulta, em vez de N . Se você precisa buscar várias associações criança, é melhor buscar uma coleção na consulta inicial eo segundo com uma consulta SQL secundário.

O link fornecido tem uma maneira muito simples exemplo do n + 1 problema. Se você aplicá-lo para o Hibernate é basicamente falando sobre a mesma coisa. Quando você consulta para um objeto, a entidade está carregado, mas nenhuma associação (a menos que configurado de outra forma) vai ser preguiçoso carregado. Daí uma consulta para os objetos raiz e outra consulta para carregar as associações de cada uma delas. 100 objetos retornados meios 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-selecciona-explicado /

Um milionário tem N de carros. Você deseja obter tudo (4) rodas.

Um (1) consulta carrega todos os carros, mas para cada um (N) carro uma consulta separada é submetida para as rodas de carga.

Custos:

Suponha índices caber na RAM.

1 + N análise de consulta e aplainamento + busca índice E 1 + N + (N * 4) de acesso placa de carregamento de carga útil.

Suponha índices não se encaixam na RAM.

custos adicionais no pior dos casos uma placa N + acessos para o índice de carregamento.

Resumo

Garrafa pescoço é o acesso placa (cerca de 70 vezes por segundo acesso aleatório em HDD) Um ansioso juntar seleccionar também aceder a placa 1 vezes + N + (N * 4) para a carga útil. Então, se os índices se encaixam ram -. Nenhum problema, o seu rápido o suficiente, porque só ram operações envolvidas

É muito mais rápido para emitir uma consulta que retorna 100 resultados do que a emissão de 100 consultas que cada resultado de retorno 1.

N + 1 seleciona questão é uma dor, e faz sentido para detectar tais casos em testes de unidade. Eu desenvolvi uma pequena biblioteca para a verificação do número de consultas executadas por um determinado método de teste ou apenas um bloco arbitrário de código - JDBC Sniffer

Basta adicionar uma regra JUnit especial à sua classe de teste e coloque a anotação com o número esperado de consultas sobre os seus métodos de ensaio:

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

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

A questão como outros declarou mais elegante é que você quer ter um produto cartesiano das colunas OneToMany ou você está fazendo N + 1 Selecciona. De qualquer possível de resultados gigantesco ou tagarela com o banco de dados, respectivamente.

Estou surpreso este não é mencionado, mas assim que eu comecei contornar este problema ... Eu faço uma mesa semi-temporário ids . Eu também faço isso quando você tem o IN () limitação cláusula .

Isto não funciona para todos os casos (provavelmente nem mesmo a maioria) mas funciona particularmente bem se você tem um monte de objetos filho de tal forma que o produto cartesiano vai sair da mão (ou seja, muitas colunas OneToMany o número de resultados serão uma multiplicação das colunas) e seus mais de um lote como trabalho.

Primeiro você inserir seus ids objeto pai lote em uma tabela ids. Este BATCH_ID é algo que geramos em nosso aplicativo e agarrar.

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

Agora, para cada coluna OneToMany você acabou de fazer um SELECT na mesa de ids INNER JOINing tabela filho com um WHERE batch_id= (ou vice-versa). Você só quer ter certeza de que você encomendar pela coluna id como ele vai fazer a fusão colunas de resultado mais fácil (caso contrário, você vai precisar de um HashMap / Mesa para todo o conjunto de resultados que pode não ser tão ruim).

Então você acabou de limpar periodicamente a tabela de ids.

Isso também funciona particularmente bem se o usuário seleciona dizer 100 ou mais itens distintos para algum tipo de processamento em massa. Coloque os 100 ids distintas na tabela temporária.

Agora, o número de consultas que você está fazendo é pelo número de colunas OneToMany.

Leve Matt Solnit exemplo, imagine que você define uma associação entre carro e rodas como preguiçoso e você precisa de alguns campos de rodas. Isto significa que após a primeira escolha, o Hibernate vai fazer "Select * from Wheels onde car_id =: id". Para cada carro

Isto torna a primeira escolha e mais 1 seleciona por cada carro N, é por isso que ele é chamado n + 1 problema.

Para evitar isso, fazer a associação buscar tão ansioso, para que cargas de hibernação dados com uma junção.

Mas atenção, se muitas vezes você não acesso associado Rodas, é melhor mantê-lo preguiçoso ou mudança de tipo de busca com critérios.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top