Entwickler-Blog · 07.03.19

Das n+1 Problem

Eine meiner Aufgaben bei MACH ist die Analyse und Behebung von Performance-Problemen. Dabei habe ich gelernt, dass es bei Standard-Anwendungen wie unserer ERP-Software nur selten ein ungünstiger Algorithmus allein ist, der eine Funktion langsam macht.

Nanosekunden und Millionen von Rechenschritten

Typischerweise ist es so, dass ein Anwender eine Reaktion des Systems als schnell wahrnimmt, wenn sie im Bereich von 0,5 - 1,5 Sekunden liegt. Hört sich nach wenig an, ist aber aus Sicht eines Computers sehr viel Zeit: Variablenzuweisungen und einfache Berechnungen laufen im Nanosekundenbereich ab. Selbst Millionen von Rechenschritten lassen sich also in weniger als 0,5 Sekunden abarbeiten.

Anders sieht die Sache aus, wenn Datenbanken ins Spiel kommen. Man kann ca. 1 - 10 Millisekunden für eine einfache Datenbank-Abfrage rechnen, je nachdem, wie viele Firewalls, Paketfilter und Ähnliches zwischen Datenbankserver und Applicationserver aufgebaut werden. Das ist an sich nicht problematisch, wird es aber, wenn wir es mit einem n+1 Problem zu tun haben.

"Zu einem n+1 Problem kommt es immer dann, wenn ein Stück Programmcode über eine Eingabe iteriert und in jedem Durchlauf eine Abfrage macht."

Dr. Jonathan Moebius Software Architect bei MACH

Zu abstrakt?

Dann ein Beispiel: Angenommen wir haben eine Anwendung, die uns eine Liste unserer Kunden zeigt. Weiter angenommen, wir wollen zusätzlich zu den eigentlichen Kundendaten noch den Umsatz wissen, den der Kunde bereits in diesem Jahr mit uns gemacht hat. Logischerweise sind die Kundendaten und die Umsatzdaten nicht in der gleichen Datenbank-Tabelle.

Ein ungünstiger Algorithmus macht das dann so:

Liste<Kundenumsatz> umsaetze = new ArrayList<>();
for (Kunde kunde: db.sucheKunden())

{ Umsatz umsatz = db.sucheUmsatzFuerKunde(kunde); umsaetze.add(new Kundenumsatz(kunde, umsatz)); }

Problematisch ist hier, dass der Umsatz für jeden Kunden einzeln aus der Datenbank geladen wird. Wird die Liste für eine kleine Zahl von Kunden gesucht, merkt man das Problem nicht. Aber spätestens, wenn jemand eine Liste mit mehr als 10.000 Kundenumsätzen laden möchte, wartet er zwischen 10 Sekunden und 1,5 Minuten auf die Antwort.

Profiler im Einsatz

Glücklicherweise sind solche Probleme mit einem Profiler, der Datenbank-Abfragen mitschreibt, recht schnell zu finden. Typischerweise geht aus der Beschreibung einer Fehlermeldung schon hervor, dass das Problem nicht immer da ist, sondern nur, wenn viele Ergebnisse vorliegen - spätestens dann kommt der Profiler zum Einsatz. Da würde ich dann erwarten, in der Auflistung der abgesetzten Datenbank-Abfragen zu sehen, dass deren Zahl umso größer wird, je mehr Kunden gefunden werden.

Die Lösung? Gar nicht so kompliziert!

Die Lösung für ein solches Problem ist meistens gar nicht so kompliziert. Im besten Fall kann die Datenbank gleich mittels "join" den Umsatz zu jedem Kunden mitliefern. Das ist in der Regel am effizientesten. In Fällen, in denen das nicht geht, bietet es sich an, die gewünschten Ergebnisse (hier Umsätze) für eine Liste von Kunden abzufragen. Dann sind es noch zwei Datenbank-Abfragen: Eine für die Liste der Kunden und eine für die Liste der Umsätze. Diese Zahl bleibt konstant - egal, wie lang die Liste ist. Und der Anwender erhält wieder nach 0,5 - 1,5 Sekunden eine Antwort.

Entwickler-Blog
#InsideMACH

Entwickler­rechner bestellt – Linux bekommen

Vor einiger Zeit stand in der Entwicklung der turnusmäßige Austausch der Entwicklerrechner an. Damit verbunden war der Wechsel von Windows 7 auf Windows 10.