Skip to main content
Le cache des requêtes permet de calculer les requêtes SELECT une seule fois, puis de servir les exécutions suivantes de la même requête directement depuis le cache. Selon le type de requêtes, cela peut réduire considérablement la latence et la consommation de ressources du serveur ClickHouse.

Contexte, conception et limites

Les caches de requêtes peuvent généralement être considérés comme cohérents ou incohérents sur le plan transactionnel.
  • Dans les caches transactionnellement cohérents, la base de données invalide (écarte) les résultats de requête mis en cache si le résultat de la requête SELECT change ou est susceptible de changer. Dans ClickHouse, les opérations qui modifient les données incluent les insertions/mises à jour/suppressions dans les tables ou les merges avec collapsing. La mise en cache transactionnellement cohérente est particulièrement adaptée aux bases de données OLTP, par exemple MySQL (qui a supprimé le cache de requêtes après la version 8.0) et Oracle.
  • Dans les caches transactionnellement incohérents, de légères imprécisions dans les résultats de requête sont acceptées, en partant du principe que toutes les entrées de cache se voient attribuer une période de validité après laquelle elles expirent (par ex. 1 minute) et que les données sous-jacentes évoluent peu pendant cette période. Cette approche est globalement plus adaptée aux bases de données OLAP. Comme exemple où une mise en cache transactionnellement incohérente est suffisante, prenons un rapport de ventes horaire dans un outil de reporting consulté simultanément par plusieurs utilisateurs. Les données de ventes changent généralement assez lentement pour que la base de données n’ait besoin de calculer le rapport qu’une seule fois (ce que représente la première requête SELECT). Les requêtes suivantes peuvent ensuite être renvoyées directement depuis le cache de requêtes. Dans cet exemple, une période de validité raisonnable pourrait être de 30 min.
La mise en cache transactionnellement incohérente est traditionnellement fournie par des outils clients ou des paquets proxy (par ex. chproxy) qui interagissent avec la base de données. Par conséquent, la même logique de mise en cache et la même configuration sont souvent dupliquées. Avec le cache de requêtes de ClickHouse, la logique de mise en cache est déplacée côté serveur. Cela réduit les efforts de maintenance et évite les redondances.

Paramètres de configuration et utilisation

Dans ClickHouse Cloud, vous devez utiliser les paramètres au niveau de la requête pour modifier les paramètres du cache de requêtes. La modification des paramètres au niveau de la configuration n’est actuellement pas prise en charge.
clickhouse-local n’exécute qu’une seule requête à la fois. Comme la mise en cache en mémoire des résultats de requête n’a pas de sens pour un processus unique, le cache de requêtes en mémoire est désactivé dans clickhouse-local. Le cache de requêtes sur disque est en revanche disponible : ses entrées sont stockées dans un cache du système de fichiers sur disque, de sorte que les résultats mis en cache par un processus clickhouse-local peuvent être servis aux processus ultérieurs utilisant le même répertoire de cache du système de fichiers.
Le paramètre use_query_cache permet de contrôler si une requête donnée ou toutes les requêtes de la session en cours doivent utiliser le cache de requêtes. Par exemple, la première exécution de la requête
stockera le résultat de la requête dans le cache de requêtes. Les exécutions suivantes de la même requête (également avec le paramètre use_query_cache = true) liront le résultat calculé depuis le cache et le renverront immédiatement.
Le paramètre use_query_cache et tous les autres paramètres liés au cache de requêtes ne prennent effet que pour des statements SELECT autonomes. En particulier, les résultats des SELECT sur des vues créées par CREATE VIEW AS SELECT [...] SETTINGS use_query_cache = true ne sont pas mis en cache, sauf si le statement SELECT est exécuté avec SETTINGS use_query_cache = true.
La manière dont le cache est utilisé peut être configurée plus finement à l’aide des paramètres enable_writes_to_query_cache et enable_reads_from_query_cache (tous deux à true par défaut). Le premier paramètre contrôle si les résultats des requêtes sont stockés dans le cache, tandis que le second détermine si la base de données doit tenter de récupérer les résultats des requêtes depuis le cache. Par exemple, la requête suivante n’utilisera le cache que passivement, c’est-à-dire qu’elle tentera d’y lire sans y stocker son résultat :
Pour un contrôle maximal, il est généralement recommandé de n’utiliser les paramètres use_query_cache, enable_writes_to_query_cache et enable_reads_from_query_cache qu’avec des requêtes spécifiques. Il est également possible d’activer la mise en cache au niveau de l’utilisateur ou du profil (par ex. via SET use_query_cache = true), mais il faut garder à l’esprit que toutes les requêtes SELECT peuvent alors renvoyer des résultats en cache. Le cache de requêtes peut être vidé à l’aide du statement SYSTEM CLEAR QUERY CACHE. Le contenu du cache de requêtes est affiché dans la table système system.query_cache. Le nombre de succès et d’échecs du cache de requêtes depuis le démarrage de la base de données est affiché sous forme d’événements “QueryCacheHits” et “QueryCacheMisses” dans la table système system.events. Ces deux compteurs ne sont mis à jour que pour les requêtes SELECT exécutées avec le paramètre use_query_cache = true ; les autres requêtes n’affectent pas “QueryCacheMisses”. Ils décrivent le cache de requêtes dans son ensemble : lorsque le cache de requêtes sur disque est activé (voir ci-dessous), une requête absente du cache en mémoire puis servie depuis le disque compte pour un “QueryCacheHits”, et une requête absente des deux backends compte pour un “QueryCacheMisses”. Le champ query_cache_usage dans la table système system.query_log indique, pour chaque requête exécutée, si le résultat de la requête a été écrit dans le cache de requêtes ou lu depuis celui-ci. Les métriques QueryCacheEntries et QueryCacheBytes dans la table système system.metrics indiquent combien d’entrées / d’octets le cache de requêtes en mémoire contient actuellement. Elles ne tiennent pas compte des entrées du cache de requêtes sur disque, qui ne dispose actuellement d’aucune métrique d’occupation. Le cache de requêtes existe en un seul exemplaire par processus serveur ClickHouse. Cependant, les résultats en cache ne sont, par défaut, pas partagés entre les utilisateurs. Cela peut être modifié (voir ci-dessous), mais ce n’est pas recommandé pour des raisons de sécurité. Les résultats de requête sont référencés dans le cache de requêtes par l’arbre syntaxique abstrait (AST) de leur requête. La mise en cache est donc insensible à la casse : par exemple, SELECT 1 et select 1 sont considérées comme une seule et même requête. Pour que la correspondance soit plus naturelle, tous les paramètres au niveau de la requête liés au cache de requêtes et au formatage de sortie) sont supprimés de l’AST. Si la requête a été interrompue en raison d’une exception ou d’une annulation par l’utilisateur, aucune entrée n’est écrite dans le cache de requêtes. La taille du cache de requêtes en octets, le nombre maximal d’entrées du cache et la taille maximale de chaque entrée du cache (en octets et en enregistrements) peuvent être configurés à l’aide de différentes options de configuration du serveur.
Il est également possible de limiter l’utilisation du cache pour chaque utilisateur à l’aide des settings profiles et des contraintes sur les paramètres. Plus précisément, vous pouvez restreindre la quantité maximale de mémoire (en octets) qu’un utilisateur peut allouer au cache de requêtes en mémoire ainsi que le nombre maximal de résultats de requête stockés (ces limites ne s’appliquent pas au cache de requêtes sur disque). Pour cela, définissez d’abord les paramètres query_cache_max_size_in_bytes et query_cache_max_entries dans un profil utilisateur dans users.xml, puis rendez les deux paramètres readonly:
Pour définir la durée minimale pendant laquelle une requête doit s’exécuter pour que son résultat puisse être mis en cache, vous pouvez utiliser le paramètre query_cache_min_query_duration. Par exemple, le résultat de la requête
n’est mis en cache que si la requête s’exécute pendant plus de 5 secondes. Il est également possible de spécifier à quelle fréquence une requête doit s’exécuter pour que son résultat soit mis en cache — pour cela, utilisez le paramètre query_cache_min_query_runs. Les entrées du cache de requêtes deviennent obsolètes après une certaine période (time-to-live). Par défaut, cette période est de 60 secondes, mais une autre valeur peut être spécifiée au niveau de la session, du profil ou de la requête à l’aide du paramètre query_cache_ttl. Le cache de requêtes évince les entrées de manière “paresseuse”, c.-à-d. que lorsqu’une entrée devient obsolète, elle n’est pas immédiatement supprimée du cache. Au lieu de cela, lorsqu’une nouvelle entrée doit être insérée dans le cache de requêtes, la base de données vérifie si le cache dispose de suffisamment d’espace libre pour la nouvelle entrée. Si ce n’est pas le cas, la base de données essaie de supprimer toutes les entrées obsolètes. Si le cache ne dispose toujours pas d’assez d’espace libre, la nouvelle entrée n’est pas insérée. Si la requête est exécutée via HTTP, alors ClickHouse définit les en-têtes Age et Expires avec l’âge (en secondes) et l’horodatage d’expiration de l’entrée mise en cache. Les entrées du cache de requêtes sont compressées par défaut. Cela réduit la consommation globale de mémoire au prix d’écritures plus lentes dans / de lectures plus lentes depuis le cache de requêtes. Pour désactiver la compression, utilisez le paramètre query_cache_compress_entries. Il est parfois utile de conserver en cache plusieurs résultats pour la même requête. Cela peut être réalisé à l’aide du paramètre query_cache_tag, qui agit comme une étiquette (ou un espace de noms) pour les entrées du cache de requêtes. Le cache de requêtes considère comme différents les résultats de la même requête avec des tags différents. Exemple de création de trois entrées différentes dans le cache de requêtes pour la même requête :
Pour supprimer uniquement les entrées portant le tag tag du cache de requêtes, vous pouvez utiliser le statement SYSTEM CLEAR QUERY CACHE TAG 'tag'.

Mise en cache des sous-requêtes

Par défaut, use_query_cache dans la requête externe n’est pas appliqué aux sous-requêtes. Cela signifie que chaque sous-requête doit activer explicitement la mise en cache :
Dans cet exemple, seul le résultat de la sous-requête interne est mis en cache. La requête externe ne l’est pas. Pour activer en une seule fois la mise en cache de toutes les sous-requêtes, utilisez le paramètre query_cache_for_subqueries :
Pour désactiver explicitement la mise en cache pour une sous-requête spécifique lorsque la propagation globale est activée, définissez use_query_cache = false pour cette sous-requête :
Les entrées du cache de sous-requête du cache de requêtes en mémoire sont visibles dans system.query_cache avec is_subquery = 1. Les résultats des sous-requêtes sont également mis en cache sur disque si le cache de requêtes sur disque est activé, mais ces entrées n’apparaissent pas dans system.query_cache. Le paramètre query_cache_ttl s’applique également aux entrées du cache de sous-requête et peut être défini pour chaque sous-requête. ClickHouse lit les données des tables en blocs de max_block_size lignes. En raison du filtrage, de l’agrégation, etc., les blocs de résultat sont généralement bien plus petits que ‘max_block_size’, mais dans certains cas, ils peuvent aussi être bien plus grands. Le paramètre query_cache_squash_partial_results (activé par défaut) détermine si les blocs de résultat sont fusionnés (s’ils sont très petits) ou divisés (s’ils sont volumineux) en blocs de taille ‘max_block_size’ avant leur insertion dans le cache des résultats de requête. Cela réduit les performances d’écriture dans le cache de requêtes, mais améliore le taux de compression des entrées du cache et offre une granularité de blocs plus naturelle lorsque les résultats des requêtes sont ensuite servis depuis le cache de requêtes. Par conséquent, le cache de requêtes stocke pour chaque requête plusieurs blocs de résultat partiels. Bien que ce comportement constitue un bon choix par défaut, il peut être désactivé à l’aide du paramètre query_cache_squash_partial_results. De plus, les résultats des requêtes contenant des fonctions non déterministes ne sont pas mis en cache par défaut. Ces fonctions incluent Pour forcer malgré tout la mise en cache des résultats des requêtes contenant des fonctions non déterministes, utilisez le paramètre query_cache_nondeterministic_function_handling. Les résultats des requêtes impliquant des tables système (par exemple system.processes` ou information_schema.tables) ne sont pas mis en cache par défaut. Pour forcer malgré tout la mise en cache des résultats des requêtes portant sur des tables système, utilisez le paramètre query_cache_system_table_handling. Enfin, les entrées du cache de requêtes ne sont pas partagées entre les utilisateurs pour des raisons de sécurité. Par exemple, l’utilisateur A ne doit pas pouvoir contourner une politique de ligne sur une table en exécutant la même requête qu’un autre utilisateur B, pour lequel aucune telle règle n’existe. Cependant, si nécessaire, les entrées du cache peuvent être rendues accessibles à d’autres utilisateurs (c’est-à-dire partagées) en spécifiant le paramètre query_cache_share_between_users.

Cache de requêtes sur disque

Par défaut, le cache de requêtes conserve ses entrées en mémoire. Les résultats de requête peuvent également être mis en cache sur disque, en s’appuyant sur un cache du système de fichiers. Par rapport au cache de requêtes en mémoire, le cache de requêtes sur disque offre davantage d’espace et survit aux redémarrages du serveur. Une entrée n’est servie que par la construction du serveur qui l’a écrite : après une mise à niveau ou un retour à une version antérieure, les entrées écrites par la construction précédente entraînent des échecs de cache, puis finissent par être écrasées ou évincées. Pour utiliser le cache de requêtes sur disque, configurez d’abord un cache du système de fichiers dans la section filesystem_caches de la configuration du serveur :
puis indiquez son nom dans le paramètre query_cache_on_disk_cache_name :
Le cache de requêtes sur disque fonctionne indépendamment du cache de requêtes en mémoire et peut être activé séparément pour les lectures et/ou les écritures à l’aide des paramètres enable_reads_from_query_cache_on_disk et enable_writes_to_query_cache_on_disk (tous deux à true par défaut). Si les lectures sont activées pour les deux caches, le lookup est d’abord tenté en mémoire, puis sur disque uniquement en cas d’échec. Si les écritures sont activées pour les deux caches, le résultat est stocké dans les deux. Si aucun cache ne peut stocker le résultat (par ex. dans clickhouse-local, qui ne dispose pas de cache de requêtes en mémoire, avec enable_writes_to_query_cache_on_disk = false), la requête n’est pas rejetée en raison de fonctions non déterministes, de tables système ou d’un mode de dépassement de capacité autre que throw : ces vérifications servent uniquement à empêcher le stockage de résultats erronés. Les entrées sur disque sont compressées avec le codec spécifié par le paramètre query_cache_on_disk_codec (ZSTD(3) par défaut). Les paramètres serveur max_entry_size_in_bytes et max_entry_size_in_rows du cache de requêtes limitent les entrées des deux backends ; chaque backend mesure une entrée selon la manière dont il la stocke, de sorte que, sur disque, la limite en octets s’applique à l’entrée sérialisée et compressée. Définir l’un ou l’autre à 0 désactive les écritures dans les deux backends. Tous les autres paramètres du cache de requêtes (par ex. query_cache_ttl, query_cache_min_query_duration) s’appliquent au cache de requêtes sur disque de la même manière qu’au cache de requêtes en mémoire, à l’exception des deux cas ci-dessous. Les paramètres query_cache_max_size_in_bytes et query_cache_max_entries sont des limites par utilisateur qui concernent uniquement le cache de requêtes en mémoire. Ils ne s’appliquent pas au cache de requêtes sur disque : les entrées sur disque sont bornées par la taille du cache du système de fichiers configuré et évincées selon ses règles (voir ci-dessous), de sorte qu’un utilisateur doté d’un settings profile restrictif peut tout de même stocker davantage de résultats sur disque que ces paramètres ne l’autorisent en mémoire. Ce comportement est intentionnel : le cache du système de fichiers est une ressource partagée et de taille limitée, dans laquelle le cache de requêtes sur disque ne réserve pas d’espace. Le paramètre query_cache_share_between_users constitue une autre exception : il a la même signification pour les deux backends (une entrée écrite avec ce paramètre est servie à tous les utilisateurs), mais les entrées non partagées sont isolées différemment. Le cache de requêtes en mémoire conserve une seule entrée par requête et la rejette à la lecture si elle appartient à un autre utilisateur ; ainsi, une entrée non partagée de l’utilisateur A masque les écritures de l’utilisateur B jusqu’à ce que l’entrée expire ou soit évincée. Le cache de requêtes sur disque, en revanche, intègre l’utilisateur et l’ensemble de ses rôles actifs dans la clé d’une entrée non partagée, de sorte que l’utilisateur B enregistre un échec sur l’entrée de l’utilisateur A et stocke sa propre copie du résultat. Il en va de même pour un même utilisateur avec différents ensembles de rôles actifs, par ex. après SET ROLE : les rôles sont porteurs de privilèges et de politiques de ligne, si bien que les résultats calculés avec des rôles différents sont mis en cache séparément. Les entrées du cache de requêtes sur disque sont des entrées ordinaires du cache du système de fichiers, impossibles à distinguer des autres données mises en cache dans le même cache du système de fichiers : elles ne sont pas protégées contre la suppression et sont évincées selon les mêmes règles (par exemple, LRU, la moins récemment utilisée) lorsque le cache du système de fichiers manque d’espace. Leur taille totale et leur nombre ne font l’objet d’aucune limite distincte : seules s’appliquent les limites du cache du système de fichiers lui-même. Le statement SYSTEM CLEAR QUERY CACHE (ainsi que SYSTEM CLEAR QUERY CACHE TAG 'tag') supprime également les entrées (portant le tag indiqué ; les entrées corrompues et celles dont le format est incompatible sont supprimées quel que soit leur tag) du cache de requêtes sur disque dans le cache du système de fichiers sélectionné par le paramètre query_cache_on_disk_cache_name de la session (par exemple après SET query_cache_on_disk_cache_name = 'query_results'), sans toucher aux autres données du cache du système de fichiers. Pour trouver ces entrées, il parcourt les métadonnées de tous les segments de fichier du cache du système de fichiers (en mémoire, sans lecture sur le disque) et lit l’en-tête de chaque clé marquée comme entrée du cache de requêtes. Ce parcours prend un temps proportionnel au nombre de segments de fichier dans le cache du système de fichiers ; dédier un cache du système de fichiers aux résultats de requêtes permet donc de limiter le coût de SYSTEM CLEAR QUERY CACHE. Si le cache du système de fichiers a déjà évincé le début d’une entrée, le reste de l’entrée est conservé jusqu’à ce que le cache du système de fichiers l’évince ou que le même résultat de la requête soit réécrit, mais il n’est jamais servi. Une entrée encore en cours d’écriture est également conservée. Le statement SYSTEM DROP FILESYSTEM CACHE '<name>' les supprime en même temps que tout le reste du cache du système de fichiers. La table système system.query_cache et les métriques QueryCacheEntries et QueryCacheBytes n’affichent que les entrées du cache de requêtes en mémoire. Le nombre de succès et d’échecs du cache de requêtes sur disque est exposé par les événements « QueryCacheOnDiskHits » et « QueryCacheOnDiskMisses » dans la table système system.events. Ces événements ne détaillent que le backend sur disque, tandis que « QueryCacheHits » et « QueryCacheMisses » restent les compteurs du cache de requêtes dans son ensemble, et que « QueryCacheAgeSeconds » prend en compte les succès des deux backends. Le cache en mémoire étant sondé en premier, le nombre de succès en mémoire est égal à « QueryCacheHits » moins « QueryCacheOnDiskHits ».