O que é o "N + 1 seleciona o problema" no ORM (Mapeamento Relacional Objeto)?

1598

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?

Lars A. Brekken
fonte
2
Este é um ótimo link com uma boa explicação para entender o problema n + 1 . Ele também aborda as soluções para combater esse problema: architects.dzone.com/articles/how-identify-and-resilve-n1
aces.
Para todos que procuram solução para este problema, encontrei um post descrevendo-o. stackoverflow.com/questions/32453989/…
damndemon
2
Considerando as respostas, isso não deveria ser denominado como problema 1 + N? Como isso parece ser uma terminologia, não estou especificamente pedindo OP.
user1418717

Respostas:

1018

Digamos que você tenha uma coleção de Carobjetos (linhas do banco de dados) e cada Carum tenha uma coleção de Wheelobjetos (também linhas). Em outras palavras, CarWheelé 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:

SELECT * FROM Cars;

E então para cada um Car:

SELECT * FROM Wheel WHERE CarId = ?

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:

SELECT * FROM Wheel

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.

Matt Solnit
fonte
140
Para esclarecer sobre "Isso é ruim" - você pode obter todas as rodas com 1 select ( SELECT * from Wheel;), em vez de N + 1. Com um N grande, o impacto no desempenho pode ser muito significativo.
tucuxi
212
@ tucuxi Estou surpreso que você tenha recebido tantos votos por estar errado. Um banco de dados é muito bom em índices, fazendo a consulta para um CarID específico retornaria muito rapidamente. Mas se você tiver todas as rodas de uma vez, precisará procurar o CarID no seu aplicativo, que não está indexado, isso é mais lento. A menos que você tenha grandes problemas de latência, o alcance de seu banco de dados com n + 1 é realmente mais rápido - e sim, comparei-o com uma grande variedade de códigos do mundo real.
Ariel
74
@ariel A maneira 'correta' é obter todas as rodas, ordenadas pelo CarId (1 seleto), e se forem necessários mais detalhes do que o CarId, faça uma segunda consulta para todos os carros (total de 2 consultas). A impressão das coisas agora é ideal e não são necessários índices ou armazenamento secundário (é possível iterar nos resultados, não é necessário fazer o download de todos). Você comparou a coisa errada. Se você ainda está confiante em seus benchmarks, você se importaria de postar um comentário mais longo (ou uma resposta completa) explicando sua experiência e resultados?
Tucuxi
92
"O Hibernate (não estou familiarizado com os outros frameworks ORM) oferece várias maneiras de lidar com isso." e assim são?
Tima 12/01
58
@Ariel Tente executar seus benchmarks com servidores de banco de dados e aplicativos em máquinas separadas. Na minha experiência, as viagens de ida e volta ao banco de dados custam mais em sobrecarga do que a própria consulta. Então, sim, as consultas são muito rápidas, mas são as viagens de ida e volta que causam estragos. Eu converti "WHERE Id = const " para "WHERE Id IN ( const , const , ...)" e obtive ordens de magnitude a partir dele.
Hans
110
SELECT 
table1.*
, table2.*
INNER JOIN table2 ON table2.SomeFkId = table1.SomeId

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.

SELECT table1.*

SELECT table2.* WHERE SomeFkId = #

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:

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 tabelas com uma estrutura semelhante. Uma ú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 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:

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 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.

cfeduke
fonte
4
A outra vantagem de n + 1 é que é mais rápido porque o banco de dados pode retornar os resultados diretamente de um índice. A junção e a classificação requerem uma tabela temporária, mais lenta. O único motivo para evitar n + 1 é se você tem muita latência conversando com seu banco de dados.
Ariel
17
A associação e a classificação podem ser bastante rápidas (porque você ingressará nos campos indexados e possivelmente classificados). Qual é o tamanho do seu 'n + 1'? Você acredita seriamente que o problema n + 1 se aplica apenas a conexões de banco de dados de alta latência?
Tucuxi
9
@ariel - Seu conselho de que N + 1 é o "mais rápido" está errado, mesmo que seus valores de referência possam estar corretos. Como isso é possível? Veja en.wikipedia.org/wiki/Anecdotal_evidence , e também meu comentário na outra resposta a esta pergunta.
whitneyland
7
@ Ariel - Eu acho que entendi bem :). Só estou tentando ressaltar que seu resultado se aplica apenas a um conjunto de condições. Eu poderia facilmente construir um contra-exemplo que mostrasse o contrário. Isso faz sentido?
whitneyland
13
Para reiterar, o problema SELECT N + 1 é, em sua essência: tenho 600 registros para recuperar. É mais rápido obter todos os 600 deles em uma consulta ou 1 de cada vez em 600 consultas. A menos que você esteja no MyISAM e / ou tenha um esquema mal normalizado / mal indexado (nesse caso, o ORM não é o problema), um banco de dados ajustado corretamente retornará as 600 linhas em 2 ms, enquanto retorna as linhas individuais em cerca de 1 ms cada. Então, geralmente vemos o N + 1 levando centenas de milissegundos em que uma junção leva apenas um par
Dogs
64

Fornecedor com uma relação de um para muitos com o Produto. Um fornecedor possui (fornece) 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 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)

// 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 select para Product
  • N selecionar instruções para Fornecedor

Este é o problema de seleção N + 1!

Summy
fonte
3
Supõe-se que seja 1 selecionado para o fornecedor e depois N selecionado para o produto?
Bencampbell_14
@bencampbell_ Sim, inicialmente senti o mesmo. Mas, em seguida, com seu exemplo, é um produto para muitos fornecedores.
Mohd Faizan Khan
38

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.

Mark Goodge
fonte
7
Chamar versões anteriores do MySQL de um DBMS relacional é bastante extenso ... Se as pessoas que encontravam esses problemas usassem um banco de dados real, elas não teriam encontrado esses tipos de problemas. ;-)
Craig
2
Curiosamente, muitos desses tipos de problemas foram resolvidos no MySQL com a introdução e a otimização subsequente do mecanismo INNODB, mas você ainda encontrará pessoas tentando promover o MYISAM porque elas acham que é mais rápido.
Craig
5
Para sua informação, um dos três JOINalgoritmos 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.
Brandon
2
@Brandon Yes! Assim como as dicas JOIN e INDEX, forçar um determinado caminho de execução em todos os casos raramente ultrapassará o banco de dados. O banco de dados quase sempre é muito, muito bom em escolher a abordagem ideal para obter os dados. Talvez nos primeiros dias do dbs você precisasse `` formular '' sua pergunta de uma maneira peculiar para persuadir o db, mas depois de décadas de engenharia de classe mundial, agora você pode obter o melhor desempenho fazendo uma pergunta relacional ao banco de dados e deixando-a decida como buscar e reunir esses dados para você.
Dogs
3
Além de o banco de dados utilizar índices e estatísticas, todas as operações também são de E / S local, muitas das quais operam com cache altamente eficiente em vez de disco. Os programadores de banco de dados dedicam muita atenção à otimização desse tipo de coisa.
Craig
27

Afastamo-nos do ORM no Django por causa desse problema. Basicamente, se você tentar fazer

for p in person:
    print p.car.colour

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

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

Isso 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

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

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

Consulte esta página da Web para obter uma implementação de dobragem em ventilador para python.

rorycl
fonte
10
Estou tão feliz por ter tropeçado no seu post, porque pensei que estava ficando louco. Quando descobri o problema do N + 1, meu pensamento imediato foi- bem, por que você simplesmente não cria uma visão que contenha todas as informações necessárias e as extrai dessa visão? você validou minha posição. obrigado senhor.
um desenvolvedor
14
Afastamo-nos do ORM no Django por causa desse problema. Hã? O Django possui select_related, que tem o objetivo de resolver isso - de fato, seus documentos começam com um exemplo semelhante ao seu p.car.colourexemplo.
Adrian17
8
Esta é uma resposta antiga, temos select_related()e prefetch_related()agora no Django.
Mariusz Jamro
1
Legal. Mas select_related()e friend não parecem fazer nenhuma das extrapolações obviamente úteis de uma junção como LEFT 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.
Rorycl 15/09/19
26

Como essa é uma pergunta muito comum, escrevi este artigo , no qual essa resposta se baseia.

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 :

As tabelas <code> post </code> e <code> post_comments </code>

Vamos criar as 4 postlinhas a seguir :

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)

E também criaremos 4 post_commentregistros filhos:

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 com SQL simples

Se você selecionar post_commentsusando 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();

E, mais tarde, você decide buscar o associado post titlepara cada um 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
    );
}

Você acionará o problema de consulta N + 1 porque, em vez de uma consulta SQL, você executou 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'

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:

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
    );
}

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 poste post_commentspara as seguintes entidades:

Entidades <code> Post </code> e <code> PostComment </code>

Os mapeamentos JPA são assim:

@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

Usar de FetchType.EAGERforma 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.EAGERAlém disso , a estratégia também é propensa a problemas de consulta N + 1.

Infelizmente, as associações @ManyToOnee @OneToOneusam FetchType.EAGERpor padrão, portanto, se seus mapeamentos se parecerem com isso:

@ManyToOne
private Post post;

Você está usando a FetchType.EAGERestratégia e, toda vez que se esquecer de usar JOIN FETCHao carregar algumas PostCommententidades com uma consulta à API JPQL ou Critérios:

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

Você vai acionar o 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 as instruções SELECT adicionais que são executadas porque a postassociação precisa ser buscada antes do retorno Listdas PostCommententidades.

Diferentemente do plano de busca padrão, que você está usando ao chamar o findmétodo da EnrityManager, 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 postassociação, está sem sorte ao usá-la FetchType.EAGERporque não há como evitar a busca. É por isso que é melhor usar FetchType.LAZYpor padrão.

Mas, se você quiser usar a postassociação, poderá usar JOIN FETCHpara evitar o 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()
    );
}

Desta vez, o Hibernate executará uma única instrução 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 mais detalhes sobre por que você deve evitar a FetchType.EAGERestratégia de busca, consulte este artigo também.

FetchType.LAZY

Mesmo se você mudar para o uso FetchType.LAZYexplícito em todas as associações, ainda poderá encontrar o problema N + 1.

Desta vez, a postassociação é mapeada da seguinte maneira:

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

Agora, quando você busca as PostCommententidades:

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

O Hibernate irá executar uma única instrução SQL:

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

Mas, se depois, você fará referência à postassociação preguiçosa :

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

Você receberá o 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'

Como a postassociaçã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 FETCHcláusula à 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()
    );
}

E, como no FetchType.EAGERexemplo, essa consulta JPQL gerará uma única instrução SQL.

Mesmo se você estiver usando FetchType.LAZYe não fizer referência à associação filho de um @OneToOnerelacionamento JPA bidirecional , ainda poderá disparar o problema de consulta N + 1.

Para obter mais detalhes sobre como você pode superar o problema de consulta N + 1 gerado por @OneToOneassociações, consulte este artigo .

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-utilprojeto de código aberto.

Primeiro, você precisa adicionar a seguinte dependência do Maven:

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

Depois, você só precisa usar o SQLStatementCountValidatorutilitário para afirmar as instruções SQL subjacentes que são geradas:

SQLStatementCountValidator.reset();

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

SQLStatementCountValidator.assertSelectCount(1);

Caso você esteja usando FetchType.EAGERe execute o caso de teste acima, você receberá a seguinte falha do caso de teste:

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 mais detalhes sobre o db-utilprojeto de código aberto, consulte este artigo .

Vlad Mihalcea
fonte
Mas agora você tem um problema com paginação. Se você tiver 10 carros, cada um com 4 rodas e deseja paginar carros com 5 carros por página. Então você basicamente você tem 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.
CappY
2
Também tenho um artigo para isso.
Vlad Mihalcea
Obrigado pelo artigo. Eu lerei isto. Por rolagem rápida - eu vi que a solução é a função Window, mas elas são relativamente novas no MariaDB - então o problema persiste em versões mais antigas. :)
CappY
@ VladMihalcea, apontei no seu artigo ou no post toda vez que você se refere ao caso ManyToOne ao explicar o problema de N + 1. Mas, na verdade, as pessoas mais interessadas no caso OneToMany relacionadas ao problema N + 1. Você poderia consultar e explicar o caso OneToMany?
JJ Beam
18

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.

davetron5000
fonte
18

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.

Joe Dean
fonte
17

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.

Nathan
fonte
3
as junções não são uma boa solução (geralmente), porque podem resultar em um produto cartesiano, o que significa que o número de linhas de resultado é o número de resultados da tabela raiz multiplicado pelo número de resultados em cada tabela filho. particularmente ruim em vários níveis de herarquia. Selecionar 20 "blogs" com 100 "postagens" em cada um e 10 "comentários" em cada post resultará em 20000 linhas de resultados. O NHibernate possui soluções alternativas, como o "tamanho do lote" (selecione filhos com cláusula in nos IDs principais) ou "subseleção".
Erik Hart
14

É muito mais rápido emitir 1 consulta que retorna 100 resultados do que emitir 100 consultas que retornam 1 resultado.

jj_
fonte
13

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

A busca por seleção (o padrão) é extremamente vulnerável a problemas com N + 1, por isso, talvez desejemos ativar a busca por junção

Anoop Isaac
fonte
2
eu li a página de hibernação. Não diz qual é realmente o problema selecionado pelo N + 1 . Mas ele diz que você pode usar junções para corrigi-lo.
Ian Boyd
3
o tamanho do lote é necessário para a busca de seleção, para selecionar objetos filhos para vários pais em uma instrução de seleção. A subseleção pode ser outra alternativa. As junções podem ficar muito ruins se você tiver vários níveis de hierarquia e um produto cartesiano for criado.
Erik Hart
10

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
9

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.

Hans Wurst
fonte
9

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:

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

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

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 OneToManycolunas, 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.

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 OneToManycoluna, basta fazer um SELECTna tabela de IDs na tabela INNER JOINfilho com um WHERE 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.

Adam Gent
fonte
1

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.

martins.tuga
fonte
1
Novamente, as junções não são uma boa solução, especialmente quando mais de 2 níveis de hierarquia podem ser carregados. Marque "subselecionar" ou "tamanho do lote"; o último carregará os filhos pelos IDs pai na cláusula "in", como "select ... from wheels onde car_id in (1,3,4,6,7,8,11,13)".
Erik Hart