Image de couverture : Arrêtez le COUNT(DISTINCT) sur des milliards de lignes : HyperLogLog dans PostgreSQL
Bases de données

Arrêtez le COUNT(DISTINCT) sur des milliards de lignes : HyperLogLog dans PostgreSQL

29 June 2026
4 min de lecture
3 vues
Sébastien Muler

Arrêtez le COUNT(DISTINCT) sur des milliards de lignes : HyperLogLog dans PostgreSQL

Quand votre équipe produit vous demande "combien de viewers uniques ont regardé chaque seconde de cette vidéo ?", la première réponse qui vient à l'esprit est un bon vieux COUNT(DISTINCT viewer_hash). Simple, lisible, évident. Et totalement inutilisable à grande échelle.

Cet article s'inspire d'un retour d'expérience publié sur dev.to par Ahmet Gedik, développeur backend chez TrendVidStream, une plateforme de streaming multi-région. Leur stack est délibérément sobre — PHP 8.4, PostgreSQL, des crons, des déploiements FTP — mais leur problème de cardinalité, lui, n'a rien de sobre.

Le problème : 40 millions d'événements par jour

Chaque lecteur vidéo envoie un heartbeat toutes les cinq secondes : identifiant vidéo, hash anonymisé du viewer, région, et position dans la vidéo. Sur 8 régions, cela représente environ 40 millions d'événements par jour. L'objectif : afficher une heatmap sous la barre de lecture, indiquant seconde par seconde combien de viewers uniques regardaient à cet instant précis.

La requête naïve ressemble à ceci :

SELECT second_bucket, COUNT(DISTINCT viewer_hash)
FROM video_events
WHERE video_id = $1
  AND event_day = $2
GROUP BY second_bucket
ORDER BY second_bucket;

Avec 90 jours de rétention, on parle de 3,6 milliards de lignes. Le temps de réponse sur cette requête ? Plusieurs minutes. Rédhibitoire pour une UI temps réel.

La solution : l'extension postgresql-hll

HyperLogLog (HLL) est un algorithme probabiliste de comptage de cardinalité. Il ne compte pas exactement — il estime — mais avec une précision configurable et une empreinte mémoire dérisoire. Au lieu de stocker chaque valeur distincte, il maintient une structure compacte qui permet d'estimer le nombre d'éléments uniques avec une erreur typique inférieure à 1 %.

L'extension postgresql-hll intègre ce type de données directement dans PostgreSQL. L'idée est d'agréger les hashes en temps réel plutôt que de les stocker bruts, puis de ne requêter que les agrégats.

Schéma de la table d'agrégats

CREATE TABLE video_second_hll (
  video_id     BIGINT        NOT NULL,
  region       TEXT          NOT NULL,
  event_day    DATE          NOT NULL,
  second_bucket SMALLINT     NOT NULL,  -- 0..86399
  viewers_hll  hll           NOT NULL DEFAULT hll_empty(),
  PRIMARY KEY (video_id, region, event_day, second_bucket)
);

Chaque ligne ne stocke plus des événements bruts, mais un sketch HLL représentant l'ensemble des viewers pour une seconde donnée. La taille typique d'un sketch : quelques kilooctets, quelle que soit la cardinalité réelle.

Insertion via upsert

Le worker PHP (exécuté en cron toutes les minutes) pousse les heartbeats agrégés :

INSERT INTO video_second_hll
  (video_id, region, event_day, second_bucket, viewers_hll)
VALUES
  ($1, $2, $3, $4, hll_add(hll_empty(), hll_hash_text($5)))
ON CONFLICT (video_id, region, event_day, second_bucket)
DO UPDATE SET
  viewers_hll = hll_union(
    video_second_hll.viewers_hll,
    EXCLUDED.viewers_hll
  );

L'opération hll_union fusionne deux sketches HLL — ce qui permet aussi de combiner les régions a posteriori sans re-scanner les événements bruts.

Requête de lecture pour la heatmap

SELECT
  second_bucket,
  ROUND(hll_cardinality(hll_union_agg(viewers_hll)))::INT AS unique_viewers
FROM video_second_hll
WHERE video_id   = $1
  AND event_day  = $2
GROUP BY second_bucket
ORDER BY second_bucket;

Temps de réponse constaté : moins de 50 ms pour une vidéo avec des milliers de secondes et plusieurs régions. Contre plusieurs minutes avec l'approche naive.

Les paramètres qui comptent vraiment

HyperLogLog expose deux paramètres principaux dans l'extension :

  • log2m : taille du registre (de 4 à 31). Une valeur de 14 donne une erreur standard d'environ 0,81 % — amplement suffisant pour une heatmap.
  • regwidth : largeur des registres en bits (1 à 8). La valeur par défaut de 5 couvre des cardinalités jusqu'à plusieurs milliards.
-- Créer un sketch avec précision explicite
SELECT hll_empty(14, 5);

Pour la plupart des cas d'usage analytics, les valeurs par défaut sont raisonnables. N'augmentez log2m que si vous avez besoin d'une précision inférieure à 0,5 % sur des cardinalités très élevées — cela multiplie la taille des sketches.

Ce que cela change côté PHP

Du côté applicatif, l'intégration est transparente. Avec PDO ou doctrine/dbal, les sketches HLL sont manipulés comme des chaînes opaques. Aucune bibliothèque PHP spécifique n'est nécessaire : toute la logique HLL reste dans PostgreSQL.

$stmt = $pdo->prepare(<<<SQL
    SELECT second_bucket,
           ROUND(hll_cardinality(hll_union_agg(viewers_hll)))::INT AS unique_viewers
    FROM video_second_hll
    WHERE video_id = ? AND event_day = ?
    GROUP BY second_bucket
    ORDER BY second_bucket
SQL);
$stmt->execute([$videoId, $day]);
$heatmap = $stmt->fetchAll(PDO::FETCH_KEY_PAIR);

Le tableau résultant mappe chaque seconde à son nombre estimé de viewers uniques — prêt à être sérialisé en JSON pour le frontend.

Conclusion

HyperLogLog n'est pas une solution universelle. Si vous avez besoin d'un comptage exact, d'un débogage viewer par viewer, ou que votre volumétrie reste raisonnable, COUNT(DISTINCT) reste parfaitement valide.

Mais dès que vous atteignez des dizaines de millions d'événements quotidiens et que votre question est "combien d'uniques ?" plutôt que "qui exactement ?", l'extension postgresql-hll offre un rapport précision/performance/complexité imbattable — sans changer de base de données, sans infrastructure supplémentaire, et sans réécrire votre stack PHP.

Le vrai enseignement de ce retour d'expérience : le bon algorithme vaut mieux que le meilleur matériel. Un sketch de quelques Ko remplace 3,6 milliards de lignes, et la requête passe de plusieurs minutes à 50 ms sur le même serveur.


Source originale : Building Per-Second Video Heatmap Analytics with HyperLogLog in Postgres par Ahmet Gedik sur dev.to.

Partager cet article