【GreatSQL优化器-16】INDEX_SKIP_SCAN

【GreatSQL优化器-16】INDEX_SKIP_SCAN

一、INDEX_SKIP_SCAN介绍

GreatSQL 优化器的索引跳跃扫描(Index Skip Scan) 是一种优化查询的技术,尤其在联合索引中用于减少扫描的无效行数。它通过"跳跃"式的扫描方式,避免了对索引中无用部分的扫描,从而提升查询效率。这种技术适合特定场景,并有一定的优缺点。

索引跳跃扫描利用的是联合索引中非首列(非最左前缀)的索引列,来提高查询效率。例如,如果你有一个复合索引 (A, B),在传统的 B-Tree 索引中,只有当查询条件包含 A 列时,索引才会生效。但在跳跃扫描中,即使 A 没有出现在查询条件中,仍然可以通过扫描 B 列来有效查询。跳跃扫描会逐步扫描 A 列的每一个可能值,然后在每个 A 值下查找 B 列中符合条件的记录。这样避免了扫描大量无关记录,提升了查询性能。

下面用一个简单的例子来说明INDEX_SKIP_SCAN怎么应用在优化器。

CREATE TABLE t1(c1 INT, c2 INT, c3 INT, c4 INT);
CREATE UNIQUE INDEX i1_t1 ON t1(c1,c2,c3);
INSERT INTO t1 VALUES (1,1,1,1), (1,1,2,2), (1,3,3,3), (1,4,4,4), (1,5,5,5),
                      (2,1,1,1), (2,2,2,2), (2,3,3,3), (2,4,4,4), (2,5,5,5);
INSERT INTO t1 SELECT c1, c2, c3+5, c4+10  FROM t1;
INSERT INTO t1 SELECT c1, c2, c3+10, c4+20 FROM t1;
INSERT INTO t1 SELECT c1, c2, c3+20, c4+40 FROM t1;
INSERT INTO t1 SELECT c1, c2, c3+40, c4+80 FROM t1;
ANALYZE TABLE t1;

-- 下面的结果用到了skip scan
greatsql> EXPLAIN SELECT c1, c2 FROM t1 WHERE c2 > 40;
+----+-------------+-------+------------+-------+---------------+-------+---------+------+------+----------+----------------------------------------+
| id | select_type | table | partitions | type  | possible_keys | key   | key_len | ref  | rows | filtered | Extra                                  |
+----+-------------+-------+------------+-------+---------------+-------+---------+------+------+----------+----------------------------------------+
|  1 | SIMPLE      | t1    | NULL       | range | i1_t1         | i1_t1 | 10      | NULL |   53 |   100.00 | Using where; Using index for skip scan |
+----+-------------+-------+------------+-------+---------------+-------+---------+------+------+----------+----------------------------------------+

/*运行过程:
1、先找到第一列的不同值,得到结果为1和2
select distinct c1 from t1; 
2、根据第一列的分组结果在分组内执行范围扫描
select c1, c2 from t1 where c1 = 1 and c2 > 40;
select c1, c2 from t1 where c1 = 2 and c2 > 40;*/
二、get_best_skip_scan代码解释

skip scan 是根据条件生成 mm tree 以后,根据 mm tree 结果和索引进行判定能不能用 skip scan,其中联合索引中范围条件列之前的列为prefix列,即用来作为分组的依据的列,如果有多个除了第一列之外的范围列,那么选取遇到的第一个列之前的列为前缀进行分组。

int test_quick_select() {
  // 先判断skip_scan是否满足条件,不满足的话执行索引合并。
  AccessPath *skip_scan_path = get_best_skip_scan();
  // 不满足skip_scan执行索引合并,相关知识看上一节介绍
}

AccessPath *get_best_skip_scan() {
  // 先判断sql是否支持skip_scan,见表一
  // 接着开始执行skip_scan,遍历所有索引
  for (uint cur_param_idx = 0; cur_param_idx < param->keys; ++cur_param_idx) {
    // covering_keys代表可以使用的联合索引,sql涉及的所有列必须都在联合索引上才会有值
    if (!table->covering_keys.is_set(cur_index)) {
      cause = "query_references_nonkey_column";
      goto next_index;
    }
    // 从mm tree抽出当前联合索引相关的SEL_ROOT,如果没有那么就不能执行skip scan
    cur_index_range_tree = get_index_range_tree(cur_index, tree, param);
    // 遍历当前索引涉及的所有列
    for (cur_part = cur_index_info->key_part; cur_part != end_part;
         cur_part++, part++) {
      // 从mm tree获取当前列相关的SEL_ROOT,就是查询条件涉及的列
      get_sel_root_for_keypart();
      1、如果找到联合索引前缀列相关条件并且不是等号条件,那么就判断为"prefix_not_const_equality"并且跳到下一个索引
      2、如果找到联合索引前缀列以外的列相关条件,以遇到的第一个条件作为skip scan的条件。
    }
    // 针对联合索引的前缀列估计范围内包含的行数,用在条件包含前缀列有等号条件的情况
    quick_prefix_records = check_quick_select();
    // 计算skip_scan的行数和cost
    cost_skip_scan();
    // 最后生成AccessPath,包含IndexSkipScanParameters
  }
}
// 获取skip scan的行数和cost结果
void cost_skip_scan() {
  table_records = table->file->stats.records;
  // 如果可以根据前缀列估算出行数,那么就用估算的值,如果不能就用全表的行数。
  if (quick_prefix_records ==
这段代码是 OSDP 协议中**接收数据包的核心逻辑**,其主要功能是: - 如果还没有解析出完整的数据包长度,则先解析包头; - 然后从接收缓冲区中读取完整的数据包; - 最后校验整个数据包的完整性与格式; - 如果成功,返回校验结果;否则等待更多数据。 --- ### 🔍 代码逐行解析: ```c if (pd->packet_len == 0) { ``` - 判断当前是否还没有解析出数据包的完整长度; ```c ret = phy_check_header(pd); if (ret < 0) { return ret; } pd->packet_len = ret; ``` - 调用 `phy_check_header()` 函数解析包头并返回数据包总长度; - 如果返回负值,说明解析失败,直接返回错误码; - 否则将返回值赋给 `packet_len`,表示整个数据包的长度; ```c if (pd->packet_scan_skip) { LOG_DBG("Packet scan skipped:%u mark:%d", pd->packet_scan_skip, ISSET_FLAG(pd, PD_FLAG_PKT_HAS_MARK)); pd->packet_scan_skip = 0; } ``` - 如果有跳过的字节(非 SOM 或 MARK),打印调试信息并清零计数器; ```c ret = osdp_rb_pop_buf(&pd->rx_rb, pd->packet_buf + pd->packet_buf_len, pd->packet_len - pd->packet_buf_len); ``` - 从环形缓冲区中读取数据到 `packet_buf`; - 读取长度为 `packet_len - packet_buf_len`,即剩余需要读取的字节数; ```c pd->packet_buf_len += ret; ``` - 更新已读取的数据长度; ```c if (pd->packet_buf_len != pd->packet_len) { return OSDP_ERR_PKT_WAIT; } ``` - 如果还没有读取完整个数据包,返回 `OSDP_ERR_PKT_WAIT`,等待下一次读取; ```c if (is_packet_trace_enabled(pd)) { osdp_capture_packet(pd, pd->packet_buf, pd->packet_buf_len); } ``` - 如果启用了包捕获功能,调用 `osdp_capture_packet()` 保存当前数据包; ```c return phy_check_packet(pd, pd->packet_buf, pd->packet_len); ``` - 最后调用 `phy_check_packet()` 校验整个数据包的完整性(如校验和、数据格式等); - 返回校验结果; --- ### ✅ 总结逻辑流程: | 阶段 | 动作 | |------|------| | 1 | 如果 `packet_len == 0`,说明还没有解析包头,调用 `phy_check_header()` 解析头部并获取包长度 | | 2 | 从环形缓冲区中读取数据,填充到 `packet_buf` 中 | | 3 | 如果未读取完整数据包,返回 `OSDP_ERR_PKT_WAIT`,等待更多数据 | | 4 | 如果启用了包捕获功能,保存当前数据包用于调试或分析 | | 5 | 调用 `phy_check_packet()` 对数据包进行最终校验,并返回结果 | --- ### 🧠 常见错误码含义: | 错误码 | 含义 | |--------|------| | `OSDP_ERR_PKT_WAIT` | 数据包不完整,需等待更多数据 | | `OSDP_ERR_PKT_FMT` | 包格式错误 | | `OSDP_ERR_PKT_CRC` | CRC 校验失败 | | `OSDP_ERR_PKT_LEN` | 包长度不合法 | --- ###
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值