ScyllaDB 二级索引机制详解:global/local 索引的 target 存储格式、默认命名规则与源码解析
2026/9/14 19:56:11 网站建设 项目流程

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.indexesoptions列的每一种取值,并结合 index/secondary_index.cc 与 cql3/statements/index_target.cc 的源码定位索引解析、校验与序列化的具体实现。

一、global 索引与 local 索引:区别在哪里

Scylla 中的二级索引目前可分为两类(见 docs/dev/secondary_index.md):

  • global 索引(默认类型):索引以被索引列作为自己的分区键(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.cc:

bool target_parser::is_local(sstring target_string) { std::optional<rjson::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):

  1. 默认索引名由表名 + 列名 +_idx后缀拼接生成;
  2. 与表名类似,索引名只能包含word characters(字母、数字、下划线),因此构造索引名之前,列名中所有非 word character 的字符会被直接丢弃
  3. 若生成的名字已被占用(例如有人已经手工建了同名的具名索引),则追加_X,其中 X 是保证名称唯一的最小数字;
  4. 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 index)v"v"
冻结集合全量索引(full collection index)FULL(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)分为三步:

  1. 先用上述正则匹配keys(...)/entries(...)/values(...)/full(...)形式,命中则通过index_target::from_sstring()把前缀文本映射为target_type"entries"对应枚举值keys_and_values,见 cql3/statements/index_target.cc),并解析括号内的列名;
  2. 若正则未命中,则尝试把整个字符串当作 JSON 解析——即 local 索引分支;
  3. 两者都不符合时,回退(fallback):把整个字符串视为单一目标列名,类型取regular_values(第 77 行)。

反向序列化由target_parser::serialize_targets()实现(index/secondary_index.cc):当只有单个目标且是单列时,regular_valuesfull两种类型都直接输出转义后的列名(不加前缀),而collection_valueskeyskeys_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.cc:

sstring 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 don't 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 it's 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 索引的 target:JSON 格式的主键定义

local 索引的 target 由显式的分区键 + 被索引列定义组成,且当前要求该分区键必须与基表的分区键一致。其序列化形式是一段表示主键的 JSON 字符串,文档给出的两个示例:

{ "pk": ["p1", "p2", "p3"], "ck": ["v"] }
{ "pk": ["p"], "ck": ["v"] }

其中pk为分区键列数组,ck为被索引的聚类/普通列数组。解析实现位于 index/secondary_index.cc:

std::optional<rjson::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 found"
  • local 索引的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 示例格式完全一致。

五、如何验证与排查

结合本文与仓库源码,实际操作中的验证路径如下:

  1. 查看索引 target 与类型

    DESCRIBE index_name; SELECT * FROM system_schema.indexes;

    options列中的target值可直接对照本文的格式表判断索引是 global 还是 local、属于哪种索引类型。

  2. 确认命名是否符合默认规则:若默认名t_v1_idx已存在,新索引会命名为t_v1_idx_1等带最小唯一后缀的名字;列名中的非 word character 已被丢弃,可用SELECT column_name FROM system_schema.columns ...核对原始列名。

  3. 解析报错时的排查依据:从源码结构看,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。

  4. 涉及特殊字符列名时:检查 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询