Zum Inhalt springen
Konfiguration

WordPress: URL in der Datenbank suchen und ersetzen, ohne serialisierte Daten zu zerstören

Die Website-URL in der WordPress-Datenbank ändern, ohne Widgets, Menüs oder Theme-Einstellungen zu verlieren. Der WP-CLI-Befehl, der serialisierte Daten korrekt behandelt.

Veröffentlicht

Führe niemals ein simples UPDATE ... SET column = REPLACE(column, 'oldurl', 'newurl') über eine WordPress-Datenbank aus. Es ändert den Text zwar korrekt und zerlegt gleichzeitig deine Einstellungen, weil WordPress Arrays und Objekte als PHP-serialisierte Zeichenketten speichert und dieses Format die Byte-Länge jeder enthaltenen Zeichenkette festhält. Ein rohes SQL-Replace schreibt den Text um, rührt die gespeicherte Länge aber nie an — und der Wert ist nicht mehr lesbar.

Nimm stattdessen ein Werkzeug, das Serialisierung versteht. Auf der Kommandozeile ist das wp search-replace. Zuerst die Datenbank sichern, dann einen Testlauf, dann der echte Durchlauf.

Was dabei tatsächlich kaputtgeht

Eine PHP-serialisierte Zeichenkette sieht so aus:

s:18:"http://example.com"

Die 18 ist die Byte-Länge des folgenden Textes. Zähl nach — http://example.com hat exakt 18 Zeichen.

Stell dir nun eine gespeicherte Theme-Einstellung vor:

a:1:{s:4:"logo";s:18:"http://example.com";}

Ein einfaches SQL-Replace von http:// zu https:// müsste Folgendes ergeben:

a:1:{s:4:"logo";s:19:"https://example.com";}

Nur ergibt es das nicht. Du bekommst das hier:

a:1:{s:4:"logo";s:18:"https://example.com";}

Die Zeichenkette ist jetzt 19 Bytes lang, behauptet aber weiterhin 18. PHP liest die angegebene Länge, springt 18 Bytes vorwärts, findet dort weder das schließende Anführungszeichen noch das Semikolon und gibt auf. Das gesamte Array lässt sich nicht mehr deserialisieren.

Warum der Fehler lautlos passiert

Genau das macht diesen Fehler so teuer. WordPress wirft keine Fehlermeldung und stoppt auch nicht. Beim Lesen einer Option läuft der Wert durch den Deserialisierungs-Helfer, PHP liefert für die defekten Daten false zurück, und WordPress reicht deinem Code einen Wert weiter, der sich verhält, als wäre die Option nie gesetzt worden. Plugin oder Theme greifen daraufhin auf ihren Standardwert zurück.

Das Symptom ist also keine Fehlerseite. Das Symptom ist:

  • Widgets verschwinden aus den Sidebars
  • Customizer-Einstellungen springen auf die Theme-Standardwerte zurück
  • Die Layout-Daten eines Page Builders kommen leer zurück, die Seite rendert nichts
  • Plugin-Einstellungsseiten sehen frisch installiert aus
  • Menüs verlieren ihre zugewiesenen Positionen

Nichts im Backend deutet darauf hin, dass etwas nicht stimmt. Die Zeile steht weiterhin in der Datenbank, weiterhin voll mit scheinbar korrektem Text. Nur das Längen-Präfix stimmt um ein oder zwei Bytes nicht. PHP gibt beim Scheitern der Deserialisierung durchaus einen Hinweis aus, aber auf einer Produktivseite mit abgeschalteter Fehleranzeige sieht den niemand. Mit WP_DEBUG samt aktiviertem WP_DEBUG_LOG und WP_DEBUG_DISPLAY auf false landen diese Hinweise in einer Logdatei, in der du sie nachlesen kannst.

Der Schaden verteilt sich außerdem ungleichmäßig. Einfache skalare Werte — siteurl, home, gewöhnlicher Beitragsinhalt, eine URL in einer eigenen Spalte — überstehen ein naives Ersetzen problemlos. Nur serialisierte Werte gehen kaputt. Deshalb sieht ein fehlerhaftes Ersetzen auf den ersten Blick oft nach Erfolg aus.

Prüfe wp-config.php, bevor du irgendetwas anfasst

Wenn wp-config.php die Konstanten WP_HOME oder WP_SITEURL definiert, gewinnen diese gegen alles, was in der Datenbank steht. Eine Änderung an wp_options scheint dann völlig wirkungslos zu sein. Suche nach Zeilen wie diesen, bevor du die Datenbank für das Problem hältst:

define( 'WP_HOME', 'http://example.com' );
define( 'WP_SITEURL', 'http://example.com' );

Entweder du trägst dort die neue URL ein oder du entfernst die Zeilen und überlässt der Datenbank das Feld.

Erst sichern — und zwar ernsthaft

Ein Serialisierungsschaden lässt sich nicht immer leicht rückgängig machen. Sobald Längen-Präfix und Inhalt auseinanderlaufen, ist der ursprüngliche Wert aus dem defekten Wert nicht mehr rekonstruierbar — ohne die Originalzeichenkette kannst du die korrekte Länge nicht kennen. Ein Backup ist die einzige echte Rückgängig-Funktion.

wp db export backup-before-replace.sql

Oder mit mysqldump, falls WP-CLI nicht verfügbar ist:

mysqldump -u USER -p DBNAME > backup-before-replace.sql

Bewahre es möglichst außerhalb des Servers auf. Ein Backup im Web-Root ist gleichzeitig Wiederherstellungspunkt und Sicherheitsproblem.

Der sichere Befehl

Das search-replace von WP-CLI deserialisiert jeden Wert, geht die dekodierte Struktur durch, ersetzt innerhalb davon und serialisiert anschließend mit korrekten Längen neu. Erst der Testlauf:

wp search-replace 'http://example.com' 'https://example.com' --dry-run --all-tables --skip-columns=guid

Lies den Bericht. Er zeigt dir, wie viele Ersetzungen pro Tabelle und Spalte anfallen würden. Wirkt die Tabellenliste falsch oder hat eine Tabelle, die du nicht kennst, Tausende Treffer, halte an und schau nach, bevor du es ernsthaft ausführst.

Dann der echte Durchlauf:

wp search-replace 'http://example.com' 'https://example.com' --all-tables --skip-columns=guid --report-changed-only

Ehrliche Anmerkungen zu den Optionen:

  • --all-tables erfasst auch Tabellen, die WordPress selbst nicht registriert — und genau dort liegen oft die Plugin-Daten. Ohne diese Option werden manche Plugin-Tabellen komplett übersprungen.
  • --skip-columns=guid ist wichtig. Die guid ist eine dauerhafte Kennung für Feedreader, keine aktive URL. Schreibt man sie um, sehen Feed-Abonnenten unter Umständen dein gesamtes Archiv als neue Beiträge.
  • Suche ohne abschließenden Schrägstrich und ohne die Protokoll-Varianten, die du nicht anfassen willst. Ein Ersetzen des blanken example.com trifft auch E-Mail-Adressen und jede Erwähnung im Beitragstext.

Wenn dir die Kommandozeile nicht zur Verfügung steht: Migrations-Plugins, die serialisierte Daten verstehen, erledigen dieselbe Dekodierarbeit über das Backend. Das gesuchte Verhalten wird ausdrücklich als Umgang mit serialisierten Daten beschrieben; steht das bei einem Werkzeug nicht dabei, geh davon aus, dass es das nicht kann.

Welche Tabellen und Spalten betroffen sind

TabelleSpalteRisiko durch Serialisierung
wp_optionsoption_valueHoch — Widgets, Theme-Mods, Transients, die meisten Plugin-Einstellungen
wp_postmetameta_valueHoch — Page-Builder-Layouts, Arrays aus benutzerdefinierten Feldern
wp_usermetameta_valueHoch — Berechtigungen, Bildschirm- und Dashboard-Einstellungen
wp_termmetameta_valueMittel — hängt von den Plugins ab
wp_postspost_content, guidNiedrig beim Inhalt, aber Shortcode-Attribute und Block-Markup enthalten URLs
wp_commentscomment_content, comment_author_urlNiedrig
wp_linkslink_url, link_imageNiedrig und meist ungenutzt

Im Multisite-Betrieb kommen wp_blogs, wp_site, wp_sitemeta sowie die seitenspezifischen wp_2_options, wp_3_options und so weiter dazu. URL-Wechsel im Multisite folgen zusätzlichen Regeln jenseits von Suchen und Ersetzen — behandle das als eigene Aufgabe.

Die Fälle, die search-replace nicht vollständig löst

Ehrlich zu den Grenzen:

JSON in der Datenbank. Manche Page Builder legen Layout-Daten als JSON mit maskierten Schrägstrichen ab, deine URL steht dort also als https:\/\/example.com statt als https://example.com. Eine Suche nach der normalen Schreibweise findet sie nicht. Unter Umständen brauchst du einen zweiten Durchlauf auf die maskierte Zeichenkette. Teste es an einer Kopie.

Base64-kodierte Werte. Kodiert ein Plugin seine Einstellungen vor dem Speichern, kann kein textbasiertes Ersetzen die URL überhaupt sehen. Solche Einstellungen müssen in der Regel über die Oberfläche des Plugins neu eingetragen werden.

Fest im Code hinterlegte URLs. Alles, was in functions.php, in einem Child-Theme-Template oder in einem fest verdrahteten Asset-Pfad steht, liegt nicht in der Datenbank und bleibt unangetastet. Durchsuche das Theme-Verzeichnis separat mit grep.

Zwischengespeicherte und generierte Ausgaben. Object Cache, Page Cache und CDN-Kopien liefern auch nach einem korrekten Ersetzen weiter die alten URLs aus. Leere sie und prüfe erneut, bevor du das Ersetzen für gescheitert hältst.

Wenn du die fehlerhafte Abfrage bereits ausgeführt hast

Spiel das Backup zurück. Das ist die Antwort, und es ist besser, sie klar auszusprechen, als eine Reparaturanleitung anzubieten, die meistens nicht funktioniert.

Gibt es kein Backup, ist der rettbare Weg schmal: Werte, die nie serialisiert waren, sind in Ordnung, und die defekten serialisierten müssen von Hand rekonstruiert oder durch erneutes Speichern der jeweiligen Einstellungsseite zurückgesetzt werden. Wenn du einen Widget-Bereich neu speicherst, eine Menü-Position neu zuweist oder die Einstellungen eines Plugins erneut einträgst, wird ein frischer, korrekt serialisierter Wert über den defekten geschrieben. Mühsam, aber wirksam — und besser, als beschädigte Zeilen liegen zu lassen, wo sie die nächste Person verwirren, die sich die Website ansieht.

Um die passenden Anweisungen vorab zu erzeugen, baue sie mit dem Werkzeug für WordPress-Suchen-und-Ersetzen-SQL — es zeigt, welche Spalten sich gefahrlos mit reinem SQL bearbeiten lassen und welche eine serialisierungsbewusste Behandlung brauchen. So siehst du die Aufteilung, bevor du dich auf einen Befehl festlegst.

FAQ

Fragen

Warum zerstört ein einfaches SQL-REPLACE eine WordPress-Website?

Weil WordPress Arrays und Objekte als PHP-serialisierten Text speichert und dieses Format die Byte-Länge jeder enthaltenen Zeichenkette mitschreibt. Ein rohes REPLACE ändert den Text, lässt die alte Längenangabe aber stehen. PHP weigert sich dann, den Wert zu deserialisieren, WordPress fällt auf einen Standardwert zurück, und die Einstellungen wirken leer statt falsch.

Wie ändere ich die WordPress-Website-URL am sichersten in der Datenbank?

Erst ein vollständiges Datenbank-Backup anlegen, dann den WP-CLI-Befehl wp search-replace zunächst als Testlauf ausführen. WP-CLI deserialisiert jeden Wert, ersetzt innerhalb der dekodierten Struktur und serialisiert anschließend mit korrigierten Längen neu. Migrations-Plugins, die serialisierte Daten verstehen, erledigen dasselbe über das Backend, falls kein Zugriff auf die Kommandozeile besteht.

In welchen Tabellen steckt die alte URL?

Vor allem in wp_options, wp_posts, wp_postmeta, wp_usermeta, wp_termmeta und wp_comments. Die Gefahr durch Serialisierung liegt in wp_options und in jeder meta_value-Spalte, denn Widgets, Theme-Mods, Transients und Page-Builder-Daten werden als Arrays abgelegt. Im Multisite-Betrieb tragen zusätzlich wp_blogs, wp_site und wp_sitemeta die Domain.

Hilft die Änderung in der Datenbank, wenn die URL in wp-config.php definiert ist?

Nein. Wenn wp-config.php WP_HOME oder WP_SITEURL definiert, überschreiben diese Konstanten alles, was siteurl und home in wp_options enthalten. Die Datenbank zu bearbeiten bewirkt dann sichtbar gar nichts, solange die Konstanten nicht angepasst oder entfernt werden. Prüfe wp-config.php zuerst, denn diese eine Zeile erklärt viele gescheiterte Migrationen.

Soll ich die Spalte guid ebenfalls ersetzen?

Nein. Die guid ist eine dauerhafte Kennung für Feedreader, keine funktionierende URL, und ein Umschreiben kann dazu führen, dass Feed-Abonnenten sämtliche alten Beiträge erneut als neu angezeigt bekommen. Übergib bei WP-CLI skip-columns mit guid. Die einzige gängige Ausnahme ist eine Website, die nie öffentliche Feeds oder Abonnenten hatte.

Woran erkenne ich, dass ein Wert nach einem fehlerhaften Ersetzen kaputt ist?

Beschädigte serialisierte Werte sehen in der Datenbank normal aus, kommen in WordPress aber leer zurück. Widgets verschwinden, Customizer-Einstellungen setzen sich zurück, und Plugin-Optionen fallen ohne jede Fehlermeldung auf die Standardwerte zurück. Mit aktiviertem WP_DEBUG und WP_DEBUG_LOG taucht der unserialize-Hinweis auf. Ein Vergleich der angegebenen Zeichenkettenlänge mit der tatsächlichen Byte-Anzahl bestätigt es direkt.