Skip to main content

Introduction

ClickHouse offre divers mécanismes pour accélérer les requêtes analytiques sur de grandes quantités de données dans des scénarios en temps réel. L’un de ces mécanismes pour accélérer vos requêtes consiste à utiliser des projections. Les projections permettent d’optimiser les requêtes en créant une réorganisation des données selon les attributs pertinents. Cela peut être :
  1. Une réorganisation complète
  2. Un sous-ensemble de la table d’origine avec un ordre différent
  3. Une agrégation précalculée (semblable à une vue matérialisée), mais avec un ordre aligné sur l’agrégation.

Comment fonctionnent les projections ?

En pratique, une projection peut être considérée comme une table supplémentaire (masquée) associée à la table d’origine. La projection peut avoir un ordre de tri différent, et donc un index primaire différent de celui de la table d’origine, et elle peut pré-calculer automatiquement et de manière incrémentielle des agrégats. Par conséquent, l’utilisation des projections offre deux « leviers d’optimisation » pour accélérer l’exécution des requêtes :
  • Utiliser correctement les index primaires
  • Pré-calculer les agrégats
Les projections sont, à certains égards, similaires aux vues matérialisées , qui permettent également d’avoir plusieurs ordres de tri et de pré-calculer des agrégations au moment de l’insertion. Les projections sont automatiquement mises à jour et restent synchronisées avec la table d’origine, contrairement aux vues matérialisées, qui doivent être mises à jour explicitement. Lorsqu’une requête cible la table d’origine, ClickHouse échantillonne automatiquement les clés primaires et choisit la table qui peut produire le même résultat correct, tout en nécessitant de lire le moins de données possible, comme illustré dans la figure ci-dessous :

Un stockage plus intelligent avec _part_offset

Depuis la version 25.5, ClickHouse prend en charge la colonne virtuelle _part_offset dans les projections, ce qui offre une nouvelle façon de définir une projection. Il existe désormais deux façons de définir une projection :
  • Stocker des colonnes complètes (comportement d’origine) : la projection contient l’intégralité des données et peut être lue directement, ce qui offre de meilleures performances lorsque les filtres correspondent à l’ordre de tri de la projection.
  • Stocker uniquement la clé de tri + _part_offset : la projection fonctionne comme un index. ClickHouse utilise l’index primaire de la projection pour localiser les lignes correspondantes, mais lit les données réelles depuis la table de base. Cela réduit la surcharge de stockage, au prix d’un peu plus d’E/S au moment de la requête.
Les approches ci-dessus peuvent également être combinées, en stockant certaines colonnes dans la projection et d’autres indirectement via _part_offset.

Quand utiliser les projections ?

Les projections constituent une fonctionnalité intéressante pour les nouveaux utilisateurs, car elles sont automatiquement maintenues lors de l’insertion des données. De plus, les requêtes peuvent simplement être envoyées à une seule table, les projections étant exploitées lorsque c’est possible pour accélérer le temps de réponse. Cela contraste avec les vues matérialisées, où l’utilisateur doit sélectionner la table cible optimisée appropriée ou réécrire sa requête selon les filtres. Cela reporte davantage de responsabilité sur les applications clientes et augmente la complexité côté client. Malgré ces avantages, les projections présentent certaines limitations inhérentes dont vous devez avoir conscience, et doivent donc être utilisées avec parcimonie.
  • Les projections ne permettent pas d’utiliser des TTL différents pour la table source et la table cible (masquée), tandis que les vues matérialisées autorisent des TTL différents.
  • Les lightweight updates et les suppressions ne sont pas prises en charge pour les tables avec projections.
  • Les vues matérialisées peuvent être chaînées : la table cible d’une vue matérialisée peut être la table source d’une autre vue matérialisée, et ainsi de suite. Ce n’est pas possible avec les projections.
  • Les définitions de projections ne prennent pas en charge les jointures, contrairement aux vues matérialisées. Cependant, les requêtes sur des tables avec projections peuvent librement utiliser des jointures.
  • Les définitions de projections ne prennent pas en charge les filtres (clause WHERE), contrairement aux vues matérialisées. Cependant, les requêtes sur des tables avec projections peuvent librement appliquer des filtres.
Nous recommandons d’utiliser les projections lorsque :
  • Une réorganisation complète des données est nécessaire. Bien que l’expression de la projection puisse, en théorie, utiliser un GROUP BY, les vues matérialisées sont plus efficaces pour maintenir des agrégats. L’optimiseur de requêtes est également plus susceptible d’exploiter des projections qui reposent sur une simple réorganisation, c.-à-d. SELECT * ORDER BY x. Vous pouvez sélectionner un sous-ensemble de colonnes dans cette expression afin de réduire l’empreinte de stockage.
  • Les utilisateurs acceptent l’augmentation potentielle de l’empreinte de stockage ainsi que le surcoût lié à l’écriture des données en double. Testez l’impact sur la vitesse d’insertion et évaluez le surcoût de stockage.

Exemples

Filtrage sur des colonnes qui ne font pas partie de la clé primaire

Dans cet exemple, nous allons voir comment ajouter une projection à une table. Nous verrons également comment une projection peut être utilisée pour accélérer les requêtes qui filtrent sur des colonnes ne faisant pas partie de la clé primaire d’une table. Pour cet exemple, nous utiliserons le jeu de données New York Taxi Data, disponible sur sql.clickhouse.com, trié par pickup_datetime. Écrivons une requête simple pour trouver tous les identifiants de trajet pour lesquels les passagers ont laissé un pourboire de plus de 200 $ à leur chauffeur : Notez que, comme nous filtrons sur tip_amount, qui ne figure pas dans ORDER BY, ClickHouse a dû parcourir l’ensemble de la table. Accélérons cette requête. Afin de préserver la table d’origine et les résultats, nous allons créer une nouvelle table et copier les données à l’aide d’un INSERT INTO SELECT :
Pour ajouter une projection, nous utilisons l’instruction ALTER TABLE avec l’instruction ADD PROJECTION suivante :
Il est nécessaire, après avoir ajouté une projection, d’utiliser l’instruction MATERIALIZE PROJECTION pour que les données qu’elle contient soient physiquement ordonnées et réécrites selon la requête spécifiée ci-dessus :
Exécutons de nouveau la requête maintenant que nous avons ajouté la projection : Notez que nous avons pu réduire considérablement le temps de requête et parcourir moins de lignes. Nous pouvons confirmer que la requête ci-dessus a bien utilisé la projection que nous avons créée en interrogeant la table system.query_log :

Utiliser les projections pour accélérer les requêtes sur les prix de l’immobilier au Royaume-Uni

Pour montrer comment les projections peuvent être utilisées pour améliorer les performances des requêtes, examinons un exemple basé sur un jeu de données réel. Pour cet exemple, nous allons utiliser la table de notre tutoriel UK Property Price Paid, qui contient 30,03 millions de lignes. Ce jeu de données est également disponible dans notre environnement sql.clickhouse.com. Si vous souhaitez voir comment la table a été créée et comment les données ont été insérées, vous pouvez consulter la page “The UK property prices dataset”. Nous pouvons exécuter deux requêtes simples sur ce jeu de données. La première liste les comtés de Londres où les prix de vente sont les plus élevés, et la seconde calcule le prix moyen par comté : Notez que, malgré leur grande rapidité, ces deux requêtes ont nécessité un parcours complet de la table des 30,03 millions de lignes, car ni town ni price ne figuraient dans notre instruction ORDER BY lorsque nous avons créé la table :
Voyons si nous pouvons accélérer cette requête à l’aide de projections. Pour préserver la table d’origine et les résultats, nous allons créer une nouvelle table et copier les données à l’aide d’un INSERT INTO SELECT :
Nous créons et peuplons la projection prj_oby_town_price, qui produit une table supplémentaire (masquée) dotée d’un index primaire, ordonnée par ville et par prix, afin d’optimiser la requête qui répertorie les comtés d’une ville donnée pour les prix payés les plus élevés :
Le paramètre mutations_sync est utilisé pour forcer l’exécution synchrone. Nous créons et peuplons la projection prj_gby_county — une table supplémentaire (masquée) qui précalcule de façon incrémentielle les valeurs agrégées de avg(price) pour les 130 comtés existants du Royaume-Uni :
S’il existe une clause GROUP BY dans une projection comme la projection prj_gby_county ci-dessus, le moteur de stockage sous-jacent de la table (masquée) devient alors AggregatingMergeTree, et toutes les fonctions d’agrégation sont converties en AggregateFunction. Cela garantit une agrégation incrémentielle correcte des données.
La figure ci-dessous est une visualisation de la table principale uk_price_paid_with_projections et de ses deux projections : Si nous exécutons maintenant de nouveau la requête qui affiche les comtés de Londres pour les trois prix les plus élevés, nous constatons une amélioration des performances de la requête : De même, pour la requête qui liste les comtés du Royaume-Uni avec les trois prix moyens les plus élevés : Notez que les deux requêtes ciblent la table d’origine et que, dans les deux cas, elles ont entraîné un parcours complet de la table (les 30,03 millions de lignes ont été lues depuis le disque) avant la création des deux projections. Notez également que la requête qui liste les comtés de Londres pour les trois prix les plus élevés nécessite la lecture de 2,17 millions de lignes. Lorsque nous avons utilisé directement une seconde table optimisée pour cette requête, seules 81 920 lignes ont été lues depuis le disque. La raison de cette différence est qu’actuellement, l’optimisation optimize_read_in_order mentionnée ci-dessus n’est pas prise en charge pour les projections. Nous examinons la table system.query_log pour vérifier que ClickHouse a automatiquement utilisé les deux projections pour les deux requêtes ci-dessus (voir la colonne projections ci-dessous) :

Autres exemples

Les exemples suivants utilisent le même jeu de données sur les prix au Royaume-Uni et comparent des requêtes avec et sans projections. Afin de préserver notre table d’origine (et ses performances), nous créons à nouveau une copie de la table à l’aide de CREATE AS et de INSERT INTO SELECT.

Créer une projection

Créons une projection agrégée selon les dimensions toYear(date), district et town :
Peuplez la projection avec les données existantes. (Sans la matérialiser, la projection ne sera créée que pour les données nouvellement insérées) :
Les requêtes suivantes comparent les performances avec et sans projections. Pour désactiver l’utilisation des projections, nous utilisons le paramètre optimize_use_projections, activé par défaut.

Requête 1. Prix moyen par année

Les résultats devraient être identiques, mais les performances sont meilleures dans ce dernier exemple !

Requête 2. Prix moyen par année à Londres

Requête 3. Les quartiers les plus chers

La condition (date >= ‘2020-01-01’) doit être modifiée afin de correspondre à la dimension de la projection (toYear(date) >= 2020)) : Là encore, le résultat est identique, mais notez l’amélioration des performances d’exécution de la 2e requête.

Combiner plusieurs projections dans une même requête

À partir de la version 25.6, en s’appuyant sur la prise en charge de _part_offset introduite dans la version précédente, ClickHouse peut désormais utiliser plusieurs projections pour accélérer une même requête comportant plusieurs filtres. Il est important de noter que ClickHouse lit toujours les données à partir d’une seule projection (ou de la table de base), mais peut utiliser les index primaires d’autres projections pour écarter les parties inutiles avant la lecture. Cela est particulièrement utile pour les requêtes qui filtrent sur plusieurs colonnes, chacune pouvant potentiellement correspondre à une projection différente.
Actuellement, ce mécanisme n’écarte que des parties entières. L’exclusion au niveau des granules n’est pas encore prise en charge.
Pour le démontrer, nous définissons la table (avec des projections utilisant des colonnes _part_offset) et insérons cinq lignes d’exemple correspondant aux schémas ci-dessus.
Ensuite, nous insérons des données dans la table :
Remarque : la table utilise des paramètres personnalisés à des fins d’illustration, comme des granules d’une seule ligne et des fusions de parts désactivées, ce qui n’est pas recommandé en production.
Cette configuration produit :
  • Cinq parts distinctes (une par ligne insérée)
  • Une entrée d’index primaire par ligne (dans la table de base et chaque projection)
  • Chaque part contient exactement une ligne
Avec cette configuration, nous exécutons une requête avec filtrage sur region et user_id. Comme l’index primaire de la table de base est construit à partir de event_date et id, il n’est d’aucune utilité ici. ClickHouse utilise donc :
  • region_proj pour élaguer les parts par région
  • user_id_proj pour élaguer davantage les parts selon user_id
Ce comportement est visible avec EXPLAIN projections = 1, qui montre comment ClickHouse sélectionne et applique les projections.
La sortie EXPLAIN (affichée ci-dessus) présente le plan de requête logique, de haut en bas : Au final, 1 seule part sur 5 est lue dans la table de base. En combinant l’analyse des index de plusieurs projections, ClickHouse réduit considérablement le volume de données analysées, ce qui améliore les performances tout en maintenant un faible surcoût de stockage.
Dernière modification le 14 août 2026