0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

MySQL 5.7的数据库优化器还是这么简单?

OSC开源社区 来源:爱可生开源社区 2023-09-22 09:35 次阅读

1 问题现象

自发布了 INSERT 并发死锁问题的文章,收到了多次死锁问题的交流。一个具体案例如下:

研发反馈应用发生死锁,收集如下诊断内容:

------------------------
LATESTDETECTEDDEADLOCK
------------------------
2023-07-0406400x7fc07dd0e700
***(1)TRANSACTION:
TRANSACTION182396268,ACTIVE0secfetchingrows
mysqltablesinuse1,locked1
LOCKWAIT21lockstruct(s),heapsize3520,2rowlock(s),undologentries1
MySQLthreadid59269692,OSthreadhandle140471135803136,queryid3738514953192.168.0.215user1updating
deletefromltb2wherec='CCRSFD07E'andj='Y15'andb>='20230717'andd!='1'ande!='1'
***(1)WAITINGFORTHISLOCKTOBEGRANTED:
RECORDLOCKSspaceid603pageno86nbits248indexPRIMARYoftable`testdb`.`ltb2`trxid182396268lock_modeXlocksrecbutnotgapwaiting
***(2)TRANSACTION:
TRANSACTION182396266,ACTIVE0secfetchingrows,threaddeclaredinsideInnoDB1729
mysqltablesinuse1,locked1
28lockstruct(s),heapsize3520,2rowlock(s),undologentries1
MySQLthreadid59261188,OSthreadhandle140464721291008,queryid3738514964192.168.0.214user1updating
updateltb2setf='0',g='0',is_value_date='0',h='0',i='0'wherec='22115001B'andj='Y4'andb>='20230717'
***(2)HOLDSTHELOCK(S):
RECORDLOCKSspaceid603pageno86nbits248indexPRIMARYoftable`testdb`.`ltb2`trxid182396266lock_modeXlocksrecbutnotgap
***(2)WAITINGFORTHISLOCKTOBEGRANTED:
RECORDLOCKSspaceid603pageno86nbits248indexPRIMARYoftable`testdb`.`ltb2`trxid182396266lock_modeXlocksrecbutnotgapwaiting
***WEROLLBACKTRANSACTION(1)
------------

以上 space id 603 page no 86 n bits 248,其中 space id 表示表空间 ID,page no 表示记录锁在表空间内的哪一页,n bits 是锁位图中的位数,而不是页面偏移量。记录的页偏移量一般以 heap no 的形式输出,但此例并未输出该信息

基本环境信息

确认如下问题相关信息:

数据库版本:Percona MySQL 5.7

事务隔离级别:Read-Commited

表结构和索引

CREATETABLE`ltb2`(
`ID`bigint(20)unsignedNOTNULLAUTO_INCREMENTCOMMENT'ID',
`j`varchar(16)DEFAULTNULLCOMMENT'',
`c`varchar(32)NOTNULLDEFAULT''COMMENT'',
`b`dateNOTNULLDEFAULT'2019-01-01'COMMENT'',
`f`varchar(1)NOTNULLDEFAULT''COMMENT'',
`g`varchar(1)NOTNULLDEFAULT''COMMENT'',
`d`varchar(1)NOTNULLDEFAULT''COMMENT'',
`e`varchar(1)NOTNULLDEFAULT''COMMENT'',
`h`varchar(1)NOTNULLDEFAULT''COMMENT'',
`i`varchar(1)DEFAULTNULLCOMMENT'',
`LAST_UPDATE_TIME`timestampNOTNULLDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMPCOMMENT'修改时间',
PRIMARYKEY(`ID`),
UNIQUEKEY`uidx_1`(`b`,`c`)
)ENGINE=InnoDBAUTO_INCREMENT=270983DEFAULTCHARSET=utf8mb4COMMENT='';

关键信息梳理

事务 T1
语句 delete from ltb2 where c = 'code001' and j = 'Y15' and b >= '20230717' and d != '1' and e != '1'
关联对象及记录 space id 603 page no 86 n bits 248 index PRIMARY of table testdb.ltb2
持有的锁 未知
等待的锁 lock_mode X locks rec but not gap waiting
事务 T2
语句 update ltb2 set f = '0', g = '0', is_value_date = '0', h = '0', i = '0' where c = '22115001B' and j = 'Y4' and b >= '20230717'
关联对象及记录 space id 603 page no 86 n bits 248 index PRIMARY of table testdb.ltb2
持有的锁 lock_mode X locks rec but not gap
等待的锁 lock_mode X locks rec but not gap waiting

可以看到在主键索引上发生了死锁,但是在查询的条件中,并未使用主键列。

那为什么会在主键列出现死锁?

在分析死锁根因问题前,需要先清楚 SQL 的执行情况。

2 SQL 执行情况

执行计划

以上两个 SQL 发现都有列 b、c 作为条件,且该列构成了索引唯一索引 uidx_1。简化 SQL 改为查询语句,并确认执行计划:

mysql>descselect*fromltb2whereb>='20230717'andc='code001';

#部分结果
+------+-------------------+------+---------+
|type|possible_keys|key|Extra|
+------+-------------------+------+---------+
|ALL|uidx_1|NULL|Usingwhere|
+------+-------------------+------+---------+

注意:自 MySQL 5.6 开始可以直接查看 UPDATE/DELETE/INSERT 等语句的执行计划。因个人习惯、避免误操作等原因,还是习惯改为 SELECT 查看执行计划。

执行计划中可能的索引有uidx_1(b,c),但实际并未使用该索引,而是采用全表扫描方式执行。

根据经验,由于列 b 为索引的最左列。但查询的条件为b>= '20230717',即该条件不是等值查询。因此数据库可能只能“使用”到 b 列。为进一步确认不使用 b 列索引的原因,查询数据分布:

mysql>selectcount(1)fromltb2;

+------------+
|count(1)|
+------------+
|4509|
+------------+

mysql>selectcount(1)fromltb2whereb>='20230717';

+------------+
|count(1)|
+------------+
|1275|
+------------+

计算满足 b 列条件的数据占比为 1275/4509 = 28%,占比差不多达到了 1/3。此时也的确不应使用该使用索引。

难道已经是作为 MySQL 5.7 的数据库,优化器还是这么简单?

ICP 特性

带着问题,将条件设置一个更大的值(但小于该列的最大值),再次执行验证查询语句:

mysql>descselect*fromltb2whereb>='20990717';

#部分结果
+----------+---------+---------+
|key_len|rows|Extra|
+----------+---------+---------+
|3|64|UsingIndexcondition|
+----------+---------+---------+

优化器预估返回 64 行,数据占比 64/4509 = 1.4%,因此可以使用索引。但通过执行计划,从Extra列看到Using index condition提示。该提示则说明使用了索引条件下推(Index Condition Pushdown, ICP)。针对该特性,参考官方简要说明如下:

使用 Index Condition Pushdown,扫描将像这样进行:

获取下一行的索引元组(但不是完整的表行)。

测试 WHERE 条件中应用于此表的部分,并且只能使用索引列的进行检查。如果不满足条件,则继续到下一行的索引元组。

如果满足条件,则使用索引元组定位并读取整个表行。

测试适用于此表的 WHERE 条件的其余部分。根据测试结果接受或拒绝该行。

既然可以使用到 ICP 特性,进一步执行如下验证语句:

mysql>descselect*fromltb2whereb>='20990717'andc='code001';

#部分结果
+----------+---------+---------+
|key_len|rows|Extra|
+----------+---------+---------+
|133|64|UsingIndexcondition|
+----------+---------+---------+

发现当新增 c 列作为条件后,并且根据 key_len(索引里使用的字节数)可以判断,的确使用到了 uidx_1 索引中的 c 列。但 rows 的结果与实际返回结果差异较大(实际执行仅返回 0 行)。

更重要的是,既然具有 ICP 特性,针对原始的 SQL 为什么不能助于 ICP 特性使用到索引呢?

mysql>select*fromltb2whereb>='20230717'andc='code001'

执行计划跟踪

继续带着问题,通过 MySQL 提供的 OPTIMIZER TRACE,跟踪执行计划生成过程。命令如下:

SETOPTIMIZER_TRACE="enabled=on",END_MARKERS_IN_JSON=on;
SETOPTIMIZER_TRACE_MAX_MEM_SIZE=1000000;
--sql-1:
select*fromltb2whereb>='20990717'andc='code001';
--sql-2:
select*fromltb2whereb>='20990717';
--sql-3
select*fromltb2whereb>='20230717'andc='code001';

SELECT*FROMINFORMATION_SCHEMA.OPTIMIZER_TRACEG
SEToptimizer_trace="enabled=off";

由于分析结果较长,截取 SQL-1 和 SQL-2 的部分结果 (rows_estimation 和 considered_execution_plans)。具体内容如下:

SQL-1

select*fromltb2whereb>='20990717'andc='code001'

#分析结果
"analyzing_range_alternatives":{
"range_scan_alternatives":[
{
"index":"uidx_1",
"ranges":[
"0xe76610<= b"
        ] /* ranges */,
        "index_dives_for_eq_ranges": true,
        "rowid_ordered": false,
        "using_mrr": false,
        "index_only": false,
        "rows":64,
        "cost": 77.81,
        "chosen": true
     }
  ] /* range_scan alternatives */
}

"best_access_path":{
    "considered access_paths":[
      "rows_to_scan": 64,
      "access_type":"range",
      "range_details":{
        "used index";"uidx 1"
        } /* range_details */,
      "resulting_rows": 64,
      "cost": 90.61,
      "chosen": true
    }
  ] /* considered access_paths */
} /* best access_path */,

SQL-2

select*fromltb2whereb>='20990717'

#分析结果
"analyzing_range_alternatives":{
"range_scan_alternatives":[
{
"index":"uidx_1",
"ranges":[
"0xe76610<= b"
        ] /* ranges */,
        "index_dives_for_eq_ranges": true,
        "rowid_ordered": false,
        "using_mrr": false,
        "index_only": false,
        "rows":64,
        "cost": 77.81,
        "chosen": true
     }
  ] /* range_scan alternatives */
}

"considered access_paths":[
  {
    "rows_to_scan": 64,
    "access_type":"range",
    "range_details":{
      "used index":"uidx_1"
      } /* range_details */,
      "resulting_rows": 64,
      "cost": 90.61,
      "chosen": true
   }
] /* considered access_paths */,

根据以上信息:两个 SQL 的 cost 部分是完全相同的,且在优化器分析阶段只能识别到 b 的条件。分析阶段,只能根据优化器认为可用的列来计算 cost。ICP 特性,应该是在执行阶段采用用到的特性。

同时,根据 SQL-3 的执行跟踪结果,对比全表扫描和索引扫描的 cost,截取部分结果如下:

SQL-3

select*fromltb2whereb>='20230717'andc='code001';

#全表扫描结果
"range_analysis":{
"table_scan":{
"rows":4669,
"cost":1018.9
}/*table_scan*/,

#索引扫描评估结果
"analyzing_range_alternatives":{
"range_scan_alternatives":[
{
"index":"uidx_1",
"ranges":[
"@xe7ce0f]<= b"
        ] /* ranges */,
        "index dives_for_eq_ranges": true,
        "rowid_ordered": false,
        "using_mrr": false,
        "index_only": false,
        " rows": 1273,
        "cost": 1528.6,
        "chosen": false,
        "cause":"cost"
      }
  ] /* range scan_alternatives */,
  
# 最优执行计划
"best_access_path": {
  "considered access_paths":[
    {
      "rows_to_scan": 4669,
      "access_type":"scan",
      "resulting_rows": 4669,
      "cost": 1016.8,
      "chosen": true
    }
  ] /* considered access_paths *//* best access_path */
}

由于优化器阶段使用使用列 b,使用索引的成本高于全表扫描。那最终数据库就会选择使用全表扫描。除非应用使用 hint 强制索引:

mysql>descselect*fromltb2FORCEINDEX(uidx_1)whereb>='20230717'andc='code001';

#部分结果
+----------+---------+---------+
|key_len|rows|Extra|
+----------+---------+---------+
|133|1273|UsingIndexcondition|
+----------+---------+---------+

同时,根据执行计划的输出结果,rows 列应该是优化器阶段的输出,key_len/Extra 则包括了执行阶段的输出。

小结

综上所述,对于问题 SQL 和索引结构,由于列 b 为索引的最左列,且查询时的条件为 b>= '20230717'(非等值条件),数据库优化器只能“使用”到 b 列。并给予“使用”的列,评估扫码的行数和 cost。

如果优化器评估后,使用索引的成本更低,则可以使用该索引,并利用 ICP 特性进一步提高查询性能;

如果优化器评估后,使用全表扫描或的成本更低,那数据库就会选择使用全表扫描。

3 SQL 优化方案

根据第 2 部分明确了问题的原因后,通过调整索引,解决最左列尾范围查询的问题即可解决该问题。具体如下:

altertableltb2dropindexuidx_1;
altertableltb2addindexuidx_1(c,b);
altertableltb2addindexidx_(b);

死锁为何发生

自此,完成了 SQL 执行计划问题的分析和解决。但直接的问题是死锁,因查询语句无法使用索引,正常就应该使用全表扫描。但是全表扫描为什么会出现死锁呢?

在此,对死锁过程进行大胆猜想:

T1 时刻

trx-2 执行了 UPDATE,在处理行时,在 row_search_mvcc 函数中,查询到数据。获取了对应行的 LOCK_X,LOCK_REC_NOT_GAP 锁;

T2 时刻

trx-1 执行了 DELETE,在处理行时,在 row_search_mvcc 函数中,查询到数据,尝试获取行的 LOCK_X,LOCK_REC_NOT_GAP。但由于 trx-1 已经持有了该锁,因此被堵塞。并会创建一个锁(以指示锁等待);

T3 时刻

trx-2 继续执行 UPDATE 操作。由于是该操作除了在 T1 时刻的操作外,在其它位置,还需要获取锁(lock_mode X locks rec but not gap)。但由于 T2 时刻,trx-1 尝试获取该锁而被堵塞,并且也增加了一个锁。

假如此时,此处的实现机制和 INSERT 死锁案例一样,也没有先进行冲突检查。而只是看记录上是否存在锁的话,那么此时也会看到该记录上有 trx-1 事务的锁。从而导致 trx-2 第二次获取锁时,被堵塞。

死锁发生!







审核编辑:刘清

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • SQL
    SQL
    +关注

    关注

    1

    文章

    762

    浏览量

    44113
  • MYSQL数据库
    +关注

    关注

    0

    文章

    96

    浏览量

    9389
  • ICP
    ICP
    +关注

    关注

    0

    文章

    69

    浏览量

    12777

原文标题:从一个死锁问题分析优化器特性

文章出处:【微信号:OSC开源社区,微信公众号:OSC开源社区】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    怎么简单实现由Labview读取的串口数据自增写入mysql5.7数据库中?

    怎么简单实现由Labview读取的串口数据自增写入mysql5.7数据库中? 已实现:串口数据的接收处理
    发表于 01-11 22:05

    labview有调用mysql数据库问题????

    labview有调用mysql数据库,请问labview打包成exe安装档,怎么把mysql数据库打包进来,是mysql
    发表于 05-19 16:17

    mysql数据库设计步骤

    mysql数据库设计和优化
    发表于 05-13 11:00

    MySQL数据库使用

    关于MySQL数据库简单操作
    发表于 10-24 14:32

    mysql数据库同步原理

    数据库的访问压力,提升整个系统的性能和可用性,降低了大访问量引发数据库宕机的故障率。 binlog简介 MySQL主从同步是基于binlog文件主从复制实现,为了更好的理解主从同步过程,这里
    发表于 09-28 11:49 0次下载
    <b class='flag-5'>mysql</b><b class='flag-5'>数据库</b>同步原理

    MySQL数据库如何安装和使用说明

    :文件夹 5.数据库管理软件:MySQL oracle,db2,sqlserver 6.数据库服务:运行数据库管理软件的计算机
    的头像 发表于 02-13 16:13 2791次阅读

    MySQL数据库:理解MySQL的性能优化优化查询

    最近一直在为大家更新MySQL相关学习内容,可能有朋友不懂MySQL的重要性。在程序,语言,架构更新换代频繁的今天,MySQL 恐怕是大家使用最多的存储数据库了。由于
    的头像 发表于 07-02 17:18 3088次阅读
    <b class='flag-5'>MySQL</b><b class='flag-5'>数据库</b>:理解<b class='flag-5'>MySQL</b>的性能<b class='flag-5'>优化</b>、<b class='flag-5'>优化</b>查询

    华为云数据库-RDS for MySQL数据库

    华为云数据库-RDS for MySQL数据库 华为云数据库作为华为云的一款数据库产品,它主要是以MyS
    的头像 发表于 10-27 11:06 1513次阅读

    MySQL数据库服务数据库和表之间是什么关系

    数据库服务MySQL安装后,会成为一个windows服务,这个windows服务可以看做是数据库服务。用CMD登录
    的头像 发表于 01-31 14:59 1203次阅读
    <b class='flag-5'>MySQL</b><b class='flag-5'>数据库</b>服务<b class='flag-5'>器</b>、<b class='flag-5'>数据库</b>和表之间是什么关系

    MySQL数据库管理与应用

    MySQL数据库管理与应用 MySQL是一种广泛使用的关系型数据库管理系统,被认为是最流行和最常见的开源数据库之一。它可以被用于多种不同的应
    的头像 发表于 08-28 17:15 966次阅读

    mysql是一个什么类型的数据库

    MySQL是一种关系型数据库管理系统(RDBMS),用于存储和管理大量结构化数据。它被广泛用于各种应用程序和网站的后端,包括电子商务平台、社交媒体网站、金融系统等等。MySQL的特点是
    的头像 发表于 11-16 14:43 1760次阅读

    MySQL数据库基础知识

    的基础知识,包括其架构、数据类型、表操作、查询语句和数据导入导出等方面。 MySQL 数据库架构 MySQL
    的头像 发表于 11-21 11:09 969次阅读

    mysql数据库基础命令

    MySQL是一个流行的关系型数据库管理系统,经常用于存储、管理和操作数据。在本文中,我们将详细介绍MySQL的基础命令,并提供与每个命令相关的详细解释。 登录
    的头像 发表于 12-06 10:56 576次阅读

    数据库数据恢复—未开启binlog的Mysql数据库数据恢复案例

    mysql数据库数据恢复环境: 本地服务,windows server操作系统 ,部署有mysql单实例,
    的头像 发表于 12-08 14:18 1129次阅读
    <b class='flag-5'>数据库</b><b class='flag-5'>数据</b>恢复—未开启binlog的<b class='flag-5'>Mysql</b><b class='flag-5'>数据库</b><b class='flag-5'>数据</b>恢复案例

    数据库数据恢复—Mysql数据库表记录丢失的数据恢复流程

    Mysql数据库故障: Mysql数据库表记录丢失。 Mysql数据库故障表现: 1、
    的头像 发表于 12-16 11:05 88次阅读
    <b class='flag-5'>数据库</b><b class='flag-5'>数据</b>恢复—<b class='flag-5'>Mysql</b><b class='flag-5'>数据库</b>表记录丢失的<b class='flag-5'>数据</b>恢复流程