Training, Workshops, Softwareentwicklung

Hibernate Tutorial

Das n plus 1 Select Problem

Eines der häufigsten Performance-Probleme bei der Arbeit mit Hibernate entsteht durch das n plus 1 Select Problem beim Lazy Loading.

Im folgenden Beispiel enthält unsere Tabelle 10 Customer:

Setup
for (int i = 0; i < 10; i++) {
    Customer customer = new Customer();
    customer.setFirstname("Buck" + 1);
    customer.setLastname("Rogers");

    Invoice invoice = new Invoice();
    invoice.setInvoiceNo(String.valueOf(i));

    customer.getInvoices().add(invoice);
    em.persist(customer);
}

Wenn wir jetzt die Customer lesen und in einer Schleife verarbeiten, dann haben wir den Salat:

N plus 1 Selects
TypedQuery<Customer> query =
        em.createQuery("SELECT c FROM Customer c", Customer.class);
List<Customer> resultList = query.getResultList(); (1)

for(Customer customer:resultList) {
    for (Invoice invoice : customer.getInvoices()) { (2)
        assertNotNull( invoice.getInvoiceNo());
    }
}
assertSelectCount(11); (3)
1 Bei Diesem Zugriff werden 10 Customer geladen.
2 Erst wenn wir auf die Invoices zugreifen, werden die Invoices "Lazy" nachgeladen. Und zwar bei jedem Schleifendurchlauf
3 Insgesamt wurden 11 (10+1) Selects zur Datenbank geschickt

Obwohl also im Code nur ein Select zu sehen ist, haben wir hier tatsächlich elf Selects programmiert.

In der Regel kann man hier Abhilfe schaffen, in dem man beim ersten Select durch einen JOIN FETCH die Invoices gleich mit initialisiert.

Join Fetch
TypedQuery<Customer> query =
        em.createQuery("SELECT c FROM Customer c LEFT JOIN FETCH c.invoices", Customer.class);
List<Customer> resultList = query.getResultList(); (1)

for(Customer customer:resultList) {
    for (Invoice invoice : customer.getInvoices()) { (2)
        assertNotNull( invoice.getInvoiceNo());
    }
}
assertSelectCount(1); (3)
1 Bei diesem Zugriff werden 10 Customer mit ihren Invoices geladen.
2 Wenn wir auf die Invoices zugreifen, ist jetzt kein Select erforderlich
3 Insgesamt wurde genau 1 Select zur Datenbank geschickt

In dem Beispiel mit nur zehn Datensätzen sieht die Performance im Benchmark so aus:

Benchmark mit 10 Datensätzen
Benchmark                             Mode  Cnt        Score        Error  Units
BenchmarkNPlusOne.benchmarkJoinFetch  avgt   30   968265,444 ± 141722,564  ns/op
BenchmarkNPlusOne.benchmarkNPlusOne   avgt   30  1988617,726 ± 170424,294  ns/op

Es ist also bereits mehr als doppelt schnell mit einem Join.

Verwendet man 100 Datensätze, wirkt es sich noch deutlicher aus:

Benchmark mit 100 Datensätzen
Benchmark                             Mode  Cnt        Score        Error  Units
BenchmarkNPlusOne.benchmarkJoinFetch  avgt   30  1463578,944 ± 126455,780  ns/op
BenchmarkNPlusOne.benchmarkNPlusOne   avgt   30  3735851,618 ± 262605,569  ns/op

Trotzdem sei hier angemerkt, das man damit nicht alle Probleme lösen kann.

Oft wird nahezu inflationär mit Objektbeziehungen umgegangen, was zu einer Vielzahl von Joins und gleichzeitig einer Explosion der Datenmenge bei dem Ergebnis der Selects führt. Dann ist auch ein Lesen mit Join schnell ein Performance-Problem. Hier hilft tatsächlich nur eines: OneToMany Beziehungen mit Bedacht und sparsam einsetzen!

Um das zu verdeutlichen seht Ihr hier den Unterschied, wenn man nur die Customer alleine liest, im Vergleich zu dem Join.

Kosten des Joins
Benchmark                             Mode  Cnt        Score        Error  Units
BenchmarkNPlusOne.benchmarkFind       avgt   30  1116577,685 ±  84208,347  ns/op
BenchmarkNPlusOne.benchmarkJoinFetch  avgt   30  1463578,944 ± 126455,780  ns/op

Selbst ein einzelner Join kommt also nicht umsonst, und je mehr davon es werden, desto langsamer wird das ganze.