根据Mysql索引原理及优化这个博主的视频学习,对现在的岁月云项目中mysql进行优化,我要向这个博主致敬,之前应用居多,理论所知甚少,于是将学习到东西,应用下来,看看是否有好的改观。
1 索引原理
1.1 B+Tree
索引的目的是用来快速查询的,怎样查找快,这就用到大学所学的数据结构的知识。理论不需要我来写,视频博主讲的比我好。我只记录我所理解的。如果所有的左边都比当前节点小,所有的右边都比当前节点大,这样就很容易缩小范围,于是想到二叉查找树。
但是二叉查找树,当数据有序的时候,就可能比较惨,形成单向链表,时间复杂度升级为O(n),于是就有了平衡二叉树(AVL 树),因为平衡,所以查询稳定,但是因为一个节点最多有两个节点,树高问题依旧存在。
于是就有了B树,因为B树允许一个节点存放多个数据,树高再次降低,但B树的存储结构是key和data都在节点中,这就是一则使得查询性能不稳定,二则因为节点也保存了数据,内存增加,且data大了,key就小了,节点自然也就增多了,B树的层高也相应就升高了,I/0次数增加,性能就会变差。另外B树的数据在树的节点当中,因此适合随机查询,而不适合范围查询,在Btree在线,这个地方可以演示插入的过程。
接着思想演化为选择B+树,B+树的特点是叶子节点包括全部关键字和相应记录的指针,则查询自然是稳定的,B+tree中叶子节点采用双向链表的结构,则范围查询的问题也就解决了。非叶子节点只有索引没有数据,因此一节点就可以存储更多的内容,树高就降低了。
InnoDB中最小存储单元是页(每页16KB),mysql设计者将B+Tree的一个节点大小设置为一个页(16KB),这样每个节点只需要一次IO就能够完整载入整页数据。知道了这个概念就知道后面查询成本怎么计算的。
一个高度为2的B+Tree,存在一个根节点和若干叶子节点,总记录数=根节点指针数*单个叶子节点的记录数。如果是主键索引(聚簇索引)根节点指针数根据主键长度来,mysql中int类型占4个字节,指针类型占6个字节,那么一页16*1024/(4+6)=1638,即一个节点最多存储1638个索引指针。每个叶子节点记录数,跟一行记录大小有关,如果一行记录为1KB,则一页记录16行,如此得出高度为2的B+Tree,总记录数=1638*16=26208。同理告诉为3的B+Tree,可以存放记录数=1638*1638*16=42,928,704,所以并不是别人说的mysql单表不能超过2000万这个固定数字。
1.2 聚簇索引
对于聚簇索引,数据存储与索引是在一起的,索引结构的叶子节点保存了具体的行数据。
对于非聚簇索引,将数据和索引分开存储,索引结构的叶子节点存储的是指向数据行对应的地址。
在主键自增情况下数据的物理存放顺序与索引顺序是一致的。MyISAM引擎中主键索引和辅助索引都是非聚簇索引。
下图acc_id_table使用的MyISAM存储引擎,有3个文件,frm是表定义文件,包含表结构的定义,包括列名、数据类型、索引等元数据信息。MYD描述数据文件。MYI是索引文件。
Innodb中有两个,frm都是一样的作用,ibd为表空间文件,存储实际数据和索引信息。
1.2.1 回表
InnoDB 使用聚簇索引来存储表的数据,而辅助索引的叶子节点不仅包含索引列的值,还包含对应的主键值。这种设计使得通过辅助索引查找数据时,能够快速定位到聚簇索引中的具体行位置。
因此就有了回表操作,先查辅助索引,但获取数据实际是根据聚簇索引(主键索引)得到对应的数据,这个过程就是回表,扫描了辅助索引树和聚簇索引树。
使用覆盖索引解决回表问题。一个索引包含了所需查询的所有字段,就可以不需要回表。这个方法虽然没有问题,但是中小型系统估计就不会去考虑这个优化,研发成本跟这点性能成本比更本就不算什么。
例如单行记录超过max_length_for_sort_data(默认1K),就会采用rowid排序,从而执行回表操作。
1.2.2 索引下推
mysql5.6引入,用于查询优化,在索引遍历过程中,对于索引包含的字段先做判断,把不符合条件的记录过滤掉,减少回表次数。
1.2.3 主键类型选择
多语言、表情符号和减少编码转换,使得utf8mb4
成为首选。在utf8mb4中1个char占4个字节。执行下面的语句,可以知道自己的表中各字段占用的空间。
SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, NUMERIC_PRECISION, NUMERIC_SCALE, CHARACTER_OCTET_LENGTH,CASEWHEN DATA_TYPE IN ('char', 'varchar') THEN CHARACTER_OCTET_LENGTHWHEN DATA_TYPE = 'tinyint' THEN 1WHEN DATA_TYPE = 'smallint' THEN 2WHEN DATA_TYPE = 'mediumint' THEN 3WHEN DATA_TYPE = 'int' THEN 4WHEN DATA_TYPE = 'bigint' THEN 8WHEN DATA_TYPE IN ('float', 'double') THEN NUMERIC_PRECISION / 8 + 1WHEN DATA_TYPE = 'decimal' THEN CEIL(NUMERIC_PRECISION / 9) * 4 + IF(NUMERIC_SCALE > 0, 2, 0)ELSE NULLEND AS estimated_bytes
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = 'acc'AND TABLE_NAME = 'acc_analysis_equation';
知道这个基础,就知道数据表中状态字段最好使用tinyint,不要使用char了。而如下面bigint也才8个字节,如果使用32位uuid,则需要128个字节,索引存储空间相比下将16倍。
2 Explain性能分析
2.1 单表
从慢日志中找到一条插入时的问题,explain结果如下:
创建索引后,再次查看 ,这个语句就过关了。
不要小看这个索引创建,可以再慢日志可以看到有很多相同的语句,这样性能就会提升很多了。下图可以看到Rows_examined预计扫码记录数203万,这个数据不小。
2.2 联表
以下面的慢日志样例作explain。
- id:表示查询中执行子句或操作表的顺序,id相同执行的顺序是由上到下。如果由子查询,id是不同的。id越大执行优先级越高,也就是说子查询会优先执行。
- type:找到数据所需要的扫码方式,重要指标
完整的连接类型,一般需要保证查询至少range级别,最好能到ref。const代表主键索引或者唯一索引;req_ref代表对于前表中每一行,后表只有一行被扫描,这个只连接使用索引的部分都是主键或者唯一非空索引,才会出现;ref代表普通索引,对于前表的每一行,后表有可能有多于一行的数据被扫描。range代表范围查询,between、in、>、<都是范围查询。index代表SQL使用了索引,但没有通过索引进行过滤,需要扫描索引上的全部数据,例如Innodb索引count查询就会扫描索引上的全部数据进行统计。
system>const>eq_ref>ref>fulltext>ref_or_null
>index_merge>unique_subquery>index_subquery
>range>index>ALL
- possible_keys:可能应用到这张表的索引,一个或多个,列出但实际不一定会用
- key:实际使用的索引,若为null,两种情况:1、没有建立索引。2、建立索引,但索引失效。查询中使用了覆盖索引,该索引只会出现在key列表中。
- key_len:索引中使用的字节数。
- ref:const代表常数等值查询、连接查询中,ref字段显示驱动表关联的字段。
- rows:表示mysql在检索时估算的行记录数量。
- extra:①using filesort:查询结果进行文件排序,性能非常差需要优化;②Using temporary;使用临时表存储数据结果。③Using where:查询的列未被索引覆盖,被全表扫描。④Using index:使用了索引获取数据,使用到了聚簇索引或者覆盖索引,非常好。⑤Using join buffer:使用了连接缓存,会显示join连接查询时所需要的查询算法。⑥Using index condition:表示了使用了索引下推,仅适用于二级索引,因为聚簇索引完整记录是被InnoDb缓存中。⑦Using where+Using index,表示查询列被索引覆盖了,但无法通过索引直接获取数据。
3 高性能索引创建及使用策略
3.1 索引列类型尽量小
知道了索引原理,就知道了,索引列类型越小,那么索引空间就比较少,查询性能也就越快,一个页纳入更多的索引数据,也就减少了磁盘IO损耗。内存中可以纳入更多的数据页缓存,在实战中更关注在选择自增、雪花id还是uuid。
int类型自增,对于绝大多数项目是够用的,但是多租户模式下一般用联合所以,这个自增就不太起作用。就可以用到雪花id。而uuid是字符串,排序自然是按照字母的循序,而因为字母是无序的,自然查询性能就慢一些。
3.2 选择性尽量高
索引是为了快速定位,如果选择性低的,比如凭证类型、性别之类的,基本就是固定选项,这些字段设置索引就没有多大意义,不能帮你过滤大批的数据。
例如下例中,现有的group_id是凭证类型:收、记、付、转,只会有四个选项,更甚的是大多数用户用的都是记账凭证。
按照选择性公式计算,选择性=不同值得数量/总行数,按照下面得计算,选择性0.0025,非常低,代表group_id列重复性很高,不适合作为索引项。选择性越高越适合作为索引项。因此需要将group_id列从联合索引中去除,以缩短索引字段长度,存储更多有效数据。
对比一下去掉前后,从下面可以看到没有变化,因此这个索引字段去掉是合理得。
因为数据库做了分表,于是写一个存储过程批量更新表索引,如下
BEGIN-- 定义一个游标来遍历表名DECLARE done INT DEFAULT FALSE;DECLARE tbl_name VARCHAR(255);DECLARE cur CURSOR FORSELECT table_nameFROM information_schema.tablesWHERE table_schema = DATABASE() AND table_name REGEXP '^acc_voucher_[0-9]+$';DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;OPEN cur;read_loop: LOOPFETCH cur INTO tbl_name;IF done THENLEAVE read_loop;END IF;-- 删除旧索引SET @sql = CONCAT('ALTER TABLE ', tbl_name,' DROP INDEX `idx_adId_period_groupId_num`');PREPARE stmt FROM @sql;EXECUTE stmt;DEALLOCATE PREPARE stmt;-- 添加新索引SET @sql = CONCAT('ALTER TABLE ', tbl_name,' ADD INDEX `idx_adId_period_num` (`as_id`, `period`, `num`) USING BTREE');PREPARE stmt FROM @sql;EXECUTE stmt;DEALLOCATE PREPARE stmt;END LOOP;CLOSE cur;
END
3.3 最佳左前缀法则
针对联合查询,因为索引排序也是有比较过程,多个字段肯定有一个顺序,你多个索引字段,自然回选择左的字段作为先排序的。这里有回到了B+Tree的原理了,B+Tree是局部有序。
查看下面的索引,最左指的是索引Fieds左边第一,并不是where条件的最左,因为查询引擎会进行优化。
从下图可以看到索引失效了,进行了全表扫描。
接着我们调整索引顺序,将relate_company调整到最左边,再次查看索引生效。
3.4 like使用
依旧是最左原则,索引是按照字母排序的,右侧%索引才有有效,左侧%索引失效,通过覆盖索引来解决这个问题。
code是二级索引字段,但是此种情况覆盖索引并没有解决问题。
3.5 null值
is null、is not null、!=、or会使索引失效,但未必一定。null值代表一个未确定的值,Mysql认为任何和null值进行比较的表达结果都为null,所以认为每个null值都是独一无二或是一份或是没有意义,mysql专门设置系统变量,用于统计索引列不重复值null的问题。总体原则是尽量不要使用null。
3.6 不要对索引列做任何操作
索引减少了磁盘IO,因此可以先获取数据,然后通过程序来做你想做的业务,而不是在sql语句上去做业务直接相关的函数操作,因为索引列作函数操作,导致索引失效,而内存速度与磁盘IO速度无法媲美。因此才有这个原则。
字符串类型隐式转换问题,如下面accounting_standard字段定义的char(1),可以查看到key_len是4,已经发生了隐式转换.
下面是按照字符串的方式查找,扫描的rows相比隐式转换的要少。
3.7 前缀索引
mysql不支持索引blob、text、较长的varchar等字段类型的全部长度,这个了解B+Tree原理就明白这个推论。通过前缀索引来解决这个问题。如下索引,取前缀N个字段。
alter table table_name add key idx_xx(column(N))
至于N取多少,可以通过left函数计算出数量与count的比值,得出一个合理的数值。
前缀索引的缺点是无法用作order by、group by,也无法利用前缀索引进行索引覆盖。
3.8 多列索引
即多列索引,注意索引列字段顺序。
- 选择性高的放在最前面
- 运行效率高低,调整索引列的顺序
- 覆盖查询需要的列,创建联合索引的时候,要注意是否是一个覆盖索引,避免回表。
- 避免冗余索引:已经有了一个联合索引,就没有必要将该局部索引字段再创建一个另外的索引。
- 权衡索引的长度:避免使用过多列,只创建查询或排序中经常使用到的字段。
4 慢日志查询分析
4.1 慢日志参数
my.cnf配置3个配置跟他有关系
# 开启慢日志
slow_query_log = 1
# 慢日志的位置
slow_query_log_file = /data/mysql_3306/logs/sql_query_slow.log
# 单位为s
long_query_time = 1
log_queries_not_using_indexes表述为使用索引的查询,也会被记录到慢查询日志中,因此再调优的时候,需要开启,执行命令set global log_queries_not_using_indexes=1;
mysql> show variables like '%log_output%';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| log_output | FILE |
+---------------+-------+
1 row in set (0.02 sec)mysql> show variables like '%log_queries%';
+-------------------------------+-------+
| Variable_name | Value |
+-------------------------------+-------+
| log_queries_not_using_indexes | OFF |
+-------------------------------+-------+
1 row in set (0.01 sec)
4.2 慢日志样例
4.2.1 超过long_query_time示例
从慢查询中选取一条查看,Query_time表明查询执行的时间,Lock_time为等待锁的时间,Rows_sent表示查询结果的数量,Rows_examine表示查询扫描的行数。
# Time: 2024-12-06T13:25:50.570195+08:00
# User@Host: root[root] @ [10.101.12.88] Id: 367258
# Query_time: 1.286461 Lock_time: 0.000064 Rows_sent: 420 Rows_examined: 2044
SET timestamp=1733462750;
selectaccVoucher.as_id as_id_accVoucher, accVoucher.id id_accVoucher, accVoucher.group_id group_id_accVoucher,accVoucherEntry.debit_amount debit_amount_accVoucherEntry, accVoucherEntry.credit_amountcredit_amount_accVoucherEntry,accVoucherEntry.price price_accVoucherEntry, accVoucherEntry.count count_accVoucherEntry,accVoucherEntry.fc_id fc_id_accVoucherEntry, accVoucherEntry.rate rate_accVoucherEntry,accVoucherEntry.debit_amount_original debit_amount_original_accVoucherEntry,accVoucherEntry.credit_amount_original credit_amount_original_accVoucherEntry,accVoucherEntry.line_num line_num_accVoucherEntryfrom acc_voucher_15 accVoucherinner join acc_voucher_entry_15 accVoucherEntry on accVoucherEntry.v_id = accVoucher.id andaccVoucherEntry.as_id=accVoucher.as_idWHERE (accVoucher.as_id = 112665 AND accVoucher.period >= '202107' AND accVoucher.period <= '202107') ORDER BY accVoucher.period ASC,accVoucher.group_id ASC,accVoucher.num ASC,accVoucherEntry.line_num ASC;
4.2.2 开启log_queries_not_using_indexes示例
从下面可以看到虽然有的查询没有超过1s,但是因为没有使用索引,也会写入到慢日志中,另外存储过程的调用也会被记录进去。当然如果想要知道全表扫描也不一定需要这么干,可以参考mysql performance schema 实践,有语句可以直接查询到对应的数据查询没有使用索引。
是否一定要索引,那到未必,因为有的查询使用索引,还不如直接磁盘获取。但是确实是潜在风险。所以我觉得可以先关掉,先解决1s以上查询的问题。
# Time: 2024-12-06T15:13:42.336056+08:00
# User@Host: root[root] @ [10.101.12.91] Id: 368858
# Query_time: 0.170161 Lock_time: 0.000000 Rows_sent: 0 Rows_examined: 3049
use acc;
SET timestamp=1733469222;
CALL init_data(186360,'202107','1');
# Time: 2024-12-06T15:13:42.410030+08:00
# User@Host: root[root] @ [10.101.12.88] Id: 368830
# Query_time: 0.003948 Lock_time: 0.000516 Rows_sent: 1 Rows_examined: 8305
use eayc;
SET timestamp=1733469222;
SELECT COUNT( * ) FROM eayc_company_business
因为我们实际希望的是如下面,我调用存储过程超过1s需要记录下来
4.2.3 备份的时候触发的
在慢日志中可以查到备份用户执行的语句,/*!40001 SQL_NO_CACHE */是mysql中的条件注释的语法,MySQL 不要将查询结果存储在查询缓存(Query Cache)。Mysql8.0+已经没有查询缓存,因为我的生产库用的是mysql5.7,因此还会有这个语句。
4.2.4 update触发
查看慢日志可以看到很多之前没有想到的问题,比如下面update问题。
# Time: 2024-12-07T17:01:34.617058+08:00
# User@Host: root[root] @ [10.101.12.88] Id: 390876
# Query_time: 1.799098 Lock_time: 0.000085 Rows_sent: 0 Rows_examined: 2057362
SET timestamp=1733562094;
UPDATE acc_fund_template_entry SET status='00' WHERE (template_id IN (42));
这里的type是index,但是rows却有31万,肯定是有问题的。通过sql并不知道是怎么产生的,就需要开发工程师去调试代码。
根据业务调试,定位代码发现,多租户模式下更新没有使用账套id,问题由此修正。
前端是传了账套id字段,但是开发工程师却忽略了。从这个问题可以看出,这些问题绝大多数测试工程师是测不出这样的问题,关注慢日志以为是运维的工作,通过慢查询可以很快知道系统的漏洞在哪里。
修正后sql如下,type也到了range,同时rows也很少了。
5 join优化
5.1 Simple Nested Loop Join
简单嵌套循环连接,这个就是简单的多层for循环遍历。但实际mysql中是不用的,因为在没有所用的时候使用的是Block Nested Loop Join策略。
5.2 Index Nested Loop join
减少了内层表数据的匹配次数,因为join字段(被驱动表用于连接的字段)使用了索引
left join 是左边为驱动表,右边为被驱动表;right join左边为被驱动表,右边为驱动表,right和left join在,但where条件触发就不一样了。inner join谁的数量少谁就是驱动表。
5.3 Block Nested Loop join
mysql通过buffer缓冲区,降低内循环的次数,参数使用join_buffer_size,默认256K。
join使用原则是用小表来驱动大表循环。减少查询的字段,join buffer所缓存的数据越多,这个参数是面向连接的。可以调大。
5.4 in函数
如果子查询结果集比较小,而主查询结果集比较大而且右索引时,则优先选择in函数。这个也是小表驱动大表原则。
5.5 exists函数
与in方向,如果主查询结果集比较小,子查询的表比较大并且有索引,则选择exists函数。
6 order by与group by优化
6.1 order by
原则是最好使用索引字段进行排序,但需要考虑的是索引失效的情况。mysql中有两种排序方式,①索引排序:通过有序索引顺序扫描。②额外排序(Filesort):对返回的数据进行文件排序。优化的核心自然是:尽量减少额外排序,通过索引直接返回有序数据。
单表容易,但联表就不好操作了,因为排序时驱动表和被驱动表都会参加排序。
6.1.1 sort_buffer_size
mysql为每个线程各维护了一个内存区域(sort buffer),用于排序。因为是面向连接,因此并不是越大越好。下面是我的配置,默认是2M。
如果加载的记录字段的总长度小于sort buffer,则使用sort buffer;超过,则使用sort buffer+临时文件进行排序。
下面写一个python代码计算一下217条数据,结果数据181.41KB,2M空间则可以容纳2449条凭证,基本够用。也就是说有些情况虽然explain得到了temparoy或者file sort并不一定非得着急去优化。
import pymysql
import math
def get_query_result_size(host, user, password, database, query):# 连接到数据库connection = pymysql.connect(host=host,user=user,password=password,database=database,charset='utf8mb4', # 确保使用 utf8mb4 字符集cursorclass=pymysql.cursors.DictCursor)try:with connection.cursor() as cursor:# 执行查询cursor.execute(query)result = cursor.fetchall()# 计算结果的字节数byte_size = sum(len(str(value).encode('utf-8')) for row in result for value in row.values())return byte_sizefinally:connection.close()def convert_size(size_bytes):"""将字节数转换为合适的单位 (KB, MB, GB)"""if size_bytes == 0:return "0B"size_name = ("B", "KB", "MB", "GB", "TB")i = int(math.floor(math.log(size_bytes, 1024)))p = math.pow(1024, i)s = round(size_bytes / p, 2)return f"{s} {size_name[i]}"def getsql():return " select " \" accVoucher.as_id as_id_accVoucher, accVoucher.id id_accVoucher, accVoucher.group_id group_id_accVoucher," \" accVoucher.num num_accVoucher, accVoucher.period period_accVoucher, accVoucher.v_date v_date_accVoucher," \" accVoucher.attach_num attach_num_accVoucher, accVoucher.status status_accVoucher," \" accVoucherEntry.debit_amount_original debit_amount_original_accVoucherEntry," \" accVoucherEntry.credit_amount_original credit_amount_original_accVoucherEntry," \" accVoucherEntry.line_num line_num_accVoucherEntry" \" from acc_voucher_44 accVoucher" \" inner join acc_voucher_entry_44 accVoucherEntry on accVoucherEntry.v_id = accVoucher.id and" \" accVoucherEntry.as_id=accVoucher.as_id" \" WHERE (accVoucher.as_id = 281794 AND accVoucher.period >= '201108' AND accVoucher.period <= '201212') " \" ORDER BY accVoucher.period ASC,accVoucher.group_id ASC,accVoucher.num ASC,accVoucherEntry.line_num ASC"# 示例用法
if __name__ == "__main__":host = 'localhost'user = 'root'password = '123456'database = 'acc'query = getsql()size_in_bytes = get_query_result_size(host, user, password, database, query)readable_size = convert_size(size_in_bytes)print(f"Result size: {readable_size}")
6.1.2 max_length_for_sort_data
如果用于排序的单条记录的全字段的总长度<=该参数的值,就使用全字段排序,否则就使用rowid排序。
这里的全字段是将查询的所有字段全部加载到sort buffer中进行排序。优点是查询过程简单查询快,确定是需要的空间过大。
rowid排序不会将全部字段放入sort buffer,在sort buffer之后还需要回表查询。只把用来排序的字段和主键id,放入到sort buffer中,其他字段通过回表获取。
原则:多利用内存,尽量减少磁盘访问。
6.1.3 排序字段来自多个索引,无法使用索引进行排序
下面可以看到排序字段来自accVoucherEntry和accVoucherEntry,来自多张表,无法使用索引进行排序。要优化的话,可以将数据加载到程序,由程序进行排序即可。
6.1.4 排序字段与索引字段顺序不一致,无法利用索引排序
从下图可以看到如果把num作为第一排序,与索引字段顺序不一致,还是会走filesort
6.1.5 where条件范围查询
还是最左原则,排序字段不同,会发生索引失效
再看等值情况下,group_id索引排序生效,但是num不生效,这里还是最左原则,索引树中二级索引,period、group_id、num是这样一个顺序,period相同,下一个排序的字段就是group_id,自然轮不到num。
6.1.6 升降序不同,无法利用索引排序
下面是索引失效的情况,这个了解B+Tree的查询逻辑,就明白了为什么升降序,索引会失效。
6.2 group by
group by默认为分组排序,这个就会耗费性能,可以通过order by null,去掉这个默认排序,性能就上去了。
6.3 分页查询
针对数据量比较大的,id自增情况就不需要使用limit来分页,直接根据id取范围,但实际数据并不会那么连续,这种办法过于理想。
另外一种办法时根据子查询,定位偏移量,但这种办法却无法针对uuid情况。
7 查询成本计算
mysql有一套自己的计算逻辑,这个看上面B站连接的视频就可以了,我这里只需要了解结果就行,我们又不需要开发一个数据库。
如下图增加format=json,可以获取到更多的信息。mysq5.6之后有这个功能,但是mariadb增加了没有效果。
explain format=json select * from acc_voucher_15 accVoucher
where accVoucher.as_id = 112665 AND accVoucher.period >= '202107'
order by accVoucher.period ASC,accVoucher.group_id DESC;
结果如下query_cost就是计算出来的查询成本,查询成本越大就需要优化了,cost_info则是详细的成本计算结果信息。
{"query_block": {"select_id": 1,"cost_info": {"query_cost": "1.20"},"ordering_operation": {"using_filesort": true,"table": {"table_name": "accVoucher","access_type": "ref","possible_keys": ["PRIMARY","idx_adId_period_num"],"key": "PRIMARY","used_key_parts": ["as_id"],"key_length": "4","ref": ["const"],"rows_examined_per_scan": 1,"rows_produced_per_join": 0,"filtered": "33.33","cost_info": {"read_cost": "1.00","eval_cost": "0.07","prefix_cost": "1.20","data_read_per_join": "325"},"used_columns": ["id","as_id","group_id","num","period","v_date","attach_num","status","remark","approve_status","create_user","create_username","create_time","update_user","update_time","auditor","audit_time"],"attached_condition": "((`acc`.`accvoucher`.`as_id` <=> 112665) and (`acc`.`accvoucher`.`period` >= '202107'))"}}}
}