Verbessern Sie INSERT-pro-Sekunde Leistung von SQLite? [c]


Answers

Versuchen Sie, SQLITE_STATIC anstelle von SQLITE_TRANSIENT für diese Einfügungen zu verwenden.

SQLITE_TRANSIENT wird dazu führen, dass SQLite die String-Daten vor der Rückkehr kopiert.

SQLITE_STATIC sagt, dass die Speicheradresse, die Sie gab, gültig ist, bis die Abfrage durchgeführt wurde (was in dieser Schleife immer der Fall ist). Dies wird Sie sparen mehrere zuzuordnen, kopieren und freigeben Operationen pro Schleife. Evtl. eine große verbesserung

Question

Die Optimierung von SQLite ist schwierig. Bulk-Insert-Performance einer C-Anwendung kann von 85 Einsätzen pro Sekunde bis über 96.000 Einsätze pro Sekunde variieren!

Hintergrund: Wir verwenden SQLite als Teil einer Desktop-Anwendung. Wir haben große Mengen an Konfigurationsdaten, die in XML-Dateien gespeichert sind, die analysiert und in eine SQLite-Datenbank geladen werden, um sie weiter zu verarbeiten, wenn die Anwendung initialisiert wird. SQLite ist ideal für diese Situation, weil es schnell ist, es erfordert keine spezialisierte Konfiguration, und die Datenbank wird auf der Festplatte als einzelne Datei gespeichert.

Begründung: Zuerst war ich enttäuscht von der Aufführung, die ich sah. Es stellt sich heraus, dass die Leistung von SQLite erheblich variieren kann (sowohl für Bulk-Inserts und Selects) je nachdem, wie die Datenbank konfiguriert ist und wie Sie die API verwenden. Es war keine triviale Angelegenheit, um herauszufinden, was alle Optionen und Techniken waren, also dachte ich, dass es umsichtig war, diesen Community-Wiki-Eintrag zu erstellen, um die Ergebnisse mit -Lesern zu teilen, um anderen die Mühe der gleichen Untersuchungen zu retten.

Das Experiment: Anstatt nur über Performance-Tipps im allgemeinen Sinne zu sprechen (zB "Use a transaction!" ), Dachte ich es am besten, einen C-Code zu schreiben und die Auswirkungen verschiedener Optionen zu messen . Wir werden mit einigen einfachen Daten beginnen:

  • Eine 28 MB TAB-getrennte Textdatei (ca. 865.000 Datensätze) des gesamten Transitplanes für die Stadt Toronto
  • Meine Testmaschine ist ein 3,60 GHz P4 mit Windows XP.
  • Der Code wird mit Visual C ++ 2005 als "Release" mit "Full Optimization" (/ Ox) und Favor Fast Code (/ Ot) kompiliert.
  • Ich benutze die SQLite "Amalgamation", die direkt in meine Testanwendung kompiliert wurde. Die SQLite-Version, die ich zufällig habe, ist ein bisschen älter (3.6.7), aber ich vermute, dass diese Ergebnisse mit der neuesten Version vergleichbar sind (bitte hinterlassen Sie einen Kommentar, wenn Sie anders denken).

Lass uns einen Code schreiben!

Der Code: Ein einfaches C-Programm, das die Textdatei zeilenweise liest, die Zeichenfolge in Werte aufteilt und dann die Daten in eine SQLite-Datenbank einfügt. In dieser "Baseline" -Version des Codes wird die Datenbank erstellt, aber wir werden keine Daten einfügen:

/*************************************************************
    Baseline code to experiment with SQLite performance.

    Input data is a 28 MB TAB-delimited text file of the
    complete Toronto Transit System schedule/route info
    from http://www.toronto.ca/open/datasets/ttc-routes/

**************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>
#include "sqlite3.h"

#define INPUTDATA "C:\\TTC_schedule_scheduleitem_10-27-2009.txt"
#define DATABASE "c:\\TTC_schedule_scheduleitem_10-27-2009.sqlite"
#define TABLE "CREATE TABLE IF NOT EXISTS TTC (id INTEGER PRIMARY KEY, Route_ID TEXT, Branch_Code TEXT, Version INTEGER, Stop INTEGER, Vehicle_Index INTEGER, Day Integer, Time TEXT)"
#define BUFFER_SIZE 256

int main(int argc, char **argv) {

    sqlite3 * db;
    sqlite3_stmt * stmt;
    char * sErrMsg = 0;
    char * tail = 0;
    int nRetCode;
    int n = 0;

    clock_t cStartClock;

    FILE * pFile;
    char sInputBuf [BUFFER_SIZE] = "\0";

    char * sRT = 0;  /* Route */
    char * sBR = 0;  /* Branch */
    char * sVR = 0;  /* Version */
    char * sST = 0;  /* Stop Number */
    char * sVI = 0;  /* Vehicle */
    char * sDT = 0;  /* Date */
    char * sTM = 0;  /* Time */

    char sSQL [BUFFER_SIZE] = "\0";

    /*********************************************/
    /* Open the Database and create the Schema */
    sqlite3_open(DATABASE, &db);
    sqlite3_exec(db, TABLE, NULL, NULL, &sErrMsg);

    /*********************************************/
    /* Open input file and import into Database*/
    cStartClock = clock();

    pFile = fopen (INPUTDATA,"r");
    while (!feof(pFile)) {

        fgets (sInputBuf, BUFFER_SIZE, pFile);

        sRT = strtok (sInputBuf, "\t");     /* Get Route */
        sBR = strtok (NULL, "\t");            /* Get Branch */
        sVR = strtok (NULL, "\t");            /* Get Version */
        sST = strtok (NULL, "\t");            /* Get Stop Number */
        sVI = strtok (NULL, "\t");            /* Get Vehicle */
        sDT = strtok (NULL, "\t");            /* Get Date */
        sTM = strtok (NULL, "\t");            /* Get Time */

        /* ACTUAL INSERT WILL GO HERE */

        n++;
    }
    fclose (pFile);

    printf("Imported %d records in %4.2f seconds\n", n, (clock() - cStartClock) / (double)CLOCKS_PER_SEC);

    sqlite3_close(db);
    return 0;
}

Die Kontrolle"

Das Ausführen des Codes als-ist nicht tatsächlich alle Datenbank-Operationen ausführen, aber es wird uns eine Vorstellung davon, wie schnell die rohe C-Datei I / O und String Verarbeitung Operationen sind.

Importierte 864913 Datensätze in 0,94 Sekunden

Groß! Wir können 920.000 Einsätze pro Sekunde machen, vorausgesetzt, wir machen eigentlich keine Einsätze :-)

Das "Worst-Case-Scenario"

Wir werden die SQL-String mit den aus der Datei gelesenen Werten generieren und diese SQL-Operation mit sqlite3_exec aufrufen:

sprintf(sSQL, "INSERT INTO TTC VALUES (NULL, '%s', '%s', '%s', '%s', '%s', '%s', '%s')", sRT, sBR, sVR, sST, sVI, sDT, sTM);
sqlite3_exec(db, sSQL, NULL, NULL, &sErrMsg);

Dies wird langsam sein, weil die SQL in VDBE-Code für jede Einfügung kompiliert werden und jede Einfügung wird in ihrer eigenen Transaktion passieren. Wie langsam

Importierte 864913 Datensätze in 9933.61 Sekunden

Yikes! 2 Stunden und 45 Minuten! Das sind nur 85 Einsätze pro Sekunde.

Verwenden einer Transaktion

Standardmäßig wird SQLite jede INSERT / UPDATE-Anweisung innerhalb einer eindeutigen Transaktion auswerten. Wenn Sie eine große Anzahl von Einsätzen ausführen, ist es ratsam, Ihren Betrieb in einer Transaktion zu verpacken:

sqlite3_exec(db, "BEGIN TRANSACTION", NULL, NULL, &sErrMsg);

pFile = fopen (INPUTDATA,"r");
while (!feof(pFile)) {

    ...

}
fclose (pFile);

sqlite3_exec(db, "END TRANSACTION", NULL, NULL, &sErrMsg);

Importierte 864913 Datensätze in 38.03 Sekunden

Das ist besser. Einfache Umwicklung aller unserer Einsätze in einer einzigen Transaktion verbesserte unsere Leistung auf 23.000 Einsätze pro Sekunde.

Verwenden einer vorbereiteten Anweisung

Die Verwendung einer Transaktion war eine große Verbesserung, aber das Neukompilieren der SQL-Anweisung für jede Einfügung macht keinen Sinn, wenn wir das gleiche SQL-Over-and-Over verwenden. Verwenden sqlite3_prepare_v2 , um unsere SQL-Anweisung einmal zu kompilieren und dann unsere Parameter an diese Anweisung mit sqlite3_bind_text :

/* Open input file and import into the database */
cStartClock = clock();

sprintf(sSQL, "INSERT INTO TTC VALUES (NULL, @RT, @BR, @VR, @ST, @VI, @DT, @TM)");
sqlite3_prepare_v2(db,  sSQL, BUFFER_SIZE, &stmt, &tail);

sqlite3_exec(db, "BEGIN TRANSACTION", NULL, NULL, &sErrMsg);

pFile = fopen (INPUTDATA,"r");
while (!feof(pFile)) {

    fgets (sInputBuf, BUFFER_SIZE, pFile);

    sRT = strtok (sInputBuf, "\t");   /* Get Route */
    sBR = strtok (NULL, "\t");        /* Get Branch */
    sVR = strtok (NULL, "\t");        /* Get Version */
    sST = strtok (NULL, "\t");        /* Get Stop Number */
    sVI = strtok (NULL, "\t");        /* Get Vehicle */
    sDT = strtok (NULL, "\t");        /* Get Date */
    sTM = strtok (NULL, "\t");        /* Get Time */

    sqlite3_bind_text(stmt, 1, sRT, -1, SQLITE_TRANSIENT);
    sqlite3_bind_text(stmt, 2, sBR, -1, SQLITE_TRANSIENT);
    sqlite3_bind_text(stmt, 3, sVR, -1, SQLITE_TRANSIENT);
    sqlite3_bind_text(stmt, 4, sST, -1, SQLITE_TRANSIENT);
    sqlite3_bind_text(stmt, 5, sVI, -1, SQLITE_TRANSIENT);
    sqlite3_bind_text(stmt, 6, sDT, -1, SQLITE_TRANSIENT);
    sqlite3_bind_text(stmt, 7, sTM, -1, SQLITE_TRANSIENT);

    sqlite3_step(stmt);

    sqlite3_clear_bindings(stmt);
    sqlite3_reset(stmt);

    n++;
}
fclose (pFile);

sqlite3_exec(db, "END TRANSACTION", NULL, NULL, &sErrMsg);

printf("Imported %d records in %4.2f seconds\n", n, (clock() - cStartClock) / (double)CLOCKS_PER_SEC);

sqlite3_finalize(stmt);
sqlite3_close(db);

return 0;

Importierte 864913 Datensätze in 16.27 Sekunden

Nett! Es gibt ein bisschen mehr Code (vergessen Sie nicht, sqlite3_clear_bindings und sqlite3_reset ), aber wir haben unsere Leistung auf 53.000 Einsätze pro Sekunde mehr als verdoppelt .

PRAGMA synchron = AUS

Standardmäßig wird SQLite nach dem Ausgeben eines Befehls auf OS-Ebene pausieren. Dadurch wird sichergestellt, dass die Daten auf die Festplatte geschrieben werden. Durch das Setzen von synchronous = OFF werden wir SQLite anweisen, die Daten einfach zum Schreiben zu übergeben und dann weiter zu fahren. Es besteht die Chance, dass die Datenbankdatei beschädigt wird, wenn der Computer einen katastrophalen Crash (oder Stromausfall) erleidet, bevor die Daten auf die Platte geschrieben werden:

/* Open the database and create the schema */
sqlite3_open(DATABASE, &db);
sqlite3_exec(db, TABLE, NULL, NULL, &sErrMsg);
sqlite3_exec(db, "PRAGMA synchronous = OFF", NULL, NULL, &sErrMsg);

Importierte 864913 Datensätze in 12.41 Sekunden

Die Verbesserungen sind jetzt kleiner, aber wir sind bis zu 69.600 Einsätze pro Sekunde.

PRAGMA journal_mode = MEMORY

PRAGMA journal_mode = MEMORY das Rollback-Journal im Speicher zu speichern, indem Sie PRAGMA journal_mode = MEMORY . Ihre Transaktion wird schneller sein, aber wenn Sie Leistung verlieren oder Ihr Programm abstürzt während einer Transaktion, die Datenbank konnte in einem korrupten Zustand mit einer teilweise abgeschlossenen Transaktion verlassen werden:

/* Open the database and create the schema */
sqlite3_open(DATABASE, &db);
sqlite3_exec(db, TABLE, NULL, NULL, &sErrMsg);
sqlite3_exec(db, "PRAGMA journal_mode = MEMORY", NULL, NULL, &sErrMsg);

Importierte 864913 Datensätze in 13.50 Sekunden

Ein wenig langsamer als die vorherige Optimierung bei 64.000 Einsätzen pro Sekunde.

PRAGMA synchronous = OFF und PRAGMA journal_mode = MEMORY

Kombinieren wir die beiden vorherigen Optimierungen. Es ist ein bisschen riskanter (im Falle eines Crashs), aber wir importieren nur Daten (keine Bank):

/* Open the database and create the schema */
sqlite3_open(DATABASE, &db);
sqlite3_exec(db, TABLE, NULL, NULL, &sErrMsg);
sqlite3_exec(db, "PRAGMA synchronous = OFF", NULL, NULL, &sErrMsg);
sqlite3_exec(db, "PRAGMA journal_mode = MEMORY", NULL, NULL, &sErrMsg);

Importierte 864913 Datensätze in 12.00 Sekunden

Fantastisch! Wir können 72.000 Einsätze pro Sekunde machen.

Verwenden einer In-Memory-Datenbank

Nur für Kicks, lasst uns auf alle vorherigen Optimierungen aufbauen und den Datenbank-Dateinamen neu definieren, damit wir ganz im RAM arbeiten:

#define DATABASE ":memory:"

Importierte 864913 Datensätze in 10.94 Sekunden

Es ist nicht super praktisch, unsere Datenbank im RAM zu speichern, aber es ist beeindruckend, dass wir 79.000 Einsätze pro Sekunde ausführen können .

Refactoring C-Code

Obwohl nicht speziell eine SQLite Verbesserung, ich mag nicht die zusätzlichen char* Zuweisung Operationen in der while Schleife. Lassen Sie uns diesen Code schnell umgestalten, um die Ausgabe von strtok() direkt in sqlite3_bind_text() , und lassen Sie den Compiler versuchen, die Dinge für uns zu beschleunigen:

pFile = fopen (INPUTDATA,"r");
while (!feof(pFile)) {

    fgets (sInputBuf, BUFFER_SIZE, pFile);

    sqlite3_bind_text(stmt, 1, strtok (sInputBuf, "\t"), -1, SQLITE_TRANSIENT); /* Get Route */
    sqlite3_bind_text(stmt, 2, strtok (NULL, "\t"), -1, SQLITE_TRANSIENT);    /* Get Branch */
    sqlite3_bind_text(stmt, 3, strtok (NULL, "\t"), -1, SQLITE_TRANSIENT);    /* Get Version */
    sqlite3_bind_text(stmt, 4, strtok (NULL, "\t"), -1, SQLITE_TRANSIENT);    /* Get Stop Number */
    sqlite3_bind_text(stmt, 5, strtok (NULL, "\t"), -1, SQLITE_TRANSIENT);    /* Get Vehicle */
    sqlite3_bind_text(stmt, 6, strtok (NULL, "\t"), -1, SQLITE_TRANSIENT);    /* Get Date */
    sqlite3_bind_text(stmt, 7, strtok (NULL, "\t"), -1, SQLITE_TRANSIENT);    /* Get Time */

    sqlite3_step(stmt);        /* Execute the SQL Statement */
    sqlite3_clear_bindings(stmt);    /* Clear bindings */
    sqlite3_reset(stmt);        /* Reset VDBE */

    n++;
}
fclose (pFile);

Hinweis: Wir sind wieder dabei, eine echte Datenbankdatei zu verwenden. In-Memory-Datenbanken sind schnell, aber nicht unbedingt praktisch

Importierte 864913 Datensätze in 8,94 Sekunden

Eine leichte Umgestaltung des in unserer Parameterbindung verwendeten String-Bearbeitungscodes hat uns erlaubt, 96.700 Einsätze pro Sekunde durchzuführen . Ich denke, es ist sicher zu sagen, dass dies viel schnell ist . Wenn wir anfangen, andere Variablen zu zwicken (dh Seitengröße, Indexerstellung usw.), wird dies unser Maßstab sein.

Zusammenfassung (so weit)

Ich hoffe du bist immer noch bei mir! Der Grund, warum wir diese Straße begonnen haben, ist, dass die Bulk-Insert-Performance so wild mit SQLite variiert, und es ist nicht immer klar, welche Änderungen gemacht werden müssen, um unsere Operation zu beschleunigen. Mit dem gleichen Compiler (und Compiler-Optionen), die gleiche Version von SQLite und die gleichen Daten haben wir unseren Code optimiert und unsere Verwendung von SQLite aus einem Worst-Case-Szenario von 85 Einsätzen pro Sekunde auf über 96.000 Einsätze pro Sekunde gehen!

CREATE INDEX dann INSERT vs. INSERT dann CREATE INDEX

Bevor wir mit der Messung der SELECT Leistung beginnen, wissen wir, dass wir Indizes erstellen werden. Es wurde in einer der Antworten unten vorgeschlagen, dass bei der Herstellung von Bulk-Einfügungen, ist es schneller, den Index zu erstellen, nachdem die Daten eingefügt worden sind (im Gegensatz zum Erstellen des Index zuerst dann das Einfügen der Daten). Lass es uns versuchen:

Erstellen Sie Index und fügen Sie Daten ein

sqlite3_exec(db, "CREATE  INDEX 'TTC_Stop_Index' ON 'TTC' ('Stop')", NULL, NULL, &sErrMsg);
sqlite3_exec(db, "BEGIN TRANSACTION", NULL, NULL, &sErrMsg);
...

Importierte 864913 Datensätze in 18.13 Sekunden

Daten einfügen und Index erstellen

...
sqlite3_exec(db, "END TRANSACTION", NULL, NULL, &sErrMsg);
sqlite3_exec(db, "CREATE  INDEX 'TTC_Stop_Index' ON 'TTC' ('Stop')", NULL, NULL, &sErrMsg);

Importierte 864913 Datensätze in 13.66 Sekunden

Wie erwartet, sind Bulk-Einsätze langsamer, wenn eine Spalte indiziert ist, aber es macht einen Unterschied, wenn der Index nach dem Einfügen der Daten erstellt wird. Unsere No-Index-Baseline beträgt 96.000 Einsätze pro Sekunde. Wenn Sie zuerst den Index erstellen, dann fügen Sie uns 47.700 Einsätze pro Sekunde hinzu, während das Einfügen der Daten zuerst dann die Erstellung des Index ergibt uns 63.300 Einsätze pro Sekunde.

Ich würde gerne Anregungen für andere Szenarien machen, um zu versuchen ... und wird dann ähnliche Daten für SELECT-Abfragen kompilieren.




Auf Schüttguteinlagen

Inspiriert von diesem Beitrag und von der Frage, die mich hier geführt hat - Ist es möglich, mehrere Zeilen zu einer Zeit in einer SQLite-Datenbank einzufügen? - Ich habe mein erstes Git- Repository veröffentlicht:

https://github.com/rdpoor/CreateOrUpdate

welche Bulk eine Reihe von ActiveRecords in MySQL- , SQLite- oder PostgreSQL- Datenbanken lädt. Es enthält eine Option, um vorhandene Datensätze zu ignorieren, sie zu überschreiben oder einen Fehler zu erheben. Meine rudimentären Benchmarks zeigen eine 10-fache Geschwindigkeitsverbesserung im Vergleich zu sequentiellen Schriften - YMMV.

Ich benutze es in Produktionscode, wo ich häufig große Datasets importieren muss, und ich bin sehr glücklich damit.




Wenn man sich nur um das Lesen kümmert, etwas schneller (kann aber auch abgestandene Daten lesen), ist die Version von mehreren Verbindungen aus mehreren Threads (Verbindung per-Thread) zu lesen.

Zuerst die Gegenstände finden, in der Tabelle:

 SELECT COUNT(*) FROM table

dann lesen Sie die Seiten (LIMIT / OFFSET)

  SELECT * FROM table ORDER BY _ROWID_ LIMIT <limit> OFFSET <offset>

wo und werden pro Thread berechnet, wie folgt:

int limit = (count + n_threads - 1)/n_threads;

für jeden Thread:

int offset = thread_index * limit

Für unsere kleine (200mb) db dies machte 50-75% beschleunigen (3.8.0.2 64-bit unter Windows 7). Unsere Tische sind schwer nicht normalisiert (1000-1500 Säulen, etwa 100.000 oder mehr Reihen).

Zu viele oder zu wenig Fäden wird es nicht tun, du musst dich benennen und profilieren.

Auch für uns hat die SHAREDCACHE die Performance langsamer gemacht, also habe ich manuell PRIVATECACHE (da es für uns global aktiviert wurde)




Nach dem Lesen dieser Frage habe ich versucht, es zu meinem Programm zu implementieren.

Ich habe 4-5 Dateien, die Adressen enthalten. Jede Datei hat ca. 30 Millionen Datensätze. Ich benutze die gleiche Konfiguration, die Sie vorschlagen, aber meine Anzahl von INSERTs pro Sekunde sind viel niedrig (10.000 Datensätze pro Sekunde).

Also hier ist, wo Ihr Vorschlag scheitert. Sie verwenden eine einzelne Transaktion für alle Datensätze und eine einzelne Einfügung. Aber was ist, wenn ein Rekord gebrochen ist und Sie den Datensatz auf mehrere Einsätze auf verschiedenen Tischen aufteilen?

Der Befehl ON CONFLICT gilt nicht, denn wenn Sie 10 Elemente in einem Datensatz haben und jedes Element in eine andere Tabelle eingefügt werden muss, wenn das Element 5 einen CONSTRAINT-Fehler erhält, müssen alle vorherigen 4 Einsätze auch gehen.

Also hier ist, wo der Rollback kommt. Aber wenn du einen Rollback machst, dann verlierst du alle deine Einsätze und fang von oben an.

Meine Lösung war, mehrere Transaktionen zu verwenden. Ich beginne und beende eine Transaktion alle 10.000 Datensätze (frage nicht, warum diese Nummer, es war die schnellste, die ich getestet habe). Ich schaffe ein Array von 10.000 und füge die erfolgreichen Datensätze dort ein. Wenn der Fehler auftritt, mache ich einen Rollback, beginne eine Transaktion, füge die Datensätze aus meinem Array ein, beende und beginne dann eine neue Transaktion nach dem defekten Datensatz.

Ich hoffe, dass diese Lösung anderen helfen wird, ihre Probleme zu umgehen, wenn sie sich mit Dateien befassen, die schlechte / doppelte Datensätze enthalten.

Der Algorithmus, den ich erstellt habe, hat mir geholfen, meinen Prozess um 2 Stunden zu reduzieren. Endgültige Ladevorgang der Datei 1hr 30m, die noch langsam ist, aber nicht im Vergleich zu den 4 Stunden, die es anfangs nahm.

Wenn jemand irgendwelche anderen Ideen hat, wie man es beschleunigt, bin ich offen für Vorschläge.

UPDATE :

Zusätzlich zu meiner Antwort oben, sollten Sie im Hinterkopf behalten, dass Einsätze pro Sekunde von der Festplatte abhängen, die Sie auch verwenden. Ich habe es auf 3 verschiedenen PCs mit verschiedenen Festplatten getestet und habe massive Unterschiede in der Zeit. PC1 (1 Std. 30m), PC2 (6 Std.) PC3 (14 Std.), Also habe ich mich gefragt, warum das so wäre.

Nach zwei Wochen Forschung und Überprüfung mehrerer Ressourcen: Hard Drive, Ram, Cache, fand ich heraus, dass einige Einstellungen auf Ihrer Festplatte die I / O-Rate beeinflussen können. Durch Anklicken von Eigenschaften auf dem gewünschten Ausgabe-Laufwerk sehen Sie zwei Optionen auf der Registerkarte Allgemein. Opt1: Komprimieren Sie dieses Laufwerk, Opt2: Erlauben Sie Dateien von diesem Laufwerk, um Inhalte indexiert zu haben.

Durch die Deaktivierung dieser beiden Optionen nehmen alle 3 PCs nun etwa die gleiche Zeit ein (1 Std. Und 20 bis 40 Min.). Wenn Sie auf langsame Einsätze stoßen, überprüfen Sie, ob Ihre Festplatte mit diesen Optionen konfiguriert ist. Es wird Ihnen viel Zeit und Kopfschmerzen, die versuchen, die Lösung zu finden