第23天SQL进阶-查询优化- performance_schema系列五:数据库对象事件与属性统计(SQL 小虚竹)

Posted 小虚竹

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了第23天SQL进阶-查询优化- performance_schema系列五:数据库对象事件与属性统计(SQL 小虚竹)相关的知识,希望对你有一定的参考价值。

回城传送–》《32天SQL筑基》

文章目录

零、前言

今天是学习 SQL 打卡的第 23 天,每天我会提供一篇文章供群成员阅读( 不需要订阅付钱 )。

希望大家先自己思考,如果实在没有想法,再看下面的解题思路,自己再实现一遍。在小虚竹JAVA社区 中对应的 【打卡贴】打卡,今天的任务就算完成了,养成每天学习打卡的好习惯。

​ 虚竹哥会组织大家一起学习同一篇文章,所以有什么问题都可以在群里问,群里的小伙伴可以迅速地帮到你,一个人可以走得很快,一群人可以走得很远,有一起学习交流的战友,是多么幸运的事情。

​ 我的学习策略很简单,题海策略+ 费曼学习法。如果能把这些题都认认真真自己实现一遍,那意味着 SQL 已经筑基成功了。后面的进阶学习,可以继续跟着我,一起走向架构师之路。

今天的学习内容是:SQL进阶-查询优化- performance_schema系列五:数据库对象事件与属性统计

本文内容有1万多字,内容非常多,建议阅读方式:记住标题和小模块的介绍 ,内容详解可帮助理解,看不懂也没事,后续章节的实战中也可慢慢体会。
本文适用于收藏,需要查阅指定配置时,可快速找到配置的详解。

一、练习题目

题目链接难度
--

二、SQL思路

SQL进阶-查询优化- performance_schema系列五:数据库对象事件与属性统计

经过昨天的performance_schema事件统计表的学习,今天学习更细粒度地进行分类统计,例如表的io开销,锁的开销,用户连接的一些属性统计等,这些就需要看数据库对象事件统计表与属性统计表。

数据库对象事件统计表

数据库表级别对象等待事件统计

按数据库对象名称(库名,表名等)进行统计等待事件。

统计数据存放在表objects_summary_global_by_type

 use performance_schema;

select * from objects_summary_global_by_type where SUM_TIMER_WAIT!=0;


简单分析下:
按照“studymysql”库的user_info_test表进行分组
统计了表相关的等待事件调用次数,总计、最小、平均、最大延迟时间信息,利用这些信息,我们可以大致了解InnoDB中表的访问效率排行统计情况,一定程度上反应了对存储引擎接口调用的效率。

表I/O等待和锁等待事件统计

与数据库表级别对象等待事件统计表类似,表I/O等待和锁等待事件统计信息更精细,细分了:细分了每个表的增删改查的执行次数,总等待时间,最小、最大、平均等待时间,甚至精细到某个索引的增删改查的等待时间。

有这么几张表:

show tables like '%table%summary%';

| table_io_waits_summary_by_index_usage | # 按照每个索引进行统计的表I/O等待事件
| table_io_waits_summary_by_table | # 按照每个表进行统计的表I/O等待事件
| table_lock_waits_summary_by_table | # 按照每个表进行统计的表锁等待事件

统计表统计内容查看
select * from table_io_waits_summary_by_index_usage where SUM_TIMER_WAIT!=0;

select * from table_io_waits_summary_by_table where SUM_TIMER_WAIT!=0;

select * from table_lock_waits_summary_by_table where SUM_TIMER_WAIT!=0;





这个统计表中的记录内容过长,大家可自行按上面提供的sql去看下完整效果。

如上所示:

  • table_io_waits_summary_by_table表是包含整个表的增删改查等待事件分类统计

  • table_io_waits_summary_by_index_usage区分了每个表的索引的增删改查等待事件分类统计

  • table_lock_waits_summary_by_table表统计增删改查对应的锁等待时间,而不是IO等待时间

对这三张表进行重点说明:

table_io_waits_summary_by_table表:

该表允许使用TRUNCATE TABLE语句。只将统计列重置为零,而不是删除行。对该表执行truncate还会隐式truncate table_io_waits_summary_by_index_usage表

table_io_waits_summary_by_index_usage表:

按照与table_io_waits_summary_by_table的分组列+INDEX_NAME列进行分组,INDEX_NAME有如下几种 :

  • 如果使用到了索引,则这里显示索引的名字,如果为PRIMARY,则表示表I/O使用到了主键索引

  • 如果值为NULL,则表示表I/O没有使用到索引

  • 如果是插入操作,则无法使用到索引,此时的统计值是按照INDEX_NAME = NULL计算的
    该表允许使用TRUNCATE TABLE语句。只将统计列重置为零,而不是删除行。

使用DDL语句更改索引结构时,会导致该表的所有索引统计信息被重置。

table_lock_waits_summary_by_table表:

该表的分组列与table_io_waits_summary_by_table表相同
该表包含有关内部和外部锁的信息:

  • 内部锁对应SQL层中的锁。是通过调用thr_lock()函数来实现的。

  • 外部锁对应存储引擎层中的锁。通过调用handler::external_lock()函数来实现。
    该表允许使用TRUNCATE TABLE语句。只将统计列重置为零,而不是删除行。

文件I/O事件统计

文件I/O事件统计表只记录等待事件中的IO事件(不包含table和socket子类别),有这么几张表:

 show tables like '%file_summary%';

file_summary_by_event_name:按照每个事件名称进行统计的文件IO等待事件
file_summary_by_instance:按照每个文件实例(对应具体的每个磁盘文件,例如:表sbtest1的表空间文件sbtest1.ibd)进行统计的文件IO等待事件

统计表统计内容查看
 select * from file_summary_by_event_name where SUM_TIMER_WAIT !=0 and EVENT_NAME like '%innodb%' limit 1;
select * from file_summary_by_instance where SUM_TIMER_WAIT!=0 and EVENT_NAME like '%innodb%' limit 1;



共同的统计字段说明:

  • COUNT_STAR,SUM_TIMER_WAIT,MIN_TIMER_WAIT,AVG_TIMER_WAIT,MAX_TIMER_WAIT:这些列统计所有I/O操作数量和操作时间 ;
  • COUNT_READ,SUM_TIMER_READ,MIN_TIMER_READ,AVG_TIMER_READ,MAX_TIMER_READ,SUM_NUMBER_OF_BYTES_READ:这些列统计了所有文件读取操作,包括FGETS,FGETC,FREAD和READ系统调用,还包含了这些I/O操作的数据字节数 ;
  • COUNT_WRITE,SUM_TIMER_WRITE,MIN_TIMER_WRITE,AVG_TIMER_WRITE,MAX_TIMER_WRITE,SUM_NUMBER_OF_BYTES_WRITE:这些列统计了所有文件写操作,包括FPUTS,FPUTC,FPRINTF,VFPRINTF,FWRITE和PWRITE系统调用,还包含了这些I/O操作的数据字节数 ;
  • COUNT_MISC,SUM_TIMER_MISC,MIN_TIMER_MISC,AVG_TIMER_MISC,MAX_TIMER_MISC:这些列统计了所有其他文件I/O操作,包括CREATE,DELETE,OPEN,CLOSE,STREAM_OPEN,STREAM_CLOSE,SEEK,TELL,FLUSH,STAT,FSTAT,CHSIZE,RENAME和SYNC系统调用。注意:这些文件I/O操作没有字节计数信息。
  • 注:允许使用TRUNCATE TABLE语句。但只将统计列重置为零,而不是删除行。

套接字事件统计

套接字事件统计了套接字的读写调用次数和发送接收字节计数信息,有这么几张表:

show tables like '%socket%summary%';

socket_summary_by_instance:针对每个socket实例的所有 socket I/O操作,这些socket操作相关的操作次数、时间和发送接收字节信息由wait/io/socket/* instruments产生。但当连接中断时,在该表中对应socket连接的信息行将被删除(这里的socket是指的当前活跃的连接创建的socket实例)
socket_summary_by_event_name:针对每个socket I/O instruments,这些socket操作相关的操作次数、时间和发送接收字节信息由wait/io/socket/* instruments产生(这里的socket是指的当前活跃的连接创建的socket实例)

统计表统计内容查看
select * from socket_summary_by_event_name;

select * from socket_summary_by_instance where COUNT_STAR!=0;



共同的统计字段说明:

  • COUNT_STAR,SUM_TIMER_WAIT,MIN_TIMER_WAIT,AVG_TIMER_WAIT,MAX_TIMER_WAIT:这些列统计所有socket读写操作的次数和时间信息

  • COUNT_READ,SUM_TIMER_READ,MIN_TIMER_READ,AVG_TIMER_READ,MAX_TIMER_READ,SUM_NUMBER_OF_BYTES_READ:这些列统计所有接收操作(socket的RECV、RECVFROM、RECVMS类型操作,即以server为参照的socket读取数据的操作)相关的次数、时间、接收字节数等信息

  • COUNT_WRITE,SUM_TIMER_WRITE,MIN_TIMER_WRITE,AVG_TIMER_WRITE,MAX_TIMER_WRITE,SUM_NUMBER_OF_BYTES_WRITE:这些列统计了所有发送操作(socket的SEND、SENDTO、SENDMSG类型操作,即以server为参照的socket写入数据的操作)相关的次数、时间、接收字节数等信息

  • COUNT_MISC,SUM_TIMER_MISC,MIN_TIMER_MISC,AVG_TIMER_MISC,MAX_TIMER_MISC:这些列统计了所有其他套接字操作,如socket的CONNECT、LISTEN,ACCEPT、CLOSE、SHUTDOWN类型操作。注意:这些操作没有字节计数

  • 注:允许使用TRUNCATE TABLE语句(除events_statements_summary_by_digest之外),只将统计列重置为零,而不是删除行

prepare语句实例统计表

什么是prepare语句?prepare语句实际上就是一个预编译语句,先把SQL语句进行编译,且可以设定参数占位符(例如:?符号),然后调用时通过用户变量传入具体的参数值。prepare语句有三个步骤,预编译prepare语句,执行prepare语句,释放销毁prepare语句。
performance_schema提供了针对prepare语句的监控记录。

统计表统计内容查看
select * from prepared_statements_instances;

没数据,看不了字段。难不了虚竹哥,顺便教下大家如何使用准备语句。

实战
 PREPARE stmt1 FROM 'SELECT SQRT(POW(?,2) + POW(?,2)) AS hypotenuse';
SET @a = 3;
SET @b = 4;
EXECUTE stmt1 USING @a, @b;

第一步,使用PREPARE语句准备执行语句。该语句用于在给定了两个边的长度时,计算三角形的斜边。
第二步,声明了两个变量@a和@b;
第三步:第三,使用EXECUTE语句来执行变量@a和@b的准备语句。
这时我们就能看到统计信息了。

第四,我们使用DEALLOCATE PREPARE来释放资源。

DEALLOCATE PREPARE stmt1;



如上所示:

  • prepare语句预编译:创建一个prepare语句。如果语句检测成功,则会在prepared_statements_instances表中新添加一行。
  • prepare语句执行:检测执行了EXECUTE语句,会更新prepare_statements_instances表中对应的行信息。
  • prepare语句解除资源分配:检测的prepare语句实例执行COM_STMT_CLOSE或SQLCOM_DEALLOCATE_PREPARE命令,同时将删除prepare_statements_instances表中对应的行信息。为了避免资源泄漏,请务必在prepare语句不需要使用的时候执行此步骤释放资源。
字段说明
  • OBJECT_INSTANCE_BEGIN:prepare语句事件的instruments 实例内存地址。

  • STATEMENT_ID:由server分配的语句内部ID。文本和二进制协议都使用该语句ID。

  • STATEMENT_NAME:对于二进制协议的语句事件,此列值为NULL。对于文本协议的语句事件,此列值是用户分配的外部语句名称。例如:PREPARE stmt FROM’SELECT 1’;,语句名称为stmt。

  • SQL_TEXT:prepare的语句文本,带“?”的表示是占位符标记,后续execute语句可以对该标记进行传参。

  • OWNER_THREAD_ID,OWNER_EVENT_ID:这些列表示创建prepare语句的线程ID和事件ID。

  • OWNER_OBJECT_TYPE,OWNER_OBJECT_SCHEMA,OWNER_OBJECT_NAME:对于由客户端会话使用SQL语句直接创建的prepare语句,这些列值为NULL。对于由存储程序创建的prepare语句,这些列值显示相关存储程序的信息。如果用户在存储程序中忘记释放prepare语句,那么这些列可用于查找这些未释放的prepare对应的存储程序,使用语句查询:

SELECT OWNER_OBJECT_TYPE,OWNER_OBJECT_SCHEMA,OWNER_OBJECT_NAME,STATEMENT_NAME,SQL_TEXT FROM performance_schema.prepared_statemments_instances WHERE OWNER_OBJECT_TYPE IS NOT NULL;

  • TIMER_PREPARE:执行prepare语句本身消耗的时间。
  • COUNT_REPREPARE:该行信息对应的prepare语句在内部被重新编译的次数,重新编译prepare语句之后,之前的相关统计信息就不可用了,因为这些统计信息是作为语句执行的一部分被聚合到表中的,而不是单独维护的。
  • COUNT_EXECUTE,SUM_TIMER_EXECUTE,MIN_TIMER_EXECUTE,AVG_TIMER_EXECUTE,MAX_TIMER_EXECUTE:执行prepare语句时的相关统计数据。
  • SUM_xxx:其余的SUM_xxx开头的列与语句统计表中的信息相同
  • 注:允许执行TRUNCATE TABLE语句,但是TRUNCATE TABLE只是重置prepared_statements_instances表的统计信息列,但是不会删除该表中的记录,该表中的记录会在prepare对象被销毁释放的时候自动删除。

锁对象记录表

performance_schema通过如下表来记录相关的锁信息:

  • metadata_locks:元数据锁的持有和请求记录;
  • table_handles:表锁的持有和请求记录。
metadata_locks表

Performance Schema通过metadata_locks表记录元数据锁信息:

  • 已授予的锁(显示哪些会话拥有当前元数据锁);
  • 已请求但未授予的锁(显示哪些会话正在等待哪些元数据锁);
  • 已被死锁检测器检测到并被杀死的锁,或者锁请求超时正在等待锁请求会话被丢弃。

metadata_locks表是只读的,无法更新。默认保留行数会自动调整,如果要配置该表大小,可以在server启动之前设置系统变量performance_schema_max_metadata_locks的值。

统计表统计内容查看
select * from metadata_locks;


字段说明:

  • OBJECT_TYPE:元数据锁子系统中使用的锁类型(类似setup_objects表中的OBJECT_TYPE列值):有效值为:GLOBAL、SCHEMA、TABLE、FUNCTION、PROCEDURE、TRIGGER(当前未使用)、EVENT、COMMIT、USER LEVEL LOCK、TABLESPACE、LOCKING SERVICE,USER LEVEL LOCK值表示该锁是使用GET_LOCK()函数获取的锁。LOCKING SERVICE值表示使用锁服务获取的锁;
  • OBJECT_SCHEMA:该锁来自于哪个库级别的对象;
  • OBJECT_NAME:instruments对象的名称,表级别对象;
  • OBJECT_INSTANCE_BEGIN:instruments对象的内存地址;
  • LOCK_TYPE:元数据锁子系统中的锁类型。有效值为:INTENTION_EXCLUSIVE、SHARED、SHARED_HIGH_PRIO、SHARED_READ、SHARED_WRITE、SHARED_UPGRADABLE、SHARED_NO_WRITE、SHARED_NO_READ_WRITE、EXCLUSIVE;
  • LOCK_DURATION:来自元数据锁子系统中的锁定时间。有效值为:STATEMENT、TRANSACTION、EXPLICIT,STATEMENT和TRANSACTION值分别表示在语句或事务结束时会释放的锁。 EXPLICIT值表示可以在语句或事务结束时被会保留,需要显式释放的锁,例如:使用FLUSH TABLES WITH READ LOCK获取的全局锁;
  • LOCK_STATUS:元数据锁子系统的锁状态。有效值为:PENDING、GRANTED、VICTIM、TIMEOUT、KILLED、PRE_ACQUIRE_NOTIFY、POST_RELEASE_NOTIFY。performance_schema根据不同的阶段更改锁状态为这些值;
  • SOURCE:源文件的名称,其中包含生成事件信息的检测代码行号;
  • OWNER_THREAD_ID:请求元数据锁的线程ID;
  • OWNER_EVENT_ID:请求元数据锁的事件ID。

performance_schema如何管理metadata_locks表中记录的内容(使用LOCK_STATUS列来表示每个锁的状态):

  • 当请求立即获取元数据锁时,将插入状态为GRANTED的锁信息行;
  • 当请求元数据锁不能立即获得时,将插入状态为PENDING的锁信息行;
  • 当之前请求不能立即获得的锁在这之后被授予时,其锁信息行状态更新为GRANTED;
  • 释放元数据锁时,对应的锁信息行被删除;
  • 当一个pending状态的锁被死锁检测器检测并选定为用于打破死锁时,这个锁会被撤销,并返回错误信息(ER_LOCK_DEADLOCK)给请求锁的会话,锁状态从PENDING更新为VICTIM;
  • 当待处理的锁请求超时,会返回错误信息(ER_LOCK_WAIT_TIMEOUT)给请求锁的会话,锁状态从PENDING更新为TIMEOUT;
  • 当已授予的锁或挂起的锁请求被杀死时,其锁状态从GRANTED或PENDING更新为KILLED;
  • VICTIM,TIMEOUT和KILLED状态值停留时间很简短,当一个锁处于这个状态时,那么表示该锁行信息即将被删除(手动执行SQL可能因为时间原因查看不到,可以使用程序抓取);
  • PRE_ACQUIRE_NOTIFY和POST_RELEASE_NOTIFY状态值停留事件都很简短,当一个锁处于这个状态时,那么表示元数据锁子系统正在通知相关的存储引擎该锁正在执行分配或释。这些状态值在5.7.11版本中新增。
  • 注:metadata_locks表不允许使用TRUNCATE TABLE语句。
table_handles表

performance_schema通过table_handles表记录表锁信息,以对当前每个打开的表所持有的表锁进行追踪记录。table_handles输出表锁instruments采集的内容。这些信息显示server中已打开了哪些表,锁定方式是什么以及被哪个会话持有。

table_handles表是只读的,不能更新。默认自动调整表数据行大小,如果要显式指定个,可以在server启动之前设置系统变量performance_schema_max_table_handles的值。

统计表统计内容查看
select * from table_handles;


字段说明:

  • OBJECT_TYPE:显示handles锁的类型,表示该表是被哪个table handles打开的;
  • OBJECT_SCHEMA:该锁来自于哪个库级别的对象;
  • OBJECT_NAME:instruments对象的名称,表级别对象;
  • OBJECT_INSTANCE_BEGIN:instruments对象的内存地址;
  • OWNER_THREAD_ID:持有该handles锁的线程ID;
  • OWNER_EVENT_ID:触发table handles被打开的事件ID,即持有该handles锁的事件ID
  • INTERNAL_LOCK:在SQL级别使用的表锁。有效值为:READ、READ WITH SHARED LOCKS、READ HIGH PRIORITY、READ NO INSERT、WRITE ALLOW WRITE、WRITE CONCURRENT INSERT、WRITE LOW PRIORITY、WRITE。
  • EXTERNAL_LOCK:在存储引擎级别使用的表锁。有效值为:READ EXTERNAL、WRITE EXTERNAL。
  • 注:table_handles表不允许使用TRUNCATE TABLE语句。

属性统计表

连接信息统计表

当客户端连接到MySQL server时,它的用户名和主机名都是特定的。performance_schema按照帐号、主机、用户名对这些连接的统计信息进行分类并保存到各个分类的连接信息表中,如下:

  • accounts:按照user@host的形式来对每个客户端的连接进行统计;
  • hosts:按照host名称对每个客户端连接进行统计;
  • users:按照用户名对每个客户端连接进行统计。
accounts表

accounts表包含连接到MySQL server的每个account的记录。对于每个帐户,每个user+host唯一标识一行,每行单独计算该帐号的当前连接数和总连接数。server启动时,表的大小会自动调整。要显式设置表大小,可以在server启动之前设置系统变量performance_schema_accounts_size的值。该系统变量设置为0时,表示禁用accounts表的统计信息功能。

select * from accounts;


字段说明:

  • USER:某连接的客户端用户名。如果是一个内部线程创建的连接,或者是无法验证的用户创建的连接,则该字段为NULL;
  • HOST:某连接的客户端主机名。如果是一个内部线程创建的连接,或者是无法验证的用户创建的连接,则该字段为NULL;
  • CURRENT_CONNECTIONS:某帐号的当前连接数;
  • TOTAL_CONNECTIONS:某帐号的总连接数(新增加一个连接累计一个,不会像当前连接数那样连接断开会减少)。
users表

users表包含连接到MySQL server的每个用户的连接信息,每个用户一行。该表将针对用户名作为唯一标识进行统计当前连接数和总连接数,server启动时,表的大小会自动调整。 要显式设置该表大小,可以在server启动之前设置系统变量performance_schema_users_size的值。该变量设置为0时表示禁用users统计信息。

select * from users;


字段说明:

  • USER:某个连接的用户名,如果是一个内部线程创建的连接,或者是无法验证的用户创建的连接,则该字段为NULL;
  • CURRENT_CONNECTIONS:某用户的当前连接数;
  • TOTAL_CONNECTIONS:某用户的总连接数。
hosts表

hosts表包含客户端连接到MySQL server的主机信息,一个主机名对应一行记录,该表针对主机作为唯一标识进行统计当前连接数和总连接数。server启动时,表的大小会自动调整。 要显式设置该表大小,可以在server启动之前设置系统变量performance_schema_hosts_size的值。如果该变量设置为0,则表示禁用hosts表统计信息。

select * from hosts;


字段说明:

  • HOST:某个连接的主机名,如果是一个内部线程创建的连接,或者是无法验证的用户创建的连接,则该字段为NULL;
  • CURRENT_CONNECTIONS:某主机的当前连接数;
  • TOTAL_CONNECTIONS:某主机的总连接数。

连接属性统计表

应用程序可以使用一些键/值对生成一些连接属性,在对mysql server创建连接时传递给server。对于C API,使用mysql_options()和mysql_options4()函数定义属性集。其他MySQL连接器可以使用一些自定义连接属性方法。
连接属性记录在如下两张表中:

  • session_account_connect_attrs:记录当前会话及其相关联的其他会话的连接属性;
  • session_connect_attrs:所有会话的连接属性。
session_account_connect_attrs表

应用程序可以使用mysql_options()和mysql_options4()C API函数在连接时提供一些要传递到server的键值对连接属性。
session_account_connect_attrs表仅包含当前连接及其相关联的其他连接的连接属性。要查看所有会话的连接属性,请查看session_connect_attrs表。

select * from session_account_connect_attrs;


字段说明:

  • PROCESSLIST_ID:会话的连接标识符,与show processlist结果中的ID字段相同;
  • ATTR_NAME:连接属性名称;
  • ATTR_VALUE:连接属性值;
  • ORDINAL_POSITION:将连接属性添加到连接属性集的顺序。
  • session_account_connect_attrs表不允许使用TRUNCATE TABLE语句。
session_connect_attrs表

表字段含义与session_account_connect_attrs表相同,但是该表是保存所有连接的连接属性表。

 select * from session_connect_attrs;


表字段含义与session_account_connect_attrs表字段含义相同。

总结

本文讲解了performance_schema中数据库对象事件与属性统计,详细介绍了数据库对象事件统计表中的:数据库表级别对象等待事件统计表、表I/O等待和锁等待事件统计表、文件I/O事件统计表、套接字事件统计表、prepare语句实例统计表和锁对象记录表。
还介绍了连接信息统计和连接属性统计这两种属性统计表。
详细介绍了这些统计表的字段说明和一些注意事项。

本文内容有1万多字,内容非常多,建议阅读方式:记住标题和小模块的介绍 ,内容详解可帮助理解,看不懂也没事,后续章节的实战中也可慢慢体会。
本文适用于收藏,需要查阅指定配置时,可快速找到配置的详解。

四、参考

数据库对象事件与属性统计 | performance_schema全方位介绍

我是虚竹哥,我们明天见~

以上是关于第23天SQL进阶-查询优化- performance_schema系列五:数据库对象事件与属性统计(SQL 小虚竹)的主要内容,如果未能解决你的问题,请参考以下文章

第15天SQL进阶-查询优化-慢查询日志(SQL 小虚竹)

第17天SQL进阶-查询优化- SHOW STATUS(SQL 小虚竹)

第17天SQL进阶-查询优化- SHOW STATUS(SQL 小虚竹)

第16天SQL进阶-查询优化一定要学EXPALIN (SQL 小虚竹)

第16天SQL进阶-查询优化一定要学EXPALIN (SQL 小虚竹)

第16天SQL进阶-查询优化一定要学EXPALIN (SQL 小虚竹)