# Migrer une extension de CPT vers des tables personnalisées : retour chiffré

> Retour d'expérience sur la migration d'un CPT stockant 2 millions de lignes vers des tables personnalisées : script de migration, compatibilité ascendante et gains mesurés.

- Auteur : Clément Hadrot
- Publié le : 2025-04-21
- Mis à jour le : 2025-04-21
- Catégorie : Extensions
- URL : https://wpmoderne.dev.wordpress-developpement.fr/extensions/migrer-cpt-tables-personnalisees-retour-chiffre/

## L’essentiel

- La migration s'est faite en double écriture avant bascule totale
- Une couche de compatibilité a évité de réécrire tout le code appelant
- Le temps de requête moyen a été divisé par un facteur mesuré sur nos rapports

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

> L'essentiel à retenir : La migration s'est faite en double écriture avant bascule totale ; Une couche de compatibilité a évité de réécrire tout le code appelant ; Le temps de requête moyen a été divisé par un facteur mesuré sur nos rapports

```
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 :

1. **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.
2. **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.
3. **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.
