前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >带你读 MySQL 源码:limit, offset

带你读 MySQL 源码:limit, offset

作者头像
csch
发布2023-05-24 10:18:27
8400
发布2023-05-24 10:18:27
举报
文章被收录于专栏:一树一溪一树一溪

我一直想写 MySQL 源码分析文章,希望能够达成 2 个目标:

  • 不想研究源码的朋友,可以通过文章了解 MySQL 常用功能的实现逻辑,做到知其然,也知其所以然。
  • 想研究源码的朋友,能够以文章为切入点,迈进 MySQL 源码研究之门。

目标是明确的,任务是艰巨的。

MySQL 源码数量庞大,各种功能的代码盘根错节,相互交织在一起,形成一张复杂的网。

想要把这张网中的某些部分拎出来写成文章,还要做到通俗易懂,这并不是件容易的事,我也就迟迟没有动手。

万事开头难,但是再难,总得开始,才能有后续,所以,就有了这篇文章。

写文章是件费时费力的事,写出来了总希望有更多人看,否则就没有写下去的动力了。

对 MySQL 源码感兴趣的朋友们,如果想看到源码分析系列的更多文章,请帮忙把文章传播出去,分享给更多人。

唠叨完前因后果,再说说我准备怎么写这个系列文章:

  • 我会挑一些常用功能,每篇文章介绍一个单点功能的源码,从简单功能开始,逐渐过渡到复杂功能。
  • 每篇文章只会介绍核心源码逻辑,源码之中增加注释,源码之外尽可能用文字展开介绍源码逻辑,以帮助大家更好的理解源码。
  • 每篇文章不会太长,如果功能复杂导致内容太长,我会拆分文章,尽量降低大家的阅读负担。

接下来,我们开始源码分析系列的第 1 篇文章。

本文内容基于 MySQL 8.0.32 源码。

正文

1. 准备工作

创建测试表:

代码语言:javascript
复制
CREATE TABLE `t1` (
  `id` int unsigned NOT NULL AUTO_INCREMENT,
  `str1` varchar(255) NOT NULL DEFAULT '',
  `i1` int NOT NULL DEFAULT '0',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;

插入测试数据:

代码语言:javascript
复制
INSERT INTO t1(id, str1, i1) VALUES
(1, 's1', 10),
(2, 's2', 20),
(3, 's3', 30),
(4, 's4', 40),
(5, 's5', 50),
(6, 's6', 60),
(7, 's7', 70),
(8, 's8', 80);

示例 SQL:

代码语言:javascript
复制
select * from t1 limit 5, 2

2. 整体介绍

我们先通过 explain 来看一下执行计划:

从 explain 输出可以看到,执行计划比较简单,SQL 执行过程包含 2 个迭代器:

  • Limit/Offset,对应 LimitOffsetIterator 迭代器。
  • Table scan,对应 TableScanIterator 迭代器。

代码执行时堆栈如下:

代码语言:javascript
复制
| > handle_connection(void*) sql/conn_handler/connection_handler_per_thread.cc:302
| + > do_command(THD*) sql/sql_parse.cc:1439
| + - > dispatch_command(...) sql/sql_parse.cc:2036
| + - x > dispatch_sql_command(THD*, Parser_state*) sql/sql_parse.cc:5322
| + - x = > mysql_execute_command(THD*, bool) sql/sql_parse.cc:4688
| + - x = | > Sql_cmd_dml::execute(THD*) sql/sql_select.cc:578
| + - x = | + > Sql_cmd_dml::execute_inner(THD*) sql/sql_select.cc:778
| + - x = | + - > Query_expression::execute(THD*) sql/sql_union.cc:1823
| + - x = | + - x > // 查询入口
| + - x = | + - x > Query_expression::ExecuteIteratorQuery(THD*) sql/sql_union.cc:1770
| + - x = | + - x = > // 实现 limit, offset
| + - x = | + - x = > LimitOffsetIterator::Read() sql/iterators/composite_iterators.cc:128
| + - x = | + - x = | > // 从存储引擎读取一条记录
| + - x = | + - x = | > TableScanIterator::Read() sql/iterators/basic_row_iterators.cc:218

3. 源码分析

TableScanIterator 迭代器用于从存储引擎读取记录,留到以后的文章介绍。

limit, offset 由 LimitOffsetIterator 迭代器实现,我们会介绍两个方法的代码:

  • Query_expression::ExecuteIteratorQuery(THD*),这是查询入口方法,介绍了它,流程才算完整。
  • LimitOffsetIterator::Read(),limit, offset 的逻辑都在这个方法里实现。

3.1 ExecuteIteratorQuery()

代码语言:javascript
复制
// sql/sql_union.cc
bool Query_expression::ExecuteIteratorQuery(THD *thd) {
  ...
  {
    ...
    for (;;) {
      // 从存储引擎读取一条记录
      int error = m_root_iterator->Read();
      DBUG_EXECUTE_IF("bug13822652_1", thd->killed = THD::KILL_QUERY;);

      // 读取出错,直接返回
      if (error > 0 || thd->is_error())  // Fatal error
        return true;
      // error < 0
      // 表示已经读完了所有符合条件的记录
      // 查询结束
      else if (error < 0)
        break;
      // SQL 被客户端干掉了
      else if (thd->killed)  // Aborted by user
      {
        thd->send_kill_message();
        return true;
      }
      ...
      // 发送数据给客户端
      if (query_result->send_data(thd, *fields)) {
        return true;
      }
      ...
    }
  }
  ...
}

从以上代码可以看到,select 查询入口方法的主体是一个无限 for 循环。

每一轮循环都会调用 m_root_iterator->Read() 方法从存储引擎读取一条记录。

对于示例 SQL 来说,m_root_iterator->Read() 就是 LimitOffsetIterator::Read()。

for 循环会一直执行,直到 m_root_iterator->Read() 的返回值命中以下任意一个条件才会结束:

  • if (error > 0 || thd->is_error()),读取出错了,以错误状态结束查询。
  • if (error < 0),已经读完所有符合条件的记录,以正常状态结束查询。
  • if (thd->killed),SQL 被客户端通过 kill <query_id> 干掉了,中止查询。 <query_id>show processlist 中的 Id 字段。

for 循环中,每次从存储引擎读取到一条记录,都会调用 query_result->send_data(thd, *fields) 方法。

对于示例 SQL 来说,这个方法的行为就是把记录发送给客户端。

3.2 LimitOffsetIterator::Read()

代码语言:javascript
复制
// sql/iterators/composite_iterators.cc
int LimitOffsetIterator::Read() {
  // 这个 if 括号里的条件理解起来会有点困难
  // 所以被省略了,眼不见为净
  //【重点】只有读取第一条和最后一条记录时才会进入这个 if 分支
  if (...) {
    ...
    // m_needs_offset = true
    // 表示 SQL 语句中指定了 offset
    if (m_needs_offset) {
      ...
      // 循环从存储引擎读取 m_offset 条记录
      // 每读取到一条记录,直接丢弃
      for (ha_rows row_idx = 0; row_idx < m_offset; ++row_idx) {
        // 读取一条记录之后
        // 如果没有出错,就接着读取下一条记录
        int err = m_source->Read();
        // 读取出错,直接返回错误码
        if (err != 0) {
          return err;
        }
        ...
      }
      // 读取 m_offset 条记录并丢弃之后
      // 把 m_seen_rows 设置为已读取记录数 
      m_seen_rows = m_offset;
      // 然后把 m_needs_offset 设置为 false
      // 表示不需要再处理 offset 逻辑了(因为已处理完成)
      // 下次读取时也就不需要再跳过 m_offset 条记录了
      m_needs_offset = false;
      ...
    }
    // 如果已经读取了 m_limit 条记录
    // 就返回 -1,表示读取结束
    // m_limit = SQL 中的 limit + offset
    if (m_seen_rows >= m_limit) {
      ...
      return -1;
    }
  }

  // 读取需要返回给客户端的记录
  const int result = m_source->Read();
  ...
  // 已读取记录数加 1
  ++m_seen_rows;
  // 返回当前读取的记录
  // 给 Query_expression::ExecuteIteratorQuery() 方法
  return result;
}

除了处理 offset 逻辑之外,LimitOffsetIterator::Read() 每次只读取一条记录,这个方法的核心逻辑分为三部分:

第 1 部分if (m_needs_offset),SQL 语句中指定了 offset,返回第一条记录给客户端之前,需要读取 offset 条记录并丢弃,从第 offset + 1 条记录开始返回给客户端。

这部分的主要逻辑是一个 for 循环,会循环 offset 次,每次读取一条记录。

如果读取成功,就接着读取下一条记录,而不会对这条记录做任何操作,也就相当于丢弃了。

如果读取失败,直接返回错误码,读取结束,客户端会收到报错信息。

第 2 部分if (m_seen_rows >= m_limit),表示已经读取了 m_limit 条记录,返回 -1 表示读取正常结束。

m_limit = SQL 中的 limit + offset。

第 3 部分result = m_source->Read() 从存储引擎读取一条记录,然后,把结果返回给 Query_expression::ExecuteIteratorQuery() 方法。

4. 总结

limit, offset 逻辑比较简单,全部由 LimitOffsetIterator::Read() 实现,核心逻辑总结如下:

  • 从存储引擎读取返回给客户端的第 1 条记录之前,会先读取 offset 条记录并丢弃,然后再读取一条记录,用于返回给客户端。
  • 从存储引擎读取第 2 ~ limit + offset 条记录时,每读取一条记录,都返回给 Query_expression::ExecuteIteratorQuery(),由该方法把记录返回给客户端。
  • 读取 limit + offset 条记录之后,返回 -1 表示读取流程正常结束。

从 LimitOffsetIterator::Read() 的实现逻辑来看,offset 越大,读取之后被丢弃的记录就越多,读取这些记录所做的都是无用功。

为了提高 SQL 的执行效率,可以通过改写 SQL 让 offset 尽可能小,理想状态是 offset = 0。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2023-04-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 一树一溪 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 准备工作
  • 2. 整体介绍
  • 3. 源码分析
    • 3.1 ExecuteIteratorQuery()
      • 3.2 LimitOffsetIterator::Read()
      • 4. 总结
      相关产品与服务
      云数据库 MySQL
      腾讯云数据库 MySQL(TencentDB for MySQL)为用户提供安全可靠,性能卓越、易于维护的企业级云数据库服务。其具备6大企业级特性,包括企业级定制内核、企业级高可用、企业级高可靠、企业级安全、企业级扩展以及企业级智能运维。通过使用腾讯云数据库 MySQL,可实现分钟级别的数据库部署、弹性扩展以及全自动化的运维管理,不仅经济实惠,而且稳定可靠,易于运维。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档