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
- 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
SELECTchange 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.
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.
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.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 :
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.
users.xml, puis rendez les deux paramètres
readonly:
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 :
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 :
query_cache_for_subqueries :
use_query_cache = false pour cette sous-requête :
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
- les fonctions d’accès aux Dictionaries :
dictGet(), etc. - les User-Defined Functions sans la balise
<deterministic>true</deterministic>dans leur définition XML, - les fonctions qui renvoient la date ou l’heure actuelles :
now(),today(),yesterday(), etc., - les fonctions qui renvoient des valeurs aléatoires :
randomString(),fuzzBits(), etc., - les fonctions dont le résultat dépend de la taille et de l’ordre des chunks internes utilisés pour le traitement des requêtes :
nowInBlock(), etc.,rowNumberInBlock(),runningDifference(),blockSize(), etc., - les fonctions qui dépendent de l’environnement :
currentUser(),queryID(),getMacro(), etc.
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 sectionfilesystem_caches de la configuration du serveur :
query_cache_on_disk_cache_name :
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 ».