Der widerspenstigen sqlite-Zähmung: Ein Wochenend-Refactoring
share forum

Der widerspenstigen sqlite-Zähmung: Ein Wochenend-Refactoring


Software • von Sven Reifschneider • 24. September 2025 • 0 Kommentare

Warum ich das überhaupt angefangen habe

Ich halte meine Workstations gern schlank. Firefox ist mein tägliches Cockpit, aber er soll nicht ein Jahrzehnt Historie im aktiven Gedächtnis mit sich herumtragen. Trotzdem sind diese Daten – wo ich war, wann, und wie oft – Gold wert. Nicht bloß Nostalgie, sondern Signal:

  • Welche Themen ich wieder und wieder aufgreife.
  • Welche Domains meine Aufmerksamkeit halten.
  • Wie sich Recherche über Tage oder Monate entfaltet.
  • Wie „Deep Work“ im Browser eigentlich aussieht.

Also baute ich ein kleines Tool in meinem eigenen PHP-Framework Charm, das unter der Haube Laravel’s Eloquent nutzt. Auftrag: Firefox’ places.sqlite (Bookmarks, Places, Visits, Origins) in meine Datenbank laden, damit ich später archivieren, analysieren und durchsuchen kann – und Firefox danach aufräumen kann, damit dieser schnell bleibt.

Als ich begonnen habe, war meine places.sqlite auf dem frischen Windows-10-Rechner klein. Der simpelste Importer reichte vollkommen: SQLite öffnen, Zeilen iterieren, in Eloquent firstOrNew(), ein paar Properties setzen, save(). Ihr kennt das Muster. Der pragmatische „Wochenend-Bonus“-Code, den man vergisst, weil er eben läuft.

Die Zeit vergeht. Die Datenbank wächst. Mein Workflow konzentriert sich auf eine Maschine. Plötzlich enthält places.sqlite praktisch alles – und der einst harmlose Importer wird zum Anker.

Eines Tages startete ich ihn. Vier Stunden später brach ich ab. Nicht weil er nicht irgendwann fertig geworden wäre – sondern weil … das inzwischen einfach inakzeptabel ist.

Der Moment der Entscheidung

Ich reservierte mir an einem ruhigen Wochenende 1–2 entspannte Stunden. Ich kenne SQL. Ich kenne Transaktionen. Ich weiß, warum row-by-row-ORMs ab einer gewissen Größenordnung die Klippe hinunterstürzen. Das Problem war nicht Wissen – es waren Prioritäten. Früher war das Spielzeug. Jetzt ist es ein interessanter Datensatz.

Also tat ich, was ich bei Mikro-Refactors immer tue: einen Schritt zurück – und das mentale Modell neu verdrahtet.

Worum geht’s wirklich?

  • Zwei Tabellen ändern sich in-place (moz_bookmarks, moz_origins).
  • Zwei Tabellen sind im Kern append-only (moz_places, moz_historyvisits).
  • Firefox-Zeitstempel sind Mikrosekunden seit Epoch; meine DB erwartet timestamp/datetime.
  • Mein Ziel-Schema hat einen Prefix; die App nutzt Eloquent in Charm; der Code soll sauber bleiben.
  • Und vor allem: Die Datenbank will set-basierte Arbeit – keinen Hagel aus Zeilen-Geflüster.

Ich musste nichts „Neues entdecken“. Ich musste nur die richtigen Wahrheiten anwenden – jetzt, wo die Daten es verlangen.

Den Import neu denken

Die Zeilenschleife flog raus, dafür kam ein Bulk-Loader: Chunks aus der Quell-SQLite lesen, Batches nach MariaDB schreiben, eine Transaktion pro Tabelle. Für append-only-Tabellen hole ich nur die Deltas (WHERE id > max(source_id)). Für veränderliche Tabellen gibt’s einen Upsert über alles. Und weil Firefox-Zeit in Mikrosekunden tickt, konvertiere ich beim Laden sauber auf UTC-Timestamps.

Das war es schon. Nicht glamourös. Einfach korrekt.

Ein paar kompakte Snippets erzählen die Geschichte.

Datenbank-Setup

Ich ersetzte PHPs pures SQLite-Handling und nutze Eloquent für beide Verbindungen – inklusive Upsert-Komfort:

use Illuminate\Database\Capsule\Manager as Capsule;
use Illuminate\Database\Connection;

$capsule = new Capsule();

// Target: MariaDB/MySQL (your app DB)
$capsule->addConnection([
    'driver'    => 'mysql',
    'host'      => '127.0.0.1',
    'database'  => 'your_db',
    'username'  => 'user',
    'password'  => 'pass',
    'charset'   => 'utf8mb4',
    'collation' => 'utf8mb4_unicode_ci',
    'prefix'    => 'ab_', // if you use a table prefix
], 'target');

// Source: Firefox places.sqlite
$capsule->addConnection([
    'driver'   => 'sqlite',
    'database' => __DIR__.'/places.sqlite',
    'prefix'   => '',
], 'places');

$capsule->setAsGlobal();
$capsule->bootEloquent();

$dst = $capsule->getConnection('target');
$src = $capsule->getConnection('places');

$dst->disableQueryLog();
$src->disableQueryLog();

Der kleine Zeit-Konverter

function mozUsToTimestamp(?int $us): ?string
{
    if (!$us || $us setTimezone(new DateTimeZone('UTC'));
    return $dt->format('Y-m-d H:i:s'); // switch to datetime(6) and add .%06d if you need microseconds
}

Upsert für eine veränderliche Tabelle (z. B. moz_origins)

In Charm nutze ich direkt den Query Builder von Eloquent (keine per-Row-Model-Events):

$src->table('moz_origins')
    ->select(['id','prefix','host','frecency'])
    ->orderBy('id')
    ->chunk(5000, function ($chunk) use ($dst) {
        $now = date('Y-m-d H:i:s');
        $payload = [];
        foreach ($chunk as $r) {
            $payload[] = [
                'source_id'  => $r->id,
                'prefix'     => $r->prefix,
                'host'       => $r->host,
                'frecency'   => $r->frecency,
                'created_at' => $now,
                'updated_at' => $now,
            ];
        }

        if ($payload) {
            // UNIQUE(source_id) on target
            $dst->table('browser_origins')->upsert(
                $payload,
                ['source_id'],
                ['prefix','host','frecency','updated_at']
            );
        }
    });

Eine Anweisung pro Chunk. MariaDB tut, wofür sie gebaut ist.

Delta-Insert für eine Append-Only-Tabelle (z. B. moz_places)

$maxId = (int) ($dst->table('browser_places')->max('source_id') ?? 0);

$src->table('moz_places')
    ->select(['id','url','title','rev_host','visit_count','hidden','typed','frecency','last_visit_date','guid'])
    ->where('id', '>', $maxId)
    ->orderBy('id')
    ->chunk(5000, function ($chunk) use ($dst) {
        $now = date('Y-m-d H:i:s');
        $rows = [];
        foreach ($chunk as $r) {
            $rows[] = [
                'source_id'       => $r->id,
                'url'             => $r->url,
                'title'           => $r->title,
                'rev_host'        => $r->rev_host,
                'visit_count'     => $r->visit_count,
                'hidden'          => $r->hidden,
                'typed'           => $r->typed,
                'frecency'        => $r->frecency,
                'last_visit_date' => mozUsToTimestamp((int)$r->last_visit_date),
                'guid'            => $r->guid,
                'created_at'      => $now,
                'updated_at'      => $now,
            ];
        }

        if ($rows) {
            // UNIQUE(source_id) on target; duplicates are ignored by the engine
            $dst->table('browser_places')->insertOrIgnore($rows);
        }
    });

Und ja, eine Transaktion pro Tabelle um diese Blöcke herum. Allein das schrumpft Stunden voller fsyncs zu einem kurzen, robusten Schreibvorgang.

Das Ergebnis (und warum es kein „Zauber“ ist)

Ich habe den Importer auf derselben Maschine erneut laufen lassen:

  • Altes Skript: nach vier Stunden abgebrochen.
  • Neuer Loader: **

Warum die places.sqlite so interessant ist

Sobald die Daten in meinem Schema liegen, wird der Browser von einer flüchtigen UI zu einem messbaren Wissensfluss.

Ich kann einiges sehen:

  • Temporale Aufmerksamkeit: nicht nur was ich lese, sondern wann – und wie es strukturiert ist. Morgens Geopolitik; nächtliche Deep-Dives in einen Recherchefaden; ein Schub Doku-Tabs während eines Refactors.
  • Domain-Gravitäten: welche Sites mein Denken verankern, welche Brücken schlagen, welche bloß Rauschen sind.
  • Projekt-Signaturen: wie eine Produktuntersuchung im Browser aussieht, lange bevor eine Zeile Code entsteht.
  • Search-to-Absorption-Zyklen: Ein Query, ein Klick, drei Tabs später – wurde daraus eine Lern-„Schleife“ oder endete es als Sackgasse?

Es gibt hier ethische Nuancen: Das ist mein eigenes Browsing, privat archiviert auf eigener Hardware, mit dem expliziten Ziel, meine Tools besser und meinen Kopf leichter zu machen. Firefox bleibt schlank, weil ich Historie aggressiv purgen kann; mein Loader bewahrt die Spur für zukünftige Analysen.

Und auch wenn ich die Daten noch nicht tiefgreifend auswerte, erfasse ich sie strukturiert. Wenn ich Zeit für ein solches Projekt habe, arbeite ich mit ordentlich strukturierten Daten und kann mich auf das Analysesystem über einem großen Datensatz konzentrieren. Es besteht zudem kein Risiko, dass alte Historie verschwindet oder mühsam aus mehreren places.sqlite rekonstruiert werden muss.

Zur Rolle von KI (und was sie nicht war)

Ich habe nicht „die KI gebeten, es schnell zu machen“. Das ist nicht mein Stil. Ich nutzte ChatGPT als Partner, um die Artikulation zu beschleunigen:

  • Die Architektur stand – Transaktionen, set-basierte Writes, Upsert vs. Append-Only.
  • Ich wollte Entscheidungen gegentesten, mir Eloquent-Details ins Gedächtnis rufen und eine saubere Oberfläche in Charm bauen, ohne in Laravel-App-Bootstrapping abzubiegen.
  • Die Zusammenarbeit nahm Reibung raus: im Flow bleiben, ein paar taktische Kanten validieren (z. B. wie ich die temporäre SQLite-Connection bevorzuge) und den Fokus scharf halten.

Ein paar praktische Dinge, die mir wichtig sind

Ich erspare euch eine Checkliste, aber drei Details zählen:

  • Schema-Ehrlichkeit. Liefert die Quelle Mikrosekunden und mir ist feine Kausalität wichtig, wechsle ich auf datetime(6) und speichere sie. Reichen Sekunden, wandle ich bewusst herunter. In jedem Fall ist es eine Entscheidung – sichtbar im Code, nicht Zufall.
  • Vorhersehbare Re-Runs. Ein Loader, der nur einmal funktioniert, ist eine Falle. Idempotenz, Deltas und eindeutige Keys halten das System performant, auch wenn ich zweimal auf „Start“ drücke.
  • Isolation im Stack. Charm liefert Ergonomie; Eloquent die Ausdrücke. Der Importer läuft elegant. Wenn ich fertig bin, kann Firefox aufräumen – und meine Statistiken bleiben.

Wie es weitergeht

Jetzt, wo die Pipeline in wenigen Sekunden läuft, beginnt der spannende Teil:

  • Explorative Statistiken, die die Realität treffen: Visit-Sequenzen, Domain-Gravitäten, Tageszeit-Muster für Deep Work, „Themen-Migration“ über Wochen.
  • Eine Suche, die wirklich mir gehört: Places, Visits und Bookmarks mit meinen Notizen, Commits und Texten kreuzverbinden.
  • Signale für Fokus-Management: Wenn eine Woche zu reaktivem Browsen kippt, will ich das so deutlich sehen wie fehlgeschlagene Tests.

Weil die Daten unter meiner Kontrolle sind – und der Loader schnell genug ist, um ihn gedankenlos erneut zu starten – kann ich an Fragen iterieren, ohne die Pipeline zu babysitten.

Schluss: Die leise Zufriedenheit, es richtig zu machen

Das war kein heroischer Rewrite. Es war die sanfte, unvermeidliche Korrektur eines Musters, das seinen Zweck erfüllt hatte – und dann zu lange blieb. Die erste Version war okay, als die Welt klein war. Die neue Version respektiert die Form der Daten und die Stärken der Datenbank.

Mir gefällt, dass es einen Nachmittag brauchte. Mir gefällt, dass es in Sekunden läuft. Mir gefällt, dass Firefox flink bleibt, während mein Archiv klüger wird. Und mir gefällt, dass die Lösung im besten Sinne langweilig ist: set-basiertes SQL, klare Grenzen, Werkzeuge, die nicht stören.

Wenn deine Side-Projects heimlich zu echten Daten geworden sind, lohnt die Frage: Erzählt der Code noch die Wahrheit über den Maßstab? Meiner tat es nicht. Jetzt schon.

Dieser Beitrag wurde von mir selbst verfasst – mit Unterstützung durch KI (GPT-5). Die Illustrationen stammen von mir (erstellt mit Sora). Entdecke, wie KI deine Inhalte inspirieren kann – Neoground GmbH.


Teile diesen Beitrag

Wenn dir dieser Artikel gefallen hat, teile ihn doch mit deinen Freunden und Bekannten! Das hilft mir dabei, noch mehr Leute zu erreichen und motiviert mich, weiterhin großartige Inhalte für euch zu erstellen. Nutze einfach die Sharing-Buttons hier unten, um den Beitrag auf deinen bevorzugten sozialen Medien zu teilen. Danke dir!

Please consider sharing this post
Please consider donating

Unterstütze den Blog

Falls du meine Arbeit und diesen Blog besonders schätzen solltest, würde ich mich riesig freuen, wenn du mich unterstützen möchtest! Du kannst mir zum Beispiel einen Kaffee spendieren, um mich bei der Arbeit an neuen Artikeln zu erfrischen, oder einfach so, um den Fortbestand des Blogs zu fördern. Jede noch so kleine Spende ist herzlich willkommen und wird sehr geschätzt!

currency_bitcoin Spende via Kryptowährungen
Bitcoin (BTC):1JZ4inmKVbM2aP5ujyvmYpzmJRCC6xS6Fu
Ethereum (ETH):0xC66B1D5ff486E7EbeEB698397F2a7b120e17A6bE
Litecoin (LTC):Laj2CkWBD1jt4ZP6g9ZQJu1GSnwEtsSGLf
Über den Autor

Sven Reifschneider

Herzliche Grüße! Ich bin Sven, ein technischer Innovator und begeisterter Fotograf aus der malerischen Wetterau, in der Nähe des lebendigen Frankfurt/Rhein-Main-Gebiets. In diesem Blog verbinde ich mein umfangreiches technisches Wissen mit meiner künstlerischen Leidenschaft, um Geschichten zu erschaffen, die fesseln und erleuchten. Als Leiter von Neoground spreng ich die Grenzen der KI-Beratung und digitalen Innovation und setze mich für Veränderungen ein, die durch Open Source Technologie Widerhall finden.

Die Fotografie ist mein Portal, um die flüchtige Schönheit des Lebens auszudrücken, die ich nahtlos mit technologischen Einsichten verbinde. Hier trifft Kunst auf Innovation, jeder Beitrag strebt nach Exzellenz und entfacht Gespräche, die inspirieren.

Neugierig, mehr zu erfahren? Folge mir in den sozialen Medien oder klicke auf "Mehr erfahren", um das Wesen meiner Vision zu erkunden.


Noch keine Kommentare

Kommentar hinzufügen

In deinem Kommentar kannst du **Markdown** nutzen. Deine E-Mail-Adresse wird nicht veröffentlicht. Mehr zum Datenschutz findest du in der Datenschutzerklärung.