Wat is het “N+1 selects-probleem” in ORM (Object-Relational Mapping)?

Het “N+1-selecteert probleem” wordt over het algemeen genoemd als een probleem in discussies over object-relationele mapping (ORM), en ik begrijp dat het iets te maken heeft met het moeten maken van veel databasequery’s voor iets dat eenvoudig lijkt in de objectwereld.

Heeft iemand een meer gedetailleerde uitleg van het probleem?


Antwoord 1, autoriteit 100%

Stel dat u een verzameling Car-objecten (databaserijen) heeft en dat elke Careen verzameling Wheel-objecten heeft (ook rijen ). Met andere woorden, CarWheelis een 1-op-veel-relatie.

Laten we zeggen dat je alle auto’s moet doorlopen en voor elke auto een lijst met wielen moet afdrukken. De naïeve O/R-implementatie zou het volgende doen:

SELECT * FROM Cars;

En dan voor elke Car:

SELECT * FROM Wheel WHERE CarId = ?

Met andere woorden, je hebt één selectie voor de auto’s en vervolgens N extra selecties, waarbij N het totale aantal auto’s is.

Als alternatief zou je alle wielen kunnen pakken en de zoekopdrachten in het geheugen kunnen uitvoeren:

SELECT * FROM Wheel

Dit vermindert het aantal retourvluchten naar de database van N+1 naar 2.
De meeste ORM-tools bieden u verschillende manieren om N+1-selecties te voorkomen.

Referentie: Java Persistence with Hibernate, hoofdstuk 13.


Antwoord 2, autoriteit 10%

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

Dat krijgt u een resultaat instellen waar kinderrijen in tabel2 duplicatie veroorzaken door de tabel1-resultaten voor elke kindrij in Tabel2 te retourneren. O / R Mappers moeten tabel1-instanties onderscheiden op basis van een uniek sleutelveld, vervolgens alle tabel2-kolommen gebruiken om kinderinstanties te vullen.

SELECT table1.*
SELECT table2.* WHERE SomeFkId = #

De N + 1 is waar de eerste query het primaire object vult en de tweede query alle kinderobjecten vult voor elk van de unieke primaire objecten die worden geretourneerd.

Overweeg:

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

en tabellen met een vergelijkbare structuur. Een enkele query voor het adres “22 valley st” kan terugkeren:

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

De O / RM moet een exemplaar van thuis vullen met ID = 1, adres = “22 vallei st” en vervolgens de array van de inwoners vullen met mensen voor Dave, John en Mike met slechts één query.

A N + 1-query voor hetzelfde adres dat hierboven is gebruikt, zou resulteren in:

Id Address
1  22 Valley St

Met een afzonderlijke query zoals

SELECT * FROM Person WHERE HouseId = 1

en resulterend in een afzonderlijke gegevensset zoals

Name    HouseId
Dave    1
John    1
Mike    1

en het eindresultaat als hetzelfde als hierboven met de Single Query.

De voordelen voor Single Select is dat u alle gegevens voor de voorkant krijgt die mogelijk is wat u uiteindelijk verlangt. De voordelen aan N + 1 zijn querycomplexiteit wordt verminderd en u kunt luie laden gebruiken waar de instellingen van het kindresultaten alleen op eerste verzoek worden geladen.


Antwoord 3, autoriteit 8%

Wat is het N+1-queryprobleem

Het N+1-queryprobleem treedt op wanneer het raamwerk voor gegevenstoegang N extra SQL-instructies heeft uitgevoerd om dezelfde gegevens op te halen die hadden kunnen worden opgehaald bij het uitvoeren van de primaire SQL-query.

Hoe groter de waarde van N, hoe meer query’s worden uitgevoerd, hoe groter de impact op de prestaties. En, in tegenstelling tot het log met trage zoekopdrachten, waarmee u langzaam lopende zoekopdrachten kunt vinden, zal het N+1-probleem niet worden opgelost, omdat elke afzonderlijke aanvullende zoekopdracht voldoende snel wordt uitgevoerd om het log met trage zoekopdrachten niet te activeren.

Het probleem is het uitvoeren van een groot aantal aanvullende zoekopdrachten die over het algemeen voldoende tijd in beslag nemen om de reactietijd te vertragen.

Laten we bedenken dat we de volgende databasetabellen post en post_comments hebben die een één-op-veel-tabelrelatie vormen:

We gaan de volgende 4 postrijen maken:

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)

En we zullen ook 4 post_commentonderliggende records maken:

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)

N+1-queryprobleem met gewone SQL

Als u de post_commentsselecteert met deze SQL-query:

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

En later besluit je de bijbehorende posttitleop te halen voor elke 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
    );
}

Je gaat het probleem met de N+1-query activeren omdat je in plaats van één SQL-query 5 (1 + 4) hebt uitgevoerd:

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'

Bevestiging Het N + 1-query-probleem is heel eenvoudig. Het enige wat u hoeft te doen, is alle gegevens die u nodig hebt in de originele SQL-query, zoals deze:

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

Deze keer wordt slechts één SQL-query uitgevoerd om alle gegevens te halen die we verder geïnteresseerd zijn in het gebruik.

N + 1 Query-probleem met JPA en HIBERNATE

Bij gebruik van JPA en Hibernate zijn er verschillende manieren waarop u de N + 1-query-probleem kunt triggeren, dus het is erg belangrijk om te weten hoe u deze situaties kunt vermijden.

Overweeg voor de volgende voorbeelden, overweeg we de posten post_commentsTabellen naar de volgende entiteiten:

De JPA-toewijzingen zien er als volgt uit:

@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

Het is een slecht idee om FetchType.EAGERimpliciet of expliciet te gebruiken voor uw JPA-associaties, omdat u veel meer gegevens gaat ophalen die u nodig hebt. Bovendien is de FetchType.EAGER-strategie ook gevoelig voor N+1-queryproblemen.

Helaas gebruiken de koppelingen @ManyToOneen @OneToOnestandaard FetchType.EAGER, dus als uw toewijzingen er als volgt uitzien:

@ManyToOne
private Post post;

U gebruikt de FetchType.EAGER-strategie en elke keer dat u vergeet JOIN FETCHte gebruiken bij het laden van sommige PostComment-entiteiten met een JPQL of Criteria API-query:

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

U gaat het probleem met de N+1-query activeren:

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

Let op de extra SELECT-instructies die worden uitgevoerd omdat de koppeling postmoet worden opgehaald voordat de Listmet PostComment-entiteiten wordt geretourneerd.

In tegenstelling tot het standaard ophaalplan, dat u gebruikt bij het aanroepen van de find-methode van de EnrityManager, definieert een JPQL- of Criteria API-query een expliciet plan dat Hibernate niet kan wijzigen door automatisch een JOIN FETCH te injecteren. U moet het dus handmatig doen.

Als u niet de postAssociation hebt gekregen, heeft u veel geluk bij het gebruik van FetchType.EAGERomdat er geen manier is om het te vermijden. Daarom is het beter om FetchType.LAZYstandaard te gebruiken.

Maar als u postAssociation wilt gebruiken, kunt u JOIN FETCHgebruiken om het N + 1-queryprobleem te vermijden:

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

Deze keer voert Hibernate een enkele SQL-instructie uit:

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'

FetchType.LAZY

Zelfs als u overschakelt naar het gebruik van FetchType.LAZYExpliciet voor alle associaties, kunt u nog steeds tegen het N + 1-probleem komen.

Deze keer, de postAssociation wordt in kaart gebracht:

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

Als u nu de entiteiten PostCommentophaalt:

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

Hibernate voert een enkele SQL-instructie uit:

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

Maar als je daarna gaat verwijzen naar de luie geladen post-associatie:

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

U krijgt het probleem met de N+1-query:

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'

Omdat de post-koppeling lui wordt opgehaald, wordt een secundaire SQL-instructie uitgevoerd bij toegang tot de luie koppeling om het logbericht op te bouwen.

Nogmaals, de oplossing bestaat uit het toevoegen van een JOIN FETCH-clausule aan de JPQL-query:

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

En, net als in het voorbeeld van FetchType.EAGER, genereert deze JPQL-query een enkele SQL-instructie.

Zelfs als u FetchType.LAZYgebruikt en niet verwijst naar de onderliggende associatie van een bidirectionele @OneToOneJPA-relatie, kunt u nog steeds de N+1-query activeren probleem.

Het probleem met de N+1-query automatisch detecteren

Als u automatisch een N+1-queryprobleem in uw gegevenstoegangslaag wilt detecteren, kunt u de db-utilopen source-project.

Eerst moet u de volgende Maven-afhankelijkheid toevoegen:

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

Daarna hoeft u alleen het hulpprogramma SQLStatementCountValidatorte gebruiken om de onderliggende SQL-instructies die worden gegenereerd, te bevestigen:

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

Als u FetchType.EAGERgebruikt en de bovenstaande testcase uitvoert, krijgt u de volgende testcasefout:

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!

Antwoord 4, autoriteit 5%

Leverancier met een één-op-veel relatie met Product. Eén leverancier heeft (levert) veel producten.

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

Factoren:

  • Luie modus voor leverancier ingesteld op “true” (standaard)

  • De ophaalmodus die wordt gebruikt voor zoekopdrachten op het product is Selecteren

  • Ophaalmodus (standaard): Leveranciersinformatie is toegankelijk

  • Caching speelt voor het eerst geen rol bij de

  • Leverancier is toegankelijk

Ophaalmodus is Ophalen selecteren (standaard)

// 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=?

Resultaat:

  • 1 select-statement voor Product
  • N selecteer verklaringen voor leverancier

Dit is een N+1 selectieprobleem!


Antwoord 5, autoriteit 3%

Ik kan niet direct reageren op andere antwoorden, omdat ik niet genoeg reputatie heb. Maar het is vermeldenswaard dat het probleem zich in wezen alleen voordoet omdat, historisch gezien, veel dbms behoorlijk slecht waren als het gaat om het afhandelen van joins (MySQL is een bijzonder opmerkelijk voorbeeld). Dus n+1 is vaak opmerkelijk sneller geweest dan een join. En dan zijn er manieren om n+1 te verbeteren, maar nog steeds zonder dat je een join nodig hebt, en dat is waar het oorspronkelijke probleem mee te maken heeft.

MySQL is nu echter een stuk beter dan vroeger als het op joins aankomt. Toen ik voor het eerst MySQL leerde, gebruikte ik veel joins. Toen ontdekte ik hoe traag ze zijn, en schakelde in plaats daarvan over naar n+1 in de code. Maar sinds kort ben ik weer overgestapt op joins, omdat MySQL er nu een stuk beter mee omgaat dan toen ik het voor het eerst begon te gebruiken.

Tegenwoordig is een simpele join op een correct geïndexeerde set tabellen zelden een probleem, in termen van prestatie. En als het een prestatiehit oplevert, lost het gebruik van indexhints ze vaak op.

Dit wordt hier besproken door een van de MySQL-ontwikkelteams:

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

De samenvatting is dus: als je in het verleden joins hebt vermeden vanwege de slechte prestaties van MySQL, probeer het dan opnieuw met de nieuwste versies. Je zult waarschijnlijk aangenaam verrast zijn.


Antwoord 6, autoriteit 2%

We zijn vanwege dit probleem weggegaan van de ORM in Django. Kortom, als je het probeert en doet

for p in person:
    print p.car.colour

De ORM retourneert graag alle mensen (meestal als instanties van een Persoonsobject), maar dan moet het de autotabel voor elke Persoon opvragen.

Een eenvoudige en zeer effectieve benadering hiervoor is iets dat ik “fanfolding” noem, waarmee het onzinnige idee wordt vermeden dat queryresultaten van een relationele database moeten worden terugverwezen naar de oorspronkelijke tabellen waaruit de query is afgeleid. samengesteld.

Stap 1: Brede selectie

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

Dit geeft iets terug als

 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

Stap 2: Objectiveren

Zoek de resultaten op in een generiek object-maker met een argument om te splitsen na het derde item. Dit betekent dat het “jones”-object niet meer dan één keer zal worden gemaakt.

Stap 3: Renderen

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

Zie deze webpaginavoor een implementatie van fanfoldingvoor python.


Antwoord 7, autoriteit 2%

Hier is een goede beschrijving van het probleem

Nu u het probleem begrijpt, kan het doorgaans worden vermeden door een lid te worden in uw vraag. Dit dwingt in feite de fetch van het luie geladen object, zodat de gegevens in één query worden opgehaald in plaats van N + 1-query’s. Ik hoop dat dit helpt.


8, Autoriteit 2%

Stel dat u bedrijf en werknemer hebt. Het bedrijf heeft veel werknemers (d.w.z. medewerker heeft een veldcompany_id).

In sommige O / R-configuraties, wanneer u een in kaart gebrachte bedrijfsobject hebt en toegang hebt om toegang te krijgen tot zijn werknemersobjecten, zal de O / R-tool er één selecteren voor elke medewerker, waarwel u gewoon dingen in rechte SQL aan het doen was, jij kan select * from employees where company_id = XX. Dus n (aantal werknemers) plus 1 (bedrijf)

Dit is hoe de eerste versies van EJB-entiteitsbonen werkten. Ik geloof dat dingen zoals overwinning hiermee hebben gedaan, maar ik ben niet zo zeker. De meeste tools bevatten meestal informatie over hun strategie voor de toewijzing.


9

Controleer Ayende Post op het onderwerp: Het bestrijden van het select N + 1-probleem in NHIBERNATE .

In principe, bij gebruik van een ORM, zoals nhibernate of entityframework, als u een relatie in één-op-veel (masterdetails) hebt en alle details per elk masterrecord wilt vermelden, moet u N + 1-query maken Oproepen naar de database, “n” als het aantal masterrecords: 1 query om alle masterrecords en N-query’s, één per master-record te krijgen, om alle details per masterrecord te krijgen.

Meer database-queryoproepen → Meer Latency-tijd → Verlaagde toepassing / databaseprestaties.

ORMS hebben echter opties om dit probleem te voorkomen, voornamelijk met behulp van joins.


10

Het is veel sneller om 1 query af te geven die 100 resultaten retourneert dan om 100 query’s uit te geven die elk 1 resultaat retourneert.


11

Naar mijn mening is het artikel geschreven in Hibernate-valkuil: waarom relaties lui moeten zijn is precies het tegenovergestelde van het echte nummer van N + 1 is.

Als u de juiste uitleg nodig hebt, raadpleegt u HEIBERNATE – HOOFDSTUK 19: Verbetering van de prestaties – Fetching-strategieën

Selecteer het halen (de standaard) is
Zeer kwetsbaar voor n + 1 selecteert
problemen, dus we willen misschien in staat
Doe mee met het ophalen


12

De meegeleverde link heeft een heel eenvoudig voorbeeld van het N + 1-probleem. Als je het toepast op Hibernate, is het in principe over hetzelfde. Wanneer u zoekt voor een object, wordt de entiteit geladen, maar alle verenigingen (tenzij anders geconfigureerd) lui worden geladen. Vandaar één vraag voor de rootobjecten en een andere query om de associaties voor elk van deze te laden. 100 geretourneerde objecten betekent één eerste query en vervolgens 100 aanvullende query’s om de vereniging voor elk, N + 1 te krijgen.

http://pramatr.com/2009/02 / 05 / SQL-N-1-selects-uitgelegd /


13

Eén miljonair heeft n-auto’s. U wilt alle (4) wielen krijgen.

Eén (1) query laadt alle auto’s, maar voor elke (n) auto wordt een afzonderlijke query ingediend voor laadwielen.

Kosten:

Neem aan om indexen in RAM aan te passen.

1 + N query ontleden en schaven + index zoeken EN 1 + N + (N * 4) plaattoegang voor het laden van nuttige lading.

Stel dat indexen niet in ram passen.

Extra kosten in het slechtste geval 1 + N plaattoegangen voor laadindex.

Samenvatting

Fleshals is plaattoegang (ca. 70 keer per seconde willekeurige toegang op hdd)
Een enthousiaste join-select zou ook 1 + N + (N * 4) keer toegang hebben tot de plaat voor nuttige lading.
Dus als de indexen in ram passen – geen probleem, het is snel genoeg omdat er alleen ram-bewerkingen bij betrokken zijn.


Antwoord 14

N+1 select probleem is lastig, en het is logisch om dergelijke gevallen in unit tests te detecteren.
Ik heb een kleine bibliotheek ontwikkeld voor het verifiëren van het aantal query’s dat is uitgevoerd door een bepaalde testmethode of gewoon een willekeurig codeblok – JDBC Sniffer

Voeg gewoon een speciale JUnit-regel toe aan uw testklasse en plaats een annotatie met het verwachte aantal zoekopdrachten op uw testmethoden:

@Rule
public final QueryCounter queryCounter = new QueryCounter();
@Expectation(atMost = 3)
@Test
public void testInvokingDatabase() {
    // your JDBC or JPA code
}

15

Zonder inch-stapel implementatiedetails te gaan, zijn er architectonisch gezien dat er minstens twee oplossingen zijn voor N + 1-probleem:

  • Heb maar 1 – Big Query – met joins. Dit levert veel informatie uit de database naar de applicatielaag, vooral als er meerdere kinderrecords zijn. Het typische resultaat van een database is een reeks rijen, geen grafiek van objecten (er zijn oplossingen voor die met verschillende DB-systemen)
  • Heb twee (of meer voor meer kinderen moesten worden toegevoegd) Query’s – 1 voor de ouder en nadat je ze hebt – doorzoek de kinderen op ID’s en breng ze in kaart. Dit minimaliseert de gegevensoverdracht tussen de DB- en APP-lagen.

Antwoord 16

Neem het voorbeeld van Matt Solnit, stel je voor dat je een associatie tussen Car en Wheels als LAZY definieert en dat je een aantal Wheels-velden nodig hebt. Dit betekent dat de slaapstand na de eerste selectie “Select * from Wheels where car_id = :id” voor ELKE auto gaat doen.

Dit maakt de eerste selectie en meer 1 selectie door elke N auto, daarom wordt het n+1 probleem genoemd.

Om dit te voorkomen, moet u ervoor zorgen dat de koppeling zo gretig wordt opgehaald, zodat de slaapstand gegevens laadt met een join.

Maar let op, als je de bijbehorende Wheels vaak niet gebruikt, is het beter om het LAZY te houden of het ophaaltype te wijzigen met Criteria.


Antwoord 17

N+1 SELECT-probleem is echt moeilijk te herkennen, vooral in projecten met een groot domein, tot het moment dat het de prestaties begint te verminderen. Zelfs als het probleem is opgelost, d.w.z. door gretig laden toe te voegen, kan een verdere ontwikkeling de oplossing breken en/of het N+1 SELECT-probleem op andere plaatsen opnieuw introduceren.

Ik heb een open source-bibliotheek jplusonegemaakt om die problemen in op JPA gebaseerde Spring Boot Java-toepassingen aan te pakken . De bibliotheek biedt twee belangrijke functies:

  1. Genereert rapporten die SQL-statements correleren met uitvoeringen van JPA-bewerkingen die ze hebben getriggerd en plaatst in de broncode van uw applicatie die erbij betrokken was
2020-10-22 18:41:43.236 DEBUG 14913 --- [ main] c.a.j.core.report.ReportGenerator:
  WORTEL
    com.adgadev.jplusone.test.domain.bookshop.BookshopControllerTest.shouldGetBookDetailsLazily(BookshopControllerTest.java:65)
com.adgadev.jplusone.test.domain.bookshop.bookshopcontroller.getsampleBookinglazyloading (BookshopController.java:31)
    com.adgadev.jplusone.test.domain.bookshop.bookshopservice.getSampleBookDetailSusinglazyloading [proxy]
      Sessiegrens
        Operatie [impliciet]
          com.adgadev.jplusone.test.domain.bookshop.bookshopservice.getsampleBookDetailSusinglazyloading (Bookshopservice.java:35)
          com.adgadev.jplusone.test.domain.bookshop.author.getName [proxy]
          com.adgadev.jplusone.test.domain.bookshop.autuister [fetching entiteit]
            Verklaring [lees]
              selecteer uit
                Auteur Author0_
                Links Outer Join Genre Genre1_ op author0_.genre_id = genre1_.id
              waar
                author0_.id = 1
        Operatie [impliciet]
          com.adgadev.jplusone.test.domain.bookshop.bookshopservice.getsampleBookDetailSusinglazyloading (Bookshopservice.java:36)
          com.adgadev.jplusone.test.domain.bookshop.author.countwrittenbooks (author.java:53)
          com.adgadev.jplusone.test.domain.bookshop.author.books [fetching collectie]
            Verklaring [lees]
              selecteer uit
                Book Books0_
              waar
                Books0_.author_id = 1
  1. Biedt API die het mogelijk maakt tests te schrijven die controleren hoe effectief uw aanvraag JPA (d.w.z. beweert hoeveelheid luie laadbewerkingen)
@SpringBootTest
class LazyLoadingTest {
    @Autowired
    private JPlusOneAssertionContext assertionContext;
    @Autowired
    private SampleService sampleService;
    @Test
    public void shouldBusinessCheckOperationAgainstJPlusOneAssertionRule() {
        JPlusOneAssertionRule rule = JPlusOneAssertionRule
                .within().lastSession()
                .shouldBe().noImplicitOperations().exceptAnyOf(exclusions -> exclusions
                        .loadingEntity(Author.class).times(atMost(2))
                        .loadingCollection(Author.class, "books")
                );
        // trigger business operation which you wish to be asserted against the rule,
        // i.e. calling a service or sending request to your API controller
        sampleService.executeBusinessOperation();
        rule.check(assertionContext);
    }
}

Other episodes