找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

4730

积分

0

好友

662

主题
发表于 3 小时前 | 查看: 2| 回复: 0

一个运行在 MySQL 5.7 上的业务系统,其核心查询 SQL 性能突然变得异常缓慢。业务 SQL 如下:

(
SELECT
        concat(A.ID, '') ID,
        date_format(A.ACCEPT_DATE, '%Y-%m-%d') ACCEPT_DATE,
        A.QYID,
        A.APPLY_NO,
        date_format(A.APPLY_DATE, '%Y-%m-%d') APPLY_DATE,
        A.APPLY_TYPE,
        T.TASK_KEY,
        A.BIZ_TYPE,
        T.ID AS TASK_ID,
        T.TASK_NAME,
        '-' SYGZR,
        A.LIC_NO,
        A.HANDLE_TYPE,
        F.JYZMC ENTERPRISE_NAME,
        F.QYLX,
        F.ZCDZ_QX,
        F.JYFS,
        F.ZCDZ_XXDZ,
        A.GMT_MODIFIED
FROM
        T_LIC_APPLY_INFO A,
        t_ypjyjbxx_jc_xzsp F,
        t_proc_ru_task T
WHERE
        A.ID = F.ID
AND A.PROC_INST_ID = T.PROC_INST_ID
AND A.IS_SUBMIT = 1
AND A.IS_DESTROY = 0
AND T.GMT_COMPLETE IS NULL
AND F.ZCDZ_QX LIKE concat('510185', '%')
ORDER BY
        A.APPLY_DATE
)
LIMIT 0,
 50;

起初,通过查看 执行计划 ,问题定位在 t_proc_ru_task 表(以下简称 T 表)进行了大量的行扫描(rows 高达 10万+)。T 表的数据总量超过 80 万行,而条件 GMT_COMPLETE IS NULL 能过滤出大约 5 万多行数据,并且该字段上已经存在索引 idx_t_proc_ru_task_gmt_complete

mysql> SELECT COUNT(*) FROM T_PROC_RU_TASK T  WHERE T.GMT_COMPLETE IS NULL;
+----------+
| COUNT(*) |
+----------+
|    53758 |
+----------+
1 row in set (0.02 sec)

初步判断是 GMT_COMPLETE IS NULL 这个条件的过滤性还不够理想,于是尝试为 T 表添加更精确的时间过滤条件,并创建了一个覆盖这两个时间字段的联合索引,试图从时间维度缩小扫描范围。

 AND T.GMT_CREATE>=DATE_SUB(NOW(), INTERVAL 7 DAY)
 AND T.GMT_CREATE<=now()

 alter table t_proc_ru_task add index idx_gmt_create_complete(gmt_create,gmt_complete);

这次改造效果显著,T 表的扫描行数从 10 万+ 下降到了 1 万+,SQL 性能得到了大幅提升。

性能是变好了,但还能不能再优化呢?观察 SQL,发现关联条件里还有 AND A.PROC_INST_ID = T.PROC_INST_ID。按照理想的索引设计思路,如果为 T 表创建一个包含 PROC_INST_IDGMT_CREATEGMT_COMPLETE 的复合索引,理论上能最大化地利用索引覆盖和过滤条件,达到最优性能。于是,我们新建了这个复合索引,并删除了之前临时添加的联合索引。

mysql> ALTER TABLE T_PROC_RU_TASK ADD INDEX idx_inst_gmt_all (PROC_INST_ID, GMT_CREATE, GMT_COMPLETE);
Query OK, 0 rows affected (22.12 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql>  alter table t_proc_ru_task drop index idx_gmt_create_complete;
Query OK, 0 rows affected (0.03 sec)
Records: 0  Duplicates: 0  Warnings: 0

然而,奇怪的事情发生了:优化器“无视”了我们精心设计的新索引,执行计划依然固执地选择了基于 gmt_complete 的单列索引 idx_t_proc_ru_task_gmt_complete,扫描行数又回到了 10 万+ 的水平。

这就非常令人费解了。从逻辑上讲,这个新创建的复合索引应该是当前查询场景下的最佳选择,但 MySQL 为什么不走呢?

问题的根源往往隐藏在细节之中。带着这个疑问,我们开始检查两张表关联字段 PROC_INST_ID 的定义,结果发现了一个致命的“坑”:

  • T_LIC_APPLY_INFO 表的 PROC_INST_ID 字段是 bigint(20) 类型。
  • T_PROC_RU_TASK 表的 PROC_INST_ID 字段是 varchar(32) 类型。

关联字段的类型不一致!MySQL 中,当不同类型的字段进行比较时(比如 bigintvarchar),会发生隐式类型转换,这通常会导致索引失效。这里就完美解释了为什么优化器拒绝使用新建的、以 PROC_INST_ID 开头的复合索引。

检查表结构确认了这一点:

mysql> SHOW CREATE TABLE T_LIC_APPLY_INFO\G
*************************** 1. row ***************************
       Table: T_LIC_APPLY_INFO
Create Table: CREATE TABLE `t_lic_apply_info` (
  `ID` bigint(20) NOT NULL COMMENT 'ID',
  `PROC_INST_ID` bigint(20) DEFAULT NULL COMMENT '流程实例ID',
  `QYID` varchar(48) COLLATE utf8_bin DEFAULT NULL COMMENT '企业ID',
  ......
  `GMT_MODIFIED` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`ID`),
  KEY `index_qyid` (`QYID`),
  KEY `index_lic_category` (`LIC_CATEGORY`),
  KEY `IDX_PROC_INST_ID` (`PROC_INST_ID`) USING BTREE,
  KEY `index_biz_type` (`BIZ_TYPE`),
  KEY `t_lic_apply_info_APPLY_DATE_IDX` (`APPLY_DATE`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='企业许可业务申请表'
1 row in set (0.00 sec)

mysql> SHOW CREATE TABLE T_PROC_RU_TASK\G
*************************** 1. row ***************************
       Table: T_PROC_RU_TASK
Create Table: CREATE TABLE `t_proc_ru_task` (
  `ID` varchar(32) NOT NULL,
  `PARENT_TASK_ID` varchar(32) DEFAULT NULL,
  `PROC_INST_ID` varchar(32) DEFAULT NULL,
  `PROC_DEFINE_ID` varchar(32) DEFAULT NULL,
  `VER` int(11) DEFAULT NULL,
  `TASK_KEY` varchar(100) DEFAULT NULL,
  `CREATOR` varchar(32) DEFAULT NULL,
  `TASK_NAME` varchar(32) DEFAULT NULL,
  `TASK_TYPE` char(1) DEFAULT NULL,
  `TASK_WAY` char(1) DEFAULT NULL,
  `GMT_CREATE` timestamp NULL DEFAULT NULL,
  `GMT_EXPIRE` timestamp NULL DEFAULT NULL,
  `GMT_COMPLETE` timestamp NULL DEFAULT NULL,
  `VARIABLE` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin,
  `GMT_MODIFIED` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `MODIFY_USER` varchar(32) DEFAULT NULL,
  PRIMARY KEY (`ID`),
  KEY `inst_id` (`PROC_INST_ID`) USING HASH,
  KEY `idx_t_proc_ru_task_TASK_KEY` (`TASK_KEY`),
  KEY `t_proc_ru_task_TASK_KEY_IDX` (`TASK_KEY`,`GMT_COMPLETE`) USING BTREE,
  KEY `idx_t_proc_ru_task_gmt_complete` (`GMT_COMPLETE`),
  KEY `idx_inst_gmt_all` (`PROC_INST_ID`,`GMT_CREATE`,`GMT_COMPLETE`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='流程任务表'
1 row in set (0.00 sec)

在与业务方沟通并核对数据后,确认 T_PROC_RU_TASK 表中的 PROC_INST_ID 字段可以安全地修改为 bigint 类型。修改前,先检查数据以确保没有非数字字符:

SELECT * FROM T_PROC_RU_TASK WHERE PROC_INST_ID REGEXP '[^0-9]';
-- 结果集为0

-- 做好数据备份之后,开始调整类型
ALTER TABLE T_PROC_RU_TASK MODIFY COLUMN PROC_INST_ID bigint(20) DEFAULT NULL;

字段类型统一后,奇迹发生了。再次查看原始 SQL 的执行计划,T 表直接使用了基于 PROC_INST_ID 的索引 inst_id,预估扫描行数骤降至 4 行!

业务 SQL 本身无需任何调整,整个查询的性能从最初的数秒级别,瞬间提升到了毫秒级。

复盘与总结

这个案例给我们敲响了警钟:数据库表设计时,必须严格保持关联字段类型的一致性。一个小小的类型差异(bigint vs varchar),就足以让一个精心设计的复合索引失效,引发严重的性能灾难。

在问题排查初期,我们的思路被局限在了如何提升单个条件的过滤性能上,尝试了添加各种时间索引,虽然有所改善,但始终没有触及根本。直到回过头检查最基础的字段定义,才发现了这个隐蔽的“元凶”。

这个经历也提醒我们,在进行 SQL 性能优化 时,除了关注执行计划、索引覆盖、查询重写等高级技巧,也别忘了检查那些最基础、最容易被忽略的 schema 设计细节。很多时候,解决问题的钥匙就藏在最基础的地方。




上一篇:腾讯游戏数据治理实践:AI如何驱动资源治理与协作提效
下一篇:从罗技“狗来”营销聊起,看 LeetCode 851 拓扑排序应用
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-3-28 08:36 , Processed in 0.586197 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表