Corrigé de la traduction des requêtes
Modèles de SQL ClickHouse pour les cinq motifs de requêtes Elasticsearch représentatifs.
Requête 1 : 10 principaux chemins de requête (état 200)
SELECT
LogAttributes['request_path'] AS request_path,
count() AS hits
FROM otel_logs
WHERE LogAttributes['log_type'] = 'web_access'
AND LogAttributes['status'] = '200'
GROUP BY request_path
ORDER BY hits DESC
LIMIT 10;Traitement de la requête par chaque moteur :
- ES : construit un compartiment Terms à partir du sous-champ de mot-clé
request_path.keyword. Le résultat est approximatif, car chaque shard renvoie son propre classement des N premières valeurs, qui sont ensuite fusionnées : un chemin classé onzième dans tous les shards peut donc être omis. Le paramètreshard_sizeélargit la liste avant fusion afin de réduire ce risque. - ClickHouse : envoie les lignes correspondantes dans une agrégation par hachage. Le résultat est exact, sans approximation par shard. Le coût de l'analyse est proportionnel au nombre de lignes où
status = '200'; il bénéficie de la colonne matérialiséeStatuset de la clé ORDER BY(ServiceName, Status, Timestamp).
Si cette requête de tableau de bord est très fréquente, promouvez request_path en colonne matérialisée (ou extrayez-le dans le collecteur). La version qui utilise LogAttributes['request_path'] fonctionne, mais analyse la Map entière pour chaque ligne.
Requête 2 : nombre d'erreurs 5xx par minute (dernière heure)
SELECT
toStartOfMinute(Timestamp) AS minute,
count() AS error_count
FROM otel_logs
WHERE Timestamp >= now() - INTERVAL 1 HOUR
AND toUInt16OrZero(LogAttributes['status']) >= 500
GROUP BY minute
ORDER BY minute;Traitement de la requête par chaque moteur :
- ES :
date_histogramparcourt le champ@timestampet crée un compartiment pour chaque intervalle fixe. Une protectionmax_buckets(65 536 par défaut) s'applique ; des périodes très longues ou des intervalles très courts provoquent une erreur. - ClickHouse :
toStartOfMinute()effectue une opération arithmétique entière peu coûteuse sur la colonneTimestamp. Le nombre de compartiments n'est pas plafonné. Avec une colonne matérialiséeStatus UInt16, le filtre devientStatus >= 500: une lecture directe de la colonne, sans décodage de la Map pour chaque ligne.
Variante pour un tableau de bord comportant une colonne matérialisée Status :
SELECT toStartOfMinute(Timestamp) AS minute, count() AS error_count
FROM otel_logs
WHERE Timestamp >= now() - INTERVAL 1 HOUR AND Status >= 500
GROUP BY minute ORDER BY minute;Requête 3 : recherche plein texte — « connection timeout » dans les journaux ERROR
SELECT Timestamp, ServiceName, Body
FROM otel_logs
WHERE SeverityText = 'ERROR'
AND hasTokenCaseInsensitive(Body, 'connection') -- accelerated by text skip index (>=26.2) or tokenbf_v1
AND hasTokenCaseInsensitive(Body, 'timeout') -- accelerated by text skip index (>=26.2) or tokenbf_v1
AND positionCaseInsensitive(Body, 'connection timeout') > 0 -- enforces phrase adjacency
ORDER BY Timestamp DESC
LIMIT 50;Pourquoi trois prédicats au lieu de deux ? Dans ES, match_phrase "connection timeout" exige que les deux lexèmes soient adjacents et dans l'ordre. Utiliser seulement hasTokenCaseInsensitive(X, 'connection') AND hasTokenCaseInsensitive(X, 'timeout') équivaudrait à match avec AND, et non à match_phrase : l'expression correspondrait aussi à "timeout occurred, connection dropped". Les appels à hasTokenCaseInsensitive apportent l'accélération de l'index de saut ; le contrôle positionCaseInsensitive(...) > 0 impose la contrainte de phrase. ClickHouse évalue les prédicats de gauche à droite : les contrôles peu coûteux de l'index de saut s'exécutent donc en premier, et la recherche de sous-chaîne ne s'applique qu'aux lignes restantes.
Pourquoi hasTokenCaseInsensitive plutôt que hasToken ? L'analyseur standard par défaut d'ES convertit le texte en minuscules lors de l'indexation et de la requête ; match_phrase "connection timeout" correspond donc aussi à « Connection Timeout », « CONNECTION TIMEOUT », etc. hasToken est sensible à la casse et manquerait ces variantes. hasTokenCaseInsensitive recherche les limites de lexème sans tenir compte de la casse et continue de bénéficier de l'index de saut.
Traitement de la requête par chaque moteur :
- ES :
match_phraseutilise l'index inversé du champmessagedécoupé en lexèmes. Chaque liste de publications demessageest toujours maintenue : les requêtes sont peu coûteuses, mais les écritures et le stockage le sont davantage. Une requêtematch_phraseparcourt les publications des deux lexèmes, trouve leur intersection et vérifie leur adjacence à l'aide des listes de publications positionnelles. - ClickHouse : un index de saut sur
Bodyélimine les granules (groupes de 8 192 lignes par défaut) qui ne contiennent assurément pas les lexèmes ; le moteur analyse ensuite uniquement les autres et applique le prédicatpositionCaseInsensitivepour vérifier la phrase. Les écritures et le stockage coûtent bien moins cher que l'index inversé d'ES, car les index de saut au niveau des granules sont minuscules. Des faux positifs restent possibles, d'où la nécessité du contrôle de sous-chaîne sur les granules candidats. Type d'index : privilégiezTYPE text(à partir de la version 26.2, spécialement conçu pour la recherche plein texte et offrant une recherche déterministe des lexèmes) ;tokenbf_v1/ngrambf_v1sont obsolètes depuis ClickHouse 26.2.
hasToken / hasTokenCaseInsensitive ou LIKE ou positionCaseInsensitive :
hasTokenrecherche des lexèmes entiers délimités par des signes de ponctuation ou des espaces (en respectant la casse).hasTokenCaseInsensitivefait de même sans tenir compte de la casse. Tous deux sont accélérés par un index de sauttext(à partir de la version 26.2, à privilégier) outokenbf_v1(obsolète). Utilisez la variante insensible à la casse pour traduire les requêtes ES qui font appel à l'analyseurstandard. Servez-vous-en comme filtre pour le premier passage peu coûteux.positionCaseInsensitive/LIKE '%substr%'recherchent de véritables sous-chaînes : ils ne tiennent pas compte des limites de lexème et ne sont pas accélérés par un index de saut. Utilisez-les comme vérification une fois quehasToken*a réduit les granules candidats. Employés seuls, ils analysent toutes les lignes.
Requête 4 : services uniques par jour (7 derniers jours)
-- Variant A: EXACT
SELECT
toDate(Timestamp) AS day,
uniqExact(ServiceName) AS unique_services
FROM otel_logs
WHERE Timestamp >= now() - INTERVAL 7 DAY
GROUP BY day
ORDER BY day;
-- Variant B: APPROXIMATE (HyperLogLog-based, much faster on high-cardinality columns)
SELECT
toDate(Timestamp) AS day,
uniq(ServiceName) AS unique_services
FROM otel_logs
WHERE Timestamp >= now() - INTERVAL 7 DAY
GROUP BY day
ORDER BY day;Choix en production : utilisez uniq() (approximatif) pour les tableaux de bord et les alertes. L'erreur est ≤ 1,6 %, avec une consommation de mémoire et de processeur bien inférieure. Réservez uniqExact aux requêtes d'audit ou de facturation qui exigent des nombres exacts. Pour cette charge de travail, ServiceName ne compte qu'environ 5 valeurs distinctes ; les deux fonctions sont donc dérisoires en coût. Le motif se généralise cependant : si la cardinalité est faible et connue, préférez uniqExact ; pour les colonnes sans limite ou à forte cardinalité (identifiants utilisateur ou de trace), préférez uniq ou uniqCombined.
Traitement de la requête par chaque moteur :
- ES : l'agrégation
cardinalityutilise HyperLogLog++ avec un paramètreprecision_thresholdréglable. Elle est toujours approximative. - ClickHouse : vous laisse le choix.
uniq→ HLL,uniqExact→ ensemble de hachage,uniqCombined→ hybride HLL+hachage.
Requête 5 : recherche d'une trace par trace.id
SELECT *
FROM otel_traces
WHERE TraceId = '<TRACE_ID>'
ORDER BY Timestamp ASC
LIMIT 1000;Traitement de la requête par chaque moteur :
- ES :
trace.idest un champkeywordindexé dans l'index inversé. La recherche s'effectue en O(log N) grâce au dictionnaire de termes, puis chaque publication est récupérée : elle est très rapide. - ClickHouse :
TraceIdne figure pas dans la clé primaireORDER BY. Une simple clauseWHERE TraceId = ?sans accélération analyserait toute la table. La solution consiste à créer un index de sautbloom_filtersurTraceId. L'index sonde le filtre de Bloom de chaque granule ; la plupart sont éliminés à faible coût et seuls les candidats sont analysés.
ALTER TABLE otel_traces
ADD INDEX trace_id_bf TraceId TYPE bloom_filter(0.01) GRANULARITY 4;
ALTER TABLE otel_traces MATERIALIZE INDEX trace_id_bf;Pourquoi TraceId ne doit PAS être la première colonne de ORDER BY :
Les clés de tri constituent un contrat avec le planificateur de requête : « la plupart des requêtes filtreront à partir de la gauche ». Placer TraceId en premier regrouperait les lignes d'une même trace, mais toutes les autres formes de requête (plage temporelle par service, erreurs les plus récentes, requêtes de vignettes de tableau de bord) devraient alors analyser toute la table, car elles ne connaissent pas TraceId. La recherche de trace consiste à chercher une aiguille dans une botte de foin : elle est rare et ponctuelle. Les index de saut (filtres de Bloom) sont précisément conçus pour ce cas ; ils offrent une recherche sous-linéaire sans imposer les contraintes d'une disposition physique particulière.