Een klant belt dat de beheeromgeving traag is. De server heeft geheugen zat, de CPU doet niets, en de site zelf laadt prima. Toch duurt het zoeken naar een klant in wp-admin zeven seconden.

In dat geval helpt een zwaardere server meestal niet. Het probleem zit meestal in de database, en specifieker: die moet zoeken zonder register.

Wat een index eigenlijk is

Stel je een boek van duizend pagina's voor zonder inhoudsopgave. Iemand vraagt: op welke pagina staat het woord "kanttekening"? Dan begin je op pagina één, en lees je door tot je het tegenkomt. Bij een dun boekje kost dat niets. Bij duizend pagina's ben je een tijd bezig.

Een database doet precies hetzelfde. Zonder index leest die elke rij van een tabel om te controleren of die aan je vraag voldoet. Dat heet een full table scan. Bij tienduizend rijen merk je er niets van. Bij vijf miljoen rijen wel.

Een index is de inhoudsopgave: een aparte, gesorteerde kopie van één of meer kolommen, met een verwijzing naar de rij waar de rest staat. Zoeken in een gesorteerde lijst gaat via halveren, en niet regel voor regel. Van vijf miljoen rijen naar de juiste rij zijn dan ongeveer drieëntwintig stappen, in plaats van vijf miljoen.

Daar staat iets tegenover. Een index kost schijfruimte, en bij elke wijziging moet de database ook de index bijwerken. Een tabel volplempen met indexes maakt schrijven trager. Het is dus een afweging: welke kolommen worden zo vaak bevraagd dat een register de moeite waard is?

Waarom WordPress er een paar mist

WordPress slaat vrijwel alles op in twee tabellen: wp_posts voor content, en wp_postmeta voor alles wat verder aan die content hangt. Die tweede tabel is een lange lijst met sleutel-waardeparen: post_id, meta_key, meta_value.

Op zich prima. Alleen: wp_postmeta heeft standaard een index op post_id en op meta_key, maar niet op de combinatie van beide, en niet op meta_value. En laat dat nou net de manier zijn waarop veel plug-ins die tabel gebruiken.

Dat is geen slordigheid, maar geschiedenis. De keuze om meta_value als longtext op te slaan is bijna twintig jaar oud, en op een longtext-kolom kun je niet zomaar een index plaatsen: MySQL en MariaDB willen dan weten hoeveel tekens ze moeten indexeren. Het ticket om dit aan te passen staat al jaren open in de WordPress-bugtracker: #45354.

Het gevolg voor jullie is er niet minder om: op een grote site staat er een tabel met miljoenen rijen die bij bepaalde vragen van voren af aan doorgelezen moet worden. Dat merk je aan performance.

Wat je ervan merkt

Het patroon is bijna altijd hetzelfde. De site voor bezoekers is snel, want die draait op caching. De beheeromgeving is traag, want die komt niet uit de cache.

Concreet zien we dit vaak terugkomen bij:

  • Zoeken naar een klant of bestelling. Elke zoekopdracht op een e-mailadres of ordernummer leest de metatabel door.
  • Filters in overzichten. Op status, op datum, op een eigen veld: elk filter dat op meta_value zoekt, kost een scan.
  • WooCommerce-shops die nog geen HPOS gebruiken. Daar staan bestellingen ook in wp_postmeta, met alle gevolgen van dien.
  • Plug-ins die eigen data opslaan. Formulierinzendingen, boekingen, abonnementen: het gaat allemaal naar dezelfde tabel.

Een sprekend voorbeeld uit de praktijk: een site waar het openen van het besteloverzicht ruim vier seconden kostte. Na het aanmaken van één samengestelde index op meta_key en meta_value was dat een fractie van een seconde. Aan de code veranderde niets.

De autoload-index: opgelost in 2019, en toch nog een probleem

Er is één index die WordPress inmiddels wél zelf aanmaakt, en die illustreert het probleem misschien nog wel het beste.

In de tabel wp_options staan instellingen: van de sitenaam tot wat plug-ins er kwijt willen. Elke rij heeft een kolom autoload met de waarde yes of no. Staat die op yes, dan wordt die optie bij elke paginaweergave in het geheugen geladen. Niet als iemand ernaar vraagt, maar altijd.

Dat is bedacht voor een handvol instellingen die je toch elke keer nodig hebt. In de praktijk zetten veel plug-ins hun eigen data er ook in: gecachete API-antwoorden, licentiegegevens, complete importlogboeken. Al die rijen worden bij elk verzoek opgehaald, ook op een pagina die er niets mee doet. Bij een paar honderd kilobyte aan autoload-data is dat nauwelijks merkbaar; bij een paar megabyte wél.

WordPress heeft dat opgelost. In versie 5.3, uitgebracht in oktober 2019, is er een index op wp_options.autoload bijgekomen. In de aankondiging voor ontwikkelaars staat het zo: sites met veel rijen in wp_options met autoload aan, zien er een flinke prestatiewinst van.

Alleen: dat helpt niet elke site. Wij komen nog regelmatig omgevingen tegen die jaren geleden zijn opgezet. Die index wordt niet met terugwerkende kracht aangemaakt. De site is netjes bijgewerkt naar de nieuwste WordPress-versie, maar de index staat er niet.

Los daarvan blijft het de moeite waard om te kijken hoeveel autoload-data er eigenlijk is. Een index maakt het ophalen sneller, maar de twee megabyte aan vergeten plug-in-data niet kleiner.

Zelf nakijken hoe het ervoor staat

De simpelste controle is kijken welke indexes er überhaupt op een tabel liggen. Daar heb je één statement voor:

SHOW CREATE TABLE wp_postmeta;

Onderaan de uitvoer staan de indexen van de tabel. Bij een standaard WordPress-installatie is dat dit:

PRIMARY KEY (`meta_id`),
KEY `post_id` (`post_id`),
KEY `meta_key` (`meta_key`(191))

Daar staat precies wat het artikel hierboven beschrijft: een index op post_id, een index op meta_key, en niets op de combinatie van beide en niets op meta_value. Staat er meer, dan heeft iemand of een plug-in er ooit iets bij gezet. Dat is meestal goed nieuws.

Doe de test ook op wp_options. Zie je daar geen KEY `autoload` staan, dan mist die site de index uit WordPress 5.3. Hoeveel er dan bij elk verzoek wordt opgehaald, zie je zo:

SELECT ROUND(SUM(LENGTH(option_value)) / 1024 / 1024, 2) AS autoload_mb
FROM wp_options WHERE autoload = 'yes';

Onder een halve megabyte is doorgaans acceptabel. Komt daar een getal van meerdere megabytes uit, is er werk aan de winkel, en is een index maar het halve antwoord.

Een index lost niet alles op

Twee andere oorzaken komen net zo vaak voor, en daar helpt geen index tegen. De eerste is het aantal queries: een pagina die er honderden of duizenden afvuurt, schaalt niet, hoe goed de database ook is afgesteld. Dat vind je met profiling, niet met een index.

De tweede is de hoeveelheid data die wordt opgehaald. Vraag geen tienduizend rijen op als je er tien nodig hebt. Zie je in EXPLAIN de woorden Using filesort of Using temporary staan, dan moet de database eerst een hele stapel sorteren of een tijdelijke tabel maken voordat die iets kan teruggeven. Meestal is dat een signaal dat de query zelf te veel vraagt.

Hoe wij het oplossen

Ons échte managed-hosting-platform Core kijkt periodiek of al jullie projecten de juiste indexes hebben. Dat gebeurt automatisch, en het resultaat komt terug in de health checks onder 'Performance'.

Staat er een aanbeveling, dan kunnen jullie de indexes met één druk op de knop aanmaken.

Zie welke indexes ontbreken, met de tabel en de kolommen erbij. "Create Missing" maakt ze aan; verder hoeven jullie niets uit te zoeken.