L’extension en question enregistre des relevés de capteurs pour des exploitations agricoles : température, humidité, niveau d’irrigation, un relevé toutes les quinze minutes par capteur, sur plusieurs centaines de capteurs installés chez les clients d’un fabricant de matériel connecté. Conçue initialement comme un CPT releve_capteur avec les valeurs stockées en post meta, l’extension a fonctionné sans problème pendant les deux premières années. Puis le volume a grimpé : à raison de plusieurs dizaines de milliers de relevés par jour toutes exploitations confondues, la table wp_postmeta a fini par dépasser deux millions de lignes rien que pour ce CPT, et les tableaux de bord d’analyse ont commencé à mettre plusieurs secondes à s’afficher.
Ce retour d’expérience détaille la migration effective vers des tables personnalisées, sans revenir sur les bases de dbDelta() déjà couvertes ailleurs — l’objectif ici est le récit de la bascule elle-même, ses risques, et ses résultats mesurés.
Diagnostiquer avant de migrer
Avant de se lancer, on a profilé les requêtes les plus lentes avec SAVEQUERIES activé sur un environnement de test chargé avec un export réel de la production. Le constat : chaque affichage de tableau de bord déclenchait une WP_Query avec des meta_query combinées sur trois champs différents (capteur, plage de dates, seuil d’alerte), ce qui générait des jointures multiples sur wp_postmeta — une table qui n’est simplement pas conçue pour ce type de filtrage combiné à ce volume, faute d’un schéma adapté aux relevés numériques et temporels.
Conception du nouveau schéma

CREATE TABLE {$wpdb->prefix}capteurs_releves (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
capteur_id BIGINT UNSIGNED NOT NULL,
exploitation_id BIGINT UNSIGNED NOT NULL,
releve_a DATETIME NOT NULL,
temperature DECIMAL(5,2) NULL,
humidite DECIMAL(5,2) NULL,
niveau_irrigation DECIMAL(5,2) NULL,
PRIMARY KEY (id),
KEY capteur_date (capteur_id, releve_a),
KEY exploitation_date (exploitation_id, releve_a)
) {$wpdb->get_charset_collate()};
Les colonnes typées (DECIMAL plutôt que des chaînes sérialisées) et les index composites sur les couples réellement interrogés (capteur + date, exploitation + date) sont la vraie source du gain : une jointure sur wp_postmeta ne peut par nature pas offrir ce niveau de sélectivité, quel que soit le soin apporté à l’indexation existante.
La migration en double écriture
Migrer deux millions de lignes en une seule opération, en coupant le service, n’était pas acceptable pour un client dont les alertes d’irrigation doivent rester actives en continu. La stratégie retenue s’est faite en trois phases étalées sur plusieurs semaines :
- Double écriture : chaque nouveau relevé continue d’être enregistré comme avant (CPT et post meta), mais est aussi écrit dans la nouvelle table, via un hook sur la fonction d’enregistrement existante. Aucune lecture ne change encore.
- Migration de l’historique en tâche de fond : un traitement par lots de 2 000 lignes, exécuté via une tâche planifiée toutes les minutes, copie progressivement l’historique complet vers la nouvelle table, avec suivi de progression en option pour permettre une reprise en cas d’interruption.
- Bascule des lectures : une fois l’historique entièrement migré et vérifié (comparaison de comptages et de sommes de contrôle entre les deux structures), le code de lecture des tableaux de bord est basculé vers la nouvelle table. L’écriture dans l’ancien CPT est arrêtée dans la foulée.
Une couche de compatibilité pour éviter une réécriture totale
Plutôt que de réécrire chaque appel existant dans le code de l’extension, une classe de façade a été introduite, avec la même interface publique que les fonctions historiques basées sur le CPT, mais une implémentation interne redirigée vers la nouvelle table :
class Capteurs_Releves_Repository {
public function obtenir_releves( int $capteur_id, DateTimeImmutable $depuis, DateTimeImmutable $jusqua ): array {
global $wpdb;
return $wpdb->get_results( $wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}capteurs_releves
WHERE capteur_id = %d AND releve_a BETWEEN %s AND %s
ORDER BY releve_a ASC",
$capteur_id,
$depuis->format( 'Y-m-d H:i:s' ),
$jusqua->format( 'Y-m-d H:i:s' )
) );
}
}
Les modules d’affichage et les rapports existants, qui appelaient auparavant une fonction capteurs_obtenir_releves() adossée au CPT, ont simplement vu cette fonction redirigée vers la nouvelle classe, sans toucher au code appelant. Le coût de la migration côté code applicatif s’est ainsi limité à la couche d’accès aux données, pas à l’ensemble des écrans.
Résultats mesurés
| Indicateur | Avant (CPT + postmeta) | Après (tables dédiées) |
|---|---|---|
| Temps moyen d’affichage d’un tableau de bord mensuel | Plusieurs secondes, dégradation continue avec le volume | Temps stable, quasi constant quel que soit le volume historique |
| Taille de wp_postmeta | En croissance continue et dominante sur la base | Allégée, le CPT n’est plus utilisé pour ce volume |
| Complexité des requêtes de rapport | meta_query imbriquées, difficiles à maintenir | SQL direct avec index dédiés, plus lisible |
Le point qu’on retiendrait le plus de ce projet : la double écriture temporaire a coûté un peu de complexité pendant quelques semaines, mais elle a rendu la bascule totalement transparente pour le client — aucune interruption de service, aucune perte de relevé, et une possibilité de revenir en arrière à tout moment tant que l’ancien CPT restait alimenté.
En résumé
Migrer un CPT à fort volume vers des tables personnalisées n’est pas une décision à prendre à la légère, mais elle devient nécessaire dès que wp_postmeta montre ses limites structurelles sur un schéma de données répétitif et fortement interrogé par plage temporelle. La clé du succès sur ce projet a été la progressivité : double écriture, migration de l’historique en tâche de fond, puis bascule des lectures une fois la cohérence vérifiée — jamais une coupure brutale, jamais une réécriture complète du code appelant grâce à une couche de compatibilité assumée comme temporaire mais fonctionnelle.