• MySQL 派生表查询导致 Crash 的根源分析与解决方案

MySQL 派生表查询导致 Crash 的根源分析与解决方案

2025-05-26 04:00:04 栏目:宝塔面板 19 阅读

一、问题发现

在之前的 MySQL 8.0.32 使用中,发现使用以下带有派生表的 SQL 会导致 MySQL Crash,以下的sequence_table(2)替换为任何非常量表都行:

仅 MySQL 8.0.32 版本有影响。

EXPLAIN FORMAT=TREE 
select
    trim(ref_15.c_ogj),
    0<>0as c_lrcm63eani
from
    (select
    0<>0as c_ogj
from
   sequence_table(2) t1
where0<>0
orderby c_ogj asc) as ref_15;

Crash 的堆栈如下:

Thread 55 "mysqld" received signal SIGSEGV, Segmentation fault.
Item_view_ref::used_tables (this=0x7fff2418f410)
    at sql/item.h:6670
6670            table_map inner_map = ref_item()->used_tables(); ==> ref_item()为空指针,因此crash了
(gdb) bt
#0  Item_view_ref::used_tables (this=0x7fff2418f410)
    at sql/item.h:6670
#1  0x0000555558e978d1 in Item::const_item (this=0x7fff2418f410)
    at sql/item.h:2342
#2  0x0000555558ecc765 in Item_ref::print (this=0x7fff2418f410, thd=0x7fff24001050, 
    str=0x7fffc83ee7e0, query_type=(QT_TO_SYSTEM_CHARSET | QT_SHOW_SELECT_NUMBER))
    at sql/item.cc:9993
#3  0x000055555903b839 in Item_func_trim::print (this=0x7fff24120d20, thd=0x7fff24001050, 
    str=0x7fffc83ee7e0, query_type=(QT_TO_SYSTEM_CHARSET | QT_SHOW_SELECT_NUMBER))
    at sql/item_strfunc.cc:3244
#4  0x0000555558ea7fc5 in Item::print_item_w_name (this=0x7fff24120d20, thd=0x7fff24001050, 
    str=0x7fffc83ee7e0, query_type=(QT_TO_SYSTEM_CHARSET | QT_SHOW_SELECT_NUMBER))
    at sql/item.cc:727
#5  0x00005555593f18c0 in Query_block::print_item_list (this=0x7fff24120768, thd=0x7fff24001050, 
    str=0x7fffc83ee7e0, query_type=(QT_TO_SYSTEM_CHARSET | QT_SHOW_SELECT_NUMBER))
    at sql/sql_lex.cc:4041
#6  0x00005555593efb50 in Query_block::print_query_block (this=0x7fff24120768, 
    thd=0x7fff24001050, str=0x7fffc83ee7e0, 
    query_type=(QT_TO_SYSTEM_CHARSET | QT_SHOW_SELECT_NUMBER))
    at sql/sql_lex.cc:3614
#7  0x00005555593efa3d in Query_block::print (this=0x7fff24120768, thd=0x7fff24001050, 
    str=0x7fffc83ee7e0, query_type=(QT_TO_SYSTEM_CHARSET | QT_SHOW_SELECT_NUMBER))
    at sql/sql_lex.cc:3598
#8  0x00005555593ee556 in Query_expression::print (this=0x7fff24120670, thd=0x7fff24001050, 
    str=0x7fffc83ee7e0, query_type=(QT_TO_SYSTEM_CHARSET | QT_SHOW_SELECT_NUMBER))
    at sql/sql_lex.cc:3232
#9  0x0000555559a89c2c in print_query_for_explain (query_thd=0x7fff24001050, 
    unit=0x7fff24120670, str=0x7fffc83ee7e0)
    at sql/opt_explain.cc:2288
#10 0x0000555559a10b11 in PrintQueryPlan[abi:cxx11](THD*, THD const*, Query_expression*) (
    ethd=0x7fff24001050, query_thd=0x7fff24001050, unit=0x7fff24120670)
    at sql/join_optimizer/explain_access_path.cc:1894
#11 0x0000555559a8985a in ExplainIterator (ethd=0x7fff24001050, query_thd=0x7fff24001050, 
    unit=0x7fff24120670) at sql/opt_explain.cc:2205
#12 0x0000555559a89e91 in explain_query (explain_thd=0x7fff24001050, query_thd=0x7fff24001050, 
    unit=0x7fff24120670) at sql/opt_explain.cc:2359
#13 0x000055555955cd46 in Sql_cmd_dml::execute_inner (this=0x7fff24165630, thd=0x7fff24001050)

二、问题调查过程

调查执行 SQL 的 optimize 的过程,分析发现该 SQL 的 SQL 变换情况如下:

以下的 trim(ref_15.c_ogj) 执行完 find_order_in_list 后,Item_func_trim的args[0]->m_ref_item[0] 等于0<>0 as c_lrcm63eani,而不是0<>0 as c_ogj,这是因为c_lrcm63eani和c_ogj的名字都一样,都是0<>0,在find_order_in_list函数里面由于名字一样因此内层字段被外层替代了。而后在Item::clean_up_after_removal执行的时候,Item_func_ne即c_lrcm63eani因为出现了2次,因此执行了2次decrement_ref_count(),然而在Query_block::delete_unused_merged_columns函数却把0<>0 as c_lrcm63eani的Item置为空了,因为这个时候c_lrcm63eani的item->decrement_ref_count()以后ref_count()为0因此继续执行Item::clean_up_after_removal了。

EXPLAIN FORMAT=TREE 
select
    trim(ref_15.c_ogj),
    0<>0as c_lrcm63eani
from
    (select
    0<>0as c_ogj
from
   sequence_table(2) t1
where0<>0
orderby c_ogj asc) as ref_15;

查看函数调用过程发现 Query_block 在 prepare 的时候执行了 delete_unused_merged_columns,

-- 函数调用过程: Query_block::prepare -> Query_block::apply_local_transforms -> Query_block::delete_unused_merged_columns

bool find_order_in_list() {
  if (select_item != not_found_item) {
      if ((*order->item)->real_item() != (*select_item)->real_item()) {
        Item::Cleanup_after_removal_context ctx(
            thd->lex->current_query_block());

        (*order->item)
            ->walk(&Item::clean_up_after_removal, walk_options,  ==>Item_func_ne执行了2次,也执行了2次decrement_ref_count()
                   pointer_cast(&ctx));
      }
  }
}

bool Query_block::apply_local_transforms(THD *thd, bool prune) {
  DBUG_TRACE;

  assert(first_execution);
-- 这个函数把((Item_func *)&fields[0][0])->args[0]->m_ref_item[0]给删了
  if (derived_table_count) delete_unused_merged_columns(&m_table_nest);
}

void Query_block::delete_unused_merged_columns(
    mem_root_deque *tables) {
  DBUG_TRACE;

  for (Table_ref *tl : *tables) {
    if (tl->nested_join == nullptr) continue;
    if (tl->is_merged()) {
      for (Field_translator *transl = tl->field_translation;
           transl < tl->field_translation_end; transl++) {
        Item *const item = transl->item;
        // Decrement the ref count as its no more used in
        // select list.
        if (item->decrement_ref_count()) continue; -- 因为执行完decrement_ref_count()以后返回的m_ref_count=0因此不会跳出这个循环

        // Cleanup the item since its not referenced from
        // anywhere.
        assert(item->fixed);
        Item::Cleanup_after_removal_context ctx(this);
        item->walk(&Item::clean_up_after_removal, walk_options,
                   pointer_cast(&ctx));
        transl->item = nullptr; -- 这个地方把Item_view_ref引用的Item_func_ne对象置为空了,即把trim函数参数的c_lrcm63eani列删除了
      }
    }
    delete_unused_merged_columns(&tl->nested_join->m_tables);
  }
}

三、解决方案

通过上面的分析,我们可以发现问题在于多执行了一次Item::clean_up_after_removal,随后在 MySQL 最新代码尝试执行以上 SQL 发现该 BUG 已经被修复,找到相关修复代码,可以发现以下修复代码。

相关commit ID号为: 2171a1260e2cdbbd379646be8ff6413a92fd48f4

-- 相关修复代码如下:
@@ -7575,7 +7865,6 @@ bool Item::clean_up_after_removal(uchar *arg) {
 
   if (reference_count() > 1) {
     (void)decrement_ref_count();
+    ctx->stop_at(this);
   }
   return false;
 }

修改完查看一下这个函数的堆栈信息:

#0  Item::clean_up_after_removal (this=0x2, 
    arg=0x41 )
    at sql/item.cc:9236
#1  0x0000555558fea5a8 in Item::walk (this=0x7fff2c338db8, processor=&virtual table offset 864, 
    walk=7, arg=0x7fffc83ee4b0 "") at sql/item.h:2543
#2  0x00005555596cc6f2 in find_order_in_list(thd=0x7fff2c001070, ref_item_array=..., 
    tables=0x7fff2c330b90, order=0x7fff2c32eae8, fields=0x7fff2c32fb20, is_group_field=false, 
    is_window_order=false) at sql/sql_resolver.cc:4625
#3  0x00005555596cd0ae in setup_order(thd=0x7fff2c001070, ref_item_array=..., 
    tables=0x7fff2c330b90, fields=0x7fff2c32fb20, order=0x7fff2c32eae8)
    at sql/sql_resolver.cc:4811
#4  0x00005555596bf528 in Query_block::prepare(this=0x7fff2c32fae0, thd=0x7fff2c001070, 
    insert_field_list=0x0) at sql/sql_resolver.cc:400
#5  0x00005555597d035d in Query_expression::prepare(this=0x7fff2c32f9e8, thd=0x7fff2c001070, 
    sel_result=0x7fff2c33b2a8, insert_field_list=0x0, added_options=0, removed_options=0)
    at sql/sql_union.cc:758
#6  0x0000555559590772 in Table_ref::resolve_derived(this=0x7fff2c339790, thd=0x7fff2c001070, 
    apply_semijoin=true) at sql/sql_derived.cc:451
#7  0x00005555596c2a80 in Query_block::resolve_placeholder_tables(this=0x7fff2c333f08, 
    thd=0x7fff2c001070, apply_semijoin=true)
    at sql/sql_resolver.cc:1408
#8  0x00005555596bea62 in Query_block::prepare(this=0x7fff2c333f08, thd=0x7fff2c001070, 
    insert_field_list=0x0) at sql/sql_resolver.cc:265

对于0<>0 as c_lrcm63eani这个Item_func_ne对象,执行到Item::clean_up_after_removal的时候,因为reference_count() > 1因此会执行新添加的ctx->stop_at(this),等到下一次再执行到这个Item_func_ne的clean_up_after_removal()函数的时候,就会因为ctx->is_stopped(this)而直接返回,不再执行一次decrement_ref_count(),从而避免了执行后面的transl->item = nullptr。

bool find_order_in_list() {
if (select_item != not_found_item) {
      if ((*order->item)->real_item() != (*select_item)->real_item()) {
        Item::Cleanup_after_removal_context ctx(
            thd->lex->current_query_block());

        (*order->item)
            ->walk(&Item::clean_up_after_removal, walk_options,  -- Item_func_ne执行了2次,而只执行了一次decrement_ref_count()
                   pointer_cast(&ctx));
      }
  }
}

void Query_block::delete_unused_merged_columns(
    mem_root_deque *tables) {
  DBUG_TRACE;

for (Table_ref *tl : *tables) {
    if (tl->nested_join == nullptr) continue;
    if (tl->is_merged()) {
      for (Field_translator *transl = tl->field_translation;
           transl < tl->field_translation_end; transl++) {
        Item *const item = transl->item;
        // Decrement the ref count as its no more used in
        // select list.
        if (item->decrement_ref_count()) continue; 因为执行完decrement_ref_count()以后返回的m_ref_count=1因此不会继续执行后面的置空设置

        // Cleanup the item since its not referenced from
        // anywhere.
        assert(item->fixed);
        Item::Cleanup_after_removal_context ctx(this);
        item->walk(&Item::clean_up_after_removal, walk_options,
                   pointer_cast(&ctx));
        transl->item = nullptr; ==>这个地方不会运行到
      }
    }
    delete_unused_merged_columns(&tl->nested_join->m_tables);
  }
}

四、问题总结

通过以上分析我们可以发现,对于复杂的 SQL 会执行复杂的 Item 变换和删除不需要的 Item,但是正是由于这样才更容易导致 Crash 的出现。分析类似这样的 Crash 问题的时候,因为涉及代码量大,代码逻辑复杂往往很难找到相关修复代码,因此需要对代码运行流程比较熟悉,同时要有相关复杂问题解决的经验才能更好的应对这类问题。

本文地址:https://www.yitenyun.com/233.html

搜索文章

Tags

数据库 API FastAPI Calcite 电商系统 MySQL 数据同步 ACK 双主架构 循环复制 Web 应用 异步数据库 序列 核心机制 生命周期 Deepseek 宝塔面板 Linux宝塔 Docker JumpServer JumpServer安装 堡垒机安装 Linux安装JumpServer esxi esxi6 root密码不对 无法登录 web无法登录 Windows Windows server net3.5 .NET 安装出错 宝塔面板打不开 宝塔面板无法访问 SSL 堡垒机 跳板机 HTTPS Windows宝塔 Mysql重置密码 无法访问宝塔面板 HTTPS加密 查看硬件 Linux查看硬件 Linux查看CPU Linux查看内存 ES 协同 修改DNS Centos7如何修改DNS scp Linux的scp怎么用 scp上传 scp下载 scp命令 防火墙 服务器 黑客 Serverless 无服务器 语言 存储 Spring SQL 动态查询 Oracle 处理机制 Linux 安全 网络架构 工具 网络配置 Canal 加密 场景 MySQL 9.3 开源 PostgreSQL 存储引擎 RocketMQ 长轮询 配置 Rsync 架构 InnoDB 缓存方案 缓存架构 缓存穿透 HexHub 信息化 智能运维 响应模型 日志文件 MIXED 3 线上 库存 预扣 索引 数据 业务 监控 聚簇 非聚簇 B+Tree ID 字段 AI 助手 数据库锁 单点故障 分库 分表 云原生 GreatSQL Hash 字段 DBMS 管理系统 Redis 自定义序列化 SpringAI 优化 万能公式 Redis 8.0 ​Redis 机器学习 推荐模型 SVM Embedding PostGIS 系统 openHalo OB 单机版 数据集成工具 SQLite Redka sqlmock SQLark 虚拟服务器 虚拟机 内存 分页查询 Netstat Linux 服务器 端口 SQLite-Web 数据库管理工具 自动重启 运维 同城 双活 缓存 sftp 服务器 参数 共享锁 • 索引 • 数据库 RDB AOF 排行榜 排序 Testcloud 云端自动化 查询 prometheus Alert 向量数据库 大模型 不宕机 容器化 分布式架构 分布式锁​ 聚簇索引 非聚簇索引 OAuth2 Token Entity 开发 技术 EasyExcel MySQL8 IT 数据备份 Postgres OTel Iceberg MongoDB 容器 数据类型 StarRocks 数据仓库 Doris SeaTunnel 人工智能 推荐系统 分页 数据结构 连接控制 机制 AIOPS Python Web LRU Milvus IT运维 Caffeine CP 部署 崖山 新版本 高可用 向量库 悲观锁 乐观锁 Ftp 池化技术 连接池 磁盘架构 MCP mini-redis INCR指令 redo log 重做日志 单线程 线程 MVCC 事务隔离 流量 开放协议 Web 接口 字典 电商 速度 服务器中毒 数据脱敏 加密算法 R2DBC QPS 高并发 原子性 对象 微软 SQL Server AI功能 窗口 函数 双引擎 RAG HelixDB 频繁 Codis Crash 代码 主库 Order SSH 引擎 性能 网络 dbt 数据转换工具 1 PG DBA ZODB List 类型 Pottery 工具链 优化器 InfluxDB 模型 意向锁 记录锁 网络故障 传统数据库 向量化 发件箱模式 事务同步 UUIDv7 主键 仪表盘 Redisson 锁芯 LLM 线程安全 INSERT COMPACT Undo Log 连接数 订单 JOIN