WordPress: DB Housekeeping

Als sogenannter WordPress »end user« kommt man mit der Datenbank hinter WordPress nicht zwangsläufig in Berührung. Der Anwender braucht sich also keine Gedanken zu machen, wie, wo und in welchen Strukturen WordPress Artikel, Kommentare, Kontroll- und Stammdaten abspeichert. Genau das macht ja auch einen Teil des Komforts aus, den WordPress seinen Anwendern bietet. Einzig bei der Datensicherung, in Form eines Dumps der MySQL Datenbank, könnte man Einsicht in die oberflächlich nicht sehr erhellende Tabellenstruktur nehmen. Auf Basis einer WordPress 3.0 Standardinstallation gibt es hierzu eine sehr detaillierte Information in der WordPress Database Description. Dort heißt es sinngemäß, daß vor Manipulationen an den Datenbeständen eine Datensicherung durchgeführt werden sollte (und man sollte auch wissen, wie eine korrekte Rücksicherung durchzuführen ist; Anm. d. Verf.). Weiter heißt es auch, daß Entwickler von Plugins, die die WordPress Datenbank manipulieren, ausschließlich selbst für die Datenintegrität verantwortlich sind. Dies ist nicht der einzige Grund, warum ich möglichst keine Plugins zum Einsatz bringe.

Vorüberlegungen

Worum geht es hier? Konkret: WordPress legt in der Tabelle wp_posts beim Speichern/Publizieren und durch Autosave, Sicherungskopien von Posts/Artikeln an, die ich nicht benötige, die diese Tabelle unnötig aufblähen, und daher gelöscht werden sollen. Pro veröffentlichtem Artikel kann ich durchschnittlich sechs sogenannter revisions feststellen, und um die geht es hier. Sie tun nichts und sie stellen weder für WordPress noch für MySQL ein Problem dar, aber sie erzeugen zwangsläufig einen sehr großen Dump bei der Datensicherung. Und so, wie ich meinen Papierkorb regelmäßig leere, möchte ich auch überflüssige Datensätze aus dieser Tabelle entfernen.

Beispiel: Ein neuer Artikel landet in der zentralen MySQL-Tabelle wp_posts und erhält dort die ID 4711. Dieser Post behält diese ID 4711, bis er selbst einmal gelöscht werden sollte. Alle Sicherungskopien, die durch Speichern oder mittels Autosave erzeugt werden, erhalten neue IDs (4712, 4713, …). Dadurch, daß der publizierte, also sichtbare Artikel trotz aller Revisionen, die über ihn kommen, seine ursprüngliche ID 4711 behält, müßten auch alle Fremdschlüssel immer auf das richtige Ziel weisen. Beispielsweise die Kommentare: In der Tabelle wp_comments weisen alle Kommentare zu diesem Artikel über das Feld wp_comments.comment_post_ID auf die wp_posts.ID 4711, gleichgültig welcher Revisionsstand dem Artikel zum Kommentarzeitpunkt gerade zugrunde lag. Wenn also nicht sonst jemand unsauber eingegriffen hat, kann es in diesem Zusammenhang also keine verwaisten Datensätze geben. Das gilt übrigens in gleichem Maße für die Tabellen wp_postmeta und wp_term_relationships.

Query Select

Bis hierhin ist also klar, daß die gesuchten Revisionen in der Tabelle wp_posts, sofern sie eindeutig identifiziert werden können, gefahrlos gelöscht werden dürfen. Dies unterstellt, daß sich nicht irgendein Plugin auf sie stützt. Allen Datensätzen, die Revisionen repräsentieren, ist folgendes in der engsten Form gemein:

wp_posts.post_type = 'revision' und
wp_posts.post_status = 'inherit' und
(wp_posts.post_name LIKE '%revision%' oder
wp_posts.post_name LIKE '%autosave%')

Wahrscheinlich würde eine Prüfung auf revision ausreichen, aber das ist nicht gesichert. Um ein Fahrgefühl zu bekommen, kann man zunächst einmal eine Select-Abfrage starten, zum Beispiel mittels phpMyAdmin:

SELECT * FROM wp_posts
WHERE (post_type = 'revision') AND
(post_status = 'inherit') AND
((post_name LIKE '%revision%') OR (post_name LIKE '%autosave%'));

View Select Count

Nun ist diese Liste nicht wirklich hilfreich, zeigt aber, daß die richtige Selektion für das anschließende Löschen durchgeführt wurde. Sinnvoll für zukünftige Zwecke könnte es aber sein die Abfrage so zu modifizieren, daß sie nur die Anzahl der revisions ausgibt und als View gespeichert wird. MySQL kann vorformulierte SQL-Queries als sogenannte Views speichern (MySQL 5.1, Kapitel 21, Views). Das nachstehende SQL-Statement erzeugt einen solchen View mit dem Namen view_posts_revisions_count der im Ergebnis die Anzahl der gefundenen Artikelrevisionen angibt. Der Name des Views kann natürlich im Rahmen der MySQL-Vorgaben beliebig gewählt werden.

CREATE OR REPLACE
ALGORITHM = UNDEFINED
VIEW view_posts_revisions_count AS
SELECT count(*) AS 'Anzahl' FROM wp_posts
WHERE (post_type = 'revision') AND
(post_status = 'inherit') AND
((post_name LIKE '%revision%') OR (post_name LIKE '%autosave%'));

Views werden in der MySQL Tabelle information_schema.VIEWS gespeichert. In phpMyAdmin findet man nach Verlassen und Neuaufruf der WordPress Datenbank die verfügbaren Views in der Liste der Tabellen auf der linken Seite. Views werden übrigens mit diesem SQL-Befehl ausgeführt:

SELECT * FROM view_posts_revisions_count;

Delete Revisions

Nachdem geklärt ist, wie Post Revisionen identifiziert und selektiert werden, und man sicher sein kann, daß, zumindest bei einer WordPress 3.0 Standardinstallation, keine Fremdschlüssel auf sie zeigen sollten, können diese Sicherungskopien nun gelöscht werden.

DELETE FROM wp_posts
WHERE (post_type = 'revision') AND
(post_status = 'inherit') AND
((post_name LIKE '%revision%') OR (post_name LIKE '%autosave%'));

Ergänzend kann noch angemerkt werden, daß die Sicherungskopien natürlich nicht bezugslos in der Tabelle wp_posts abgelegt waren. Zum einen existiert das Datenfeld post_parent, das bei allen Revisionen grundsätzlich die ID des ursprünglichen Posts (4711) enthält (nicht die ID seines jeweiligen Sicherungsvorgängers). Zum anderen gibt der Inhalt des Felds post_name Auskunft über die Sicherungsversion. Damit ließen sich veröffentlichter Post und seine Kopien wieder in eine zeitliche Reihenfolge bringen. Eine Prüfung, ob es Sicherungskopien gibt, die keinen Vater mehr haben, was aber nach dem vorstehenden DELETE zu spät wäre, könnte so erfolgen:

SELECT p1.* FROM wp_posts AS p1
WHERE p1.post_type = 'revision' and NOT EXISTS (
SELECT p2.* FROM wp_posts AS p2
where p1.post_parent = p2.ID );

oder als Variante mittels eines Joins

SELECT p1.ID, p1.post_parent, p1.post_date,
p1.post_title, p1.post_name FROM wp_posts AS p1
LEFT JOIN wp_posts AS p2
ON p1.post_parent = p2.ID
WHERE p1.post_type = 'revision' and p2.ID IS NULL;

Diese Abfragen sollten selbstverständlich keine Ergebnisse liefern.

Stored Procedures

Die vorstehende Löschroutine könnte man in einer Textdatei speichern und per copy and paste bei Bedarf in einen geeigneten SQL-Klienten (z. B. phpMyAdmin) kopieren und ausführen lassen. Bequemer ist es aber eine Prozedur anzulegen, die in der Datenbank gespeichert wird und jederzeit aufgerufen werden kann (MySQL 5.1, Kapitel 19, Gespeicherte Prozeduren und Funktionen):

DELIMITER $$
DROP PROCEDURE IF EXISTS proc_posts_revisions_delete$$
CREATE PROCEDURE proc_posts_revisions_delete()
BEGIN
DELETE FROM wp_posts
WHERE (post_type = 'revision') AND
(post_status = 'inherit') AND
((post_name LIKE '%revision%') OR (post_name LIKE '%autosave%'));
END$$
DELIMITER ;

Diese Prozedur wird in der MySQL Tabelle information_schema.ROUTINES gespeichert. Das MySQL Standardbegrenzungszeichen (DELIMITER) Semikolon, muß kurzfristig umgebogen werden, damit der Kern der Prozedur (hier DELETE) interpretationsfrei an den MySQL-Server weitergereicht werden kann. Damit ist zunächst nur die Prozedur dauerhaft eingerichtet worden. Der eigentliche Aufruf geschieht mittels eines Calls:

CALL proc_posts_revisions_delete();

Gespeicherte Prozeduren findet man in phpMyAdmin etwas versteckt unterhalb der Strukturdarstellung der WordPress Tabellen in dem Link Routinen.

Verwaiste Schlüssel

Wie eingangs festgestellt, beziehen sich laut der WordPress Beschreibung die drei Tabellen wp_comments, wp_postmeta und wp_term_relationships über ihre Fremdschlüssel auf die Tabelle wp_posts. Zur Feststellung, ob es hier Waisen gibt, muß die Integrität dieser Tabellen geprüft werden:

CREATE OR REPLACE
ALGORITHM = UNDEFINED
VIEW view_comments_orphan_count AS
SELECT count(*) AS 'Anzahl' FROM wp_comments
LEFT JOIN wp_posts
ON wp_comments.comment_post_ID = wp_posts.ID
WHERE wp_posts.ID IS NULL;

Wenn in der Tabelle wp_posts im Feld ID kein Gegenstück zu wp_comments.comment_post_ID gefunden wurde, wird für die Tabelle wp_posts ein Datensatz erzeugt, dessen Feld mit NULL gefüllt ist. Dieser Umstand wird abgeprüft und gezählt. Ein zweiter View für die Tabelle wp_postmeta würde so angelegt werden:

CREATE OR REPLACE
ALGORITHM = UNDEFINED
VIEW view_postmeta_orphan_count AS
SELECT count(*) AS 'Anzahl' FROM wp_postmeta
LEFT JOIN wp_posts
ON wp_postmeta.post_id = wp_posts.ID
WHERE wp_posts.ID IS NULL;

Fazit

Views sollten von einer Datensicherung ausgenommen werden, da die Rücksicherung Probleme bereitet. Zudem können sie im Schadensfall jederzeit wieder nachkonstruiert werden. Wurden verwaiste Schlüssel oder Sicherungskopien ohne Väter festgestellt, sollte man vor weiteren Manipulationen an den Datenbeständen ohnehin absehen und zunächst die Ursachen hierfür herausfinden. Durch das »in den Papierkorb legen« eines Artikels am Redaktionsarbeitsplatz wird in der Tabelle wp_posts das Feld post_status auf »trash« gesetzt. Die Revisionen des Artikels bleiben von dieser Aktion unberührt. In der Tabelle wp_comments wird das Feld comment_approved aller Kommentare, die sich auf den Artikel beziehen, auf »post-trashed« gesetzt. Erst durch das Leeren des Papierkorbs werden sämtliche Datensätze, die sich auf den gelöschten Artikel beziehen, aus allen drei Tabellen rückstandslos entfernt.
rh2011-07-004