Ce qui se passe
Les données analytiques regorgent de libellés répétés : produits, pays, user
agents, catégories. L'équipe DuckDB montre ce qu'ils coûtent dans un
GROUP BY et ajoute la parade à son
guide de performance.
L'idée vient de Richard Wesley, à l'occasion d'un rapport de consommation
mémoire.
Pourquoi une chaîne coûte
DuckDB agrège par table de hachage. Une valeur texte y occupe une structure de 16 octets : jusqu'à 12 octets, la chaîne tient dedans ; au-delà, on garde un préfixe de 4 octets et un pointeur. Pour une chaîne longue, il faut hacher tous ses octets à chaque ligne, suivre le pointeur pour confirmer une égalité, et recopier la chaîne dans la table à chaque nouveau groupe. La table s'élargit, tient moins bien dans les caches et déborde plus tôt sur disque. Le dictionnaire que DuckDB applique aux colonnes sur disque n'y change rien : à l'agrégation, chaque clé redevient une chaîne complète.
La table de dimension
Le jeu public des trains néerlandais compte 380 959 lignes pour 537 gares distinctes. Trois étapes :
- mesurer la cardinalité : 537, trop pour
UTINYINT(255), assez pourUSMALLINT(65 535) ; - attribuer les clés dans l'ordre alphabétique, ce qui permet de trier sur l'entier et rend la numérotation reproductible ;
- réécrire la table de faits avec la clé à la place du texte, une fois pour toutes.
CREATE TABLE stations AS
SELECT station_name,
(row_number() OVER (ORDER BY station_name))::USMALLINT AS station_id
FROM (SELECT DISTINCT station_name FROM train_services
WHERE station_name IS NOT NULL);
CREATE TABLE trains AS
SELECT t.* EXCLUDE (station_name), s.station_id
FROM train_services t LEFT JOIN stations s USING (station_name);On agrège ensuite sur station_id et on joint stations sur le résultat
réduit — après le LIMIT pour un top N. Sur une plage aussi étroite,
l'optimiseur peut choisir un agrégat à hachage parfait (perfect_ht_threshold)
qui indexe directement par la clé.
Variantes
ENUMdonne l'essentiel du gain sans jointure, si la liste est fixe. La table de dimension l'emporte quand de nouvelles valeurs arrivent, quand on veut des attributs en plus, ou quand les données sortent vers Parquet ou CSV.- Plusieurs colonnes : une dimension par colonne, chacune avec son type ;
les 15 types de train tiennent en
UTINYINT. - Mise à jour : les nouvelles valeurs, filtrées par
ANTI JOIN, prennent des clés après le maximum. Elles perdent alors l'ordre alphabétique : il faut reconstruire de temps en temps.
Quand ça ne suffit pas
Des clés étroites rendent chaque groupe plus petit, pas moins nombreux. Le
rapport duckdb#14584 agrégeait
9,2 milliards de lignes en 320 millions de groupes, sur 8 threads. Chaque
thread tenant sa propre table, la mémoire tendait vers threads × groupes.
Réduire threads échange de la vitesse contre de la mémoire.
Quand s'abstenir
Chaînes courtes, colonnes presque uniques, requête ponctuelle, ou colonne
jamais agrégée : le gain ne paie pas la complexité. Le gain reste d'ailleurs
modeste sur ces 380 000 lignes. Comparer les deux versions avec
EXPLAIN ANALYZE, et la mémoire avec un memory_limit réduit.
Source : Faster String Aggregations with Dimension Tables, DuckDB, 2 octobre 2026.