2026/9/14 19:56:15

ScyllaDB 二级索引机制详解:global/local 索引的 target 存储格式、默认命名规则与源码解析

ScyllaDB 二级索引机制详解:global/local 索引的 target 存储格式、默认命名规则与源码解析 ScyllaDB 二级索引机制详解global/local 索引的 target 存储格式、默认命名规则与源码解析【免费下载链接】scylladbNoSQL data store using the Seastar framework, compatible with Apache Cassandra and Amazon DynamoDB项目地址: https://gitcode.com/GitHub_Trending/sc/scylladb本篇围绕 ScyllaDB 中二级索引Secondary Indexes的核心设计展开global 索引与 local 索引如何通过索引元数据中的target字段加以区分、CQL 层面支持的各类索引目标普通列、集合 key/value/entry、冻结集合全量索引如何序列化、索引的默认命名规则及其冲突处理以及 local 索引采用 JSON 格式存储主键信息的底层原因。读完后你将能够准确解读system_schema.indexes中options列的每一种取值并结合 index/secondary_index.cc 与 cql3/statements/index_target.cc 的源码定位索引解析、校验与序列化的具体实现。一、global 索引与 local 索引区别在哪里Scylla 中的二级索引目前可分为两类见 docs/dev/secondary_index.mdglobal 索引默认类型索引以被索引列作为自己的分区键partition key索引数据分散到所有节点上local 索引索引与基表共享分区键索引数据只存在于基表所在的节点上天然支持本地查找local lookup。这两类索引的区分信息并不单独存放而是编码在索引的index target中——一个保存在索引 options 映射map下、键名为target的字符串。在 cql3/statements/index_target.cc 中可以看到这一约定的常量定义const sstring index_target::target_option_name target;官方文档给出的同一张表、同一列上分别建立 global 索引和 local 索引的实际输出示例如下SELECT * FROM system_schema.indexes; keyspace_name | table_name | index_name | kind | options ---------------------------------------------------------------------------- demodb | t | local_t_v1 | COMPOSITES | {target: {pk:[p],ck:[v1]}} demodb | t | t_v1_idx | COMPOSITES | {target: v1}可以观察到global 索引的target只是列名字符串v1而 local 索引的target是一个带pk/ck字段的 JSON 对象字符串。这一判断逻辑在源码中由target_parser::is_local()实现见 index/secondary_index.ccbool target_parser::is_local(sstring target_string) { std::optionalrjson::value json_value rjson::try_parse(target_string); if (!json_value || !json_value-IsObject()) { return false; } rjson::value* pk rjson::find(*json_value, PK_TARGET_KEY); // pk rjson::value* ck rjson::find(*json_value, CK_TARGET_KEY); // ck return pk ck pk-IsArray() ck-IsArray() !pk-Empty() !ck-Empty(); }也就是说只要target能解析为同时包含非空pk数组和ck数组的 JSON 对象该索引即被判定为 local 索引否则视为 global 索引。这也解释了为什么 global 索引的 target 绝不应该恰好写成这种 JSON 形状。二、默认命名规则表名_列名_idx与冲突后缀文档明确了索引的默认命名约定docs/dev/secondary_index.md默认索引名由表名 列名 _idx后缀拼接生成与表名类似索引名只能包含word characters字母、数字、下划线因此构造索引名之前列名中所有非 word character 的字符会被直接丢弃若生成的名字已被占用例如有人已经手工建了同名的具名索引则追加_X其中 X 是保证名称唯一的最小数字global 与 local 索引遵循完全相同的默认命名规则。举例在表t的列v1上建索引默认名为t_v1_idx若该名字已被占用则变为t_v1_idx_1。当对索引归属存疑时文档建议用以下命令查看索引的 target 与类型DESCRIBE index_name; SELECT * FROM system_schema.indexes;三、global 索引的 target 格式与序列化global 索引的 target 通常就是被索引列名本身但如果索引具有特定类型则采用类型前缀 列名的形式。CQL 支持的全部类型及其序列化形式如下索引类型CQL 目标写法options 中存储的 target 字符串普通索引regular indexvv冻结集合全量索引full collection indexFULL(v)v无full前缀与普通索引相同map 键索引KEYS(v)keys(v)map/set/list 值索引VALUES(v)values(v)map 条目键值对索引ENTRIES(v)entries(v)序列化规则除full之外其他类型使用小写的类型名作为前缀。因此合法的 target 字符串为v、keys(v)、values(v)、entries(v)而列v上的冻结集合全量索引直接存为v与普通索引的存储形式一致。这一解析逻辑的源头是正则表达式定义于 cql3/statements/index_target.cc 并在 index/secondary_index.cc 中复用const boost::regex index_target::target_regex(^(keys|entries|values|full)\\((.)\\)$);target_parser::parse()的匹配流程index/secondary_index.cc分为三步先用上述正则匹配keys(...)/entries(...)/values(...)/full(...)形式命中则通过index_target::from_sstring()把前缀文本映射为target_typeentries对应枚举值keys_and_values见 cql3/statements/index_target.cc并解析括号内的列名若正则未命中则尝试把整个字符串当作 JSON 解析——即 local 索引分支两者都不符合时回退fallback把整个字符串视为单一目标列名类型取regular_values第 77 行。反向序列化由target_parser::serialize_targets()实现index/secondary_index.cc当只有单个目标且是单列时regular_values与full两种类型都直接输出转义后的列名不加前缀而collection_values、keys、keys_and_values则输出to_sstring(type) (列名)与上表的存储形式一一对应。列名转义防止目标字符串被误解析CQL 列名可以包含任意字符。如果列名本身含有括号、大括号等字符直接拼进 target 字符串就会让后续的正则解析产生歧义。因此序列化时使用column_identifier::to_cql_string()即 CQL 引号标识符语法进行转义列名用双引号包裹内部的双引号字符被加倍。文档给出的两个典型案例列名hEllo因区分大小写需保留原文存储为hEllo带双引号注意这里引号是 target 字符串的一部分列名keys(m)若不加引号会被正则误认为keys类型的索引目标因此存储为keys(m)整体加双引号后正则^(keys|...)\(不再命中。对应的源码在 cql3/statements/index_target.ccsstring index_target::escape_target_column(const cql3::column_identifier col) { return col.to_cql_string(); } sstring index_target::unescape_target_column(std::string_view str) { // We dont have a reverse version of util::maybe_quote(), so // we need to open-code it here. Cassandra has this too - in // index/TargetParser.java if (str.size() 2 str.starts_with() str.ends_with()) { str.remove_prefix(1); str.remove_suffix(1); // remove doubled quotes in the middle of the string, which to_cql_string() // adds. This code is inefficient but rarely called so its fine. static const boost::regex double_quote_re(\\); return boost::regex_replace(std::string(str), double_quote_re, \); } return sstring(str); }unescape_target_column()是escape_target_column()的手写逆过程剥掉首尾双引号再把中间成对出现的还原为单个。代码注释也说明了它与 Cassandra 的index/TargetParser.java保持了行为对齐。四、local 索引的 targetJSON 格式的主键定义local 索引的 target 由显式的分区键 被索引列定义组成且当前要求该分区键必须与基表的分区键一致。其序列化形式是一段表示主键的 JSON 字符串文档给出的两个示例{ pk: [p1, p2, p3], ck: [v] }{ pk: [p], ck: [v] }其中pk为分区键列数组ck为被索引的聚类/普通列数组。解析实现位于 index/secondary_index.ccstd::optionalrjson::value json_value rjson::try_parse(target); if (json_value json_value-IsObject()) { rjson::value* pk rjson::find(*json_value, PK_TARGET_KEY); rjson::value* ck rjson::find(*json_value, CK_TARGET_KEY); if (!pk || !ck || !pk-IsArray() || !ck-IsArray()) { throw std::runtime_error(pk and ck fields of JSON definition must be arrays); } for (const rjson::value v : pk-GetArray()) { info.pk_columns.push_back(get_column(sstring(rjson::to_string_view(v)))); } for (const rjson::value v : ck-GetArray()) { info.ck_columns.push_back(get_column(sstring(rjson::to_string_view(v)))); } info.type index_target::target_type::regular_values; return info; }几个值得注意的实现细节JSON 分支中列名无需额外转义——JSON 本身已能正确表达特殊字符源码注释index/secondary_index.cc明确说明了这一点pk/ck必须都是数组否则抛出pk and ck fields of JSON definition must be arrays每一列都会通过get_column()在 schema 中做存在性校验找不到时抛出Column {name} not foundlocal 索引的target_type固定为regular_values整个解析过程带有统一异常包装任何解析失败都会向上抛出configuration_exception信息形如Unable to parse targets for index {name} ({target})index/secondary_index.cc便于从报错直接定位是哪条索引、哪个 target 字符串出了问题。反向序列化时serialize_targets()对多目标/多列场景统一输出 JSON 对象index/secondary_index.cc第一个目标写入pk数组其余目标依次写入ck数组再由rjson::print()输出与文档中的 JSON 示例格式完全一致。五、如何验证与排查结合本文与仓库源码实际操作中的验证路径如下查看索引 target 与类型DESCRIBE index_name; SELECT * FROM system_schema.indexes;options列中的target值可直接对照本文的格式表判断索引是 global 还是 local、属于哪种索引类型。确认命名是否符合默认规则若默认名t_v1_idx已存在新索引会命名为t_v1_idx_1等带最小唯一后缀的名字列名中的非 word character 已被丢弃可用SELECT column_name FROM system_schema.columns ...核对原始列名。解析报错时的排查依据从源码结构看target_parser::parse()是唯一入口常见报错有三类——列不存在Column ... not found、JSON 格式错误pk and ck fields ... must be arrays、整体解析失败Unable to parse targets for index ...分别对应 index/secondary_index.cc、index/secondary_index.cc 与 index/secondary_index.cc。涉及特殊字符列名时检查 target 是否带双引号包裹如keys(m)确认转义逻辑escape_target_column/unescape_target_column未被绕过。以上格式与规则均以当前仓库 docs/dev/secondary_index.md、index/secondary_index.cc、index/target_parser.hh 及 cql3/statements/index_target.cc 的实现为准。【免费下载链接】scylladbNoSQL data store using the Seastar framework, compatible with Apache Cassandra and Amazon DynamoDB项目地址: https://gitcode.com/GitHub_Trending/sc/scylladb创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考