ClickHouse Rust : Ordre des colonnes calculées dans les vues
Problème
Lors de l'utilisation de SELECT alias.* , extra_col FROM my_view alias LEFT JOIN ..., ClickHouse
développe alias.* pour inclure TOUTES les colonnes définies dans la vue — y compris les colonnes
calculées ajoutées par la vue elle-même (p. ex. SELECT *, expr AS trending_score FROM base).
Ces colonnes calculées apparaissent À L'INTÉRIEUR du développement alias.*, AVANT les colonnes
ajoutées après dans la requête externe.
Le crate Rust clickhouse (#[derive(Row)]) désérialise positionnellement — le champ N
dans la struct Rust reçoit la colonne N du résultat de la requête. Si l'ordre des champs de la struct
ne correspond pas à l'ordre réel des colonnes SQL, les mauvais octets se retrouvent dans les mauvais champs,
causant des erreurs de type à la désérialisation même si le nombre de colonnes est correct.
Symptômes
- HTTP 500 sur une variante de tri (p. ex.
sort=trending,sort=popular) mais pas sur d'autres (p. ex.sort=recent) qui interrogent une vue ou table différente - Erreur du type :
"cannot decode Float64 from String"ou autre erreur de type - Le nombre de colonnes est correct (pas d'erreur « not enough data »)
- Le bug apparaît après l'ajout d'une colonne calculée à une vue existante
Explication de la cause racine
Supposons que trending_videos soit définie comme :
CREATE VIEW trending_videos AS
SELECT
*, -- toutes les colonnes de base
(views * 0.5 + likes * 2.0) AS trending_score -- colonne calculée AJOUTÉE ICI
FROM video_stats;
Une requête externe effectue ensuite :
SELECT
tv.*, -- se développe en : base_cols..., trending_score
text_track_ref, -- les colonnes de sous-titre viennent APRÈS
text_track_content
FROM trending_videos tv
LEFT JOIN subtitle_subquery USING (id);
L'ordre réel des colonnes du résultat SQL est :
[...base_cols, trending_score, text_track_ref, text_track_content]
Mais si la struct Rust a été écrite avec les champs de sous-titre avant trending_score :
pub struct TrendingVideo {
// ...champs de base...
pub text_track_ref: String, // position N <- FAUX : reçoit les octets de trending_score
pub text_track_content: String, // position N+1 <- FAUX : reçoit les octets de text_track_ref
pub trending_score: f64, // position N+2 <- FAUX : reçoit les octets de text_track_content
}
ClickHouse envoie un Float64 alors que Rust attend une String → erreur de désérialisation → 500.
Pourquoi seules certaines requêtes échouent
La variante sort=recent interroge directement video_stats avec vs.* — cette vue n'a
pas de colonnes calculées supplémentaires, donc les colonnes de sous-titre arrivent à la même
position relative que la struct s'y attend. Seule la vue trending_videos ajoute trending_score
à l'intérieur de vs.*, décalant tout ce qui suit.
Étapes de débogage
-
Identifiez quelles variantes de requête échouent ou réussissent. Les défaillantes utilisent probablement une vue différente ou ont une source
SELECT *différente. -
*Développez le `SELECT alias.` défaillant** — exécutez la requête de vue interne directement :
curl -u 'user:pass' 'https://clickhouse-host:8443' \ --data-binary "SELECT * FROM trending_videos LIMIT 0 FORMAT TabSeparated"Ou utilisez
DESCRIBE TABLE trending_videospour voir l'ordre des colonnes déclarées. -
Listez l'ordre réel des colonnes de la requête externe complète :
curl ... --data-binary \ "SELECT tv.*, '' as text_track_ref, '' as text_track_content FROM trending_videos tv LIMIT 0 FORMAT TabSeparatedWithNames" -
Comparez avec l'ordre des champs de la struct Rust ligne par ligne — ils doivent correspondre exactement.
-
Trouvez le champ mal placé — recherchez les colonnes calculées ajoutées à la vue qui apparaissent à l'intérieur de
alias.*mais après des colonnes qui apparaissent dans leSELECTexterne.
Solution
Réorganisez les champs de la struct Rust pour correspondre à l'ordre réel des colonnes SQL — les colonnes
VUE calculées doivent apparaître avant toute colonne ajoutée dans le SELECT externe :
pub struct TrendingVideo {
// ...champs de base dans le même ordre que la table de base...
// trending_score provient de tv.* (calculée par la vue), donc elle apparaît
// AVANT les colonnes de sous-titre que nous ajoutons dans la requête externe
pub trending_score: f64,
// Les colonnes de sous-titre sont ajoutées après tv.* dans le SELECT externe
pub text_track_ref: String,
pub text_track_content: String,
}
NE modifiez PAS la requête SQL ou la vue — alignez simplement l'ordre des champs de la struct.
Règle clé
Quand une VUE ClickHouse ajoute une colonne calculée via
SELECT *, expr AS col FROM base, cette colonne devient partie de la liste des colonnes de la vue. ToutSELECT alias.*dans une requête externe l'émettra dans l'ordre déclaré de la vue — AVANT toute colonne supplémentaire ajoutée aprèsalias.*dans leSELECTexterne. La struct Rust#[derive(Row)]doit refléter cet ordre exact.
Prévention
- Quand vous ajoutez une colonne calculée à une vue ClickHouse, vérifiez immédiatement TOUTES les
structs Rust qui interrogent cette vue avec
SELECT alias.*et mettez à jour l'ordre des champs. - Ajoutez un test d'intégration qui interroge le point de terminaison affecté ; la CI détectera les écarts futurs d'ordre avant le staging.
- Envisagez d'utiliser
SELECT col1, col2, ..., trending_score, text_track_ref, text_track_content(liste explicite de colonnes au lieu de*) dans la requête externe pour rendre l'ordre explicite et immunisé contre les changements de schéma de vue.
Compétences connexes
clickhouse-rust-type-mismatches— couvre « not enough data » (erreur du NOMBRE de colonnes), encodage FixedString, problèmes de nullabilité Option vs String. Cette compétence couvre les erreurs d'ordre des colonnes (le nombre de colonnes est correct, les types correspondent, mais l'ordre positionnel est faux).
Références
- clickhouse crate Row derive macro
- Expansion ClickHouse
SELECT *: suit l'ordre de déclaration des colonnes de la table/vue source