mysql> select * from test_20221120;
Empty set (0.00 sec)
mysql> set profiling=1;
mysql> show profiles;
+----------+------------+-----------------------------+
| Query_ID | Duration | Query |
+----------+------------+-----------------------------+
| 1 | 0.00043925 | select * from test_20221120 |
+----------+------------+-----------------------------+
1 row in set, 1 warning (0.00 sec)
show PROFILE;
Status | Duration |
---|---|
starting | 0.000087 |
checking permissions | 0.000008 |
Opening tables | 0.000023 |
init | 0.000021 |
System lock | 0.000010 |
optimizing | 0.000005 |
statistics | 0.000016 |
preparing | 0.000013 |
executing | 0.000004 |
Sending data | 0.000038 |
end | 0.000004 |
query end | 0.000008 |
closing tables | 0.000008 |
freeing items | 0.000029 |
cleaning up | 0.000015 |
SHOW PROFILE [type [, type] ... ]
[FOR QUERY n]
[LIMIT row_count [OFFSET offset]]
type: {
ALL:显示所有信息
| BLOCK IO: 显示块IO操作的次数
| CONTEXT SWITCHES : 上下文切换次数,被动和主动;
| CPU:显示用户CPU时间、系统CPU时间
| IPC:发送和接受的消息数量
| MEMORY:暂未实现
| PAGE FAULTS:显示页错误数量
| SOURCE:显示源码中的函数名与名称
| SWAPS:显示swap的次数
}
mysql> SELECT @@profiling;
+-------------+
| @@profiling |
+-------------+
| 0 |
+-------------+
1 row in set (0.00 sec)
mysql> SET profiling = 1;
Query OK, 0 rows affected (0.00 sec)
mysql> DROP TABLE IF EXISTS t1;
Query OK, 0 rows affected, 1 warning (0.00 sec)
mysql> CREATE TABLE T1 (id INT);
Query OK, 0 rows affected (0.01 sec)
mysql> SHOW PROFILES;
+----------+----------+--------------------------+
| Query_ID | Duration | Query |
+----------+----------+--------------------------+
| 0 | 0.000088 | SET PROFILING = 1 |
| 1 | 0.000136 | DROP TABLE IF EXISTS t1 |
| 2 | 0.011947 | CREATE TABLE t1 (id INT) |
+----------+----------+--------------------------+
3 rows in set (0.00 sec)
mysql> SHOW PROFILE;
+----------------------+----------+
| Status | Duration |
+----------------------+----------+
| checking permissions | 0.000040 |
| creating table | 0.000056 |
| After create | 0.011363 |
| query end | 0.000375 |
| freeing items | 0.000089 |
| logging slow query | 0.000019 |
| cleaning up | 0.000005 |
+----------------------+----------+
7 rows in set (0.00 sec)
mysql> SHOW PROFILE FOR QUERY 1;
+--------------------+----------+
| Status | Duration |
+--------------------+----------+
| query end | 0.000107 |
| freeing items | 0.000008 |
| logging slow query | 0.000015 |
| cleaning up | 0.000006 |
+--------------------+----------+
4 rows in set (0.00 sec)
mysql> SHOW PROFILE CPU FOR QUERY 2;
+----------------------+----------+----------+------------+
| Status | Duration | CPU_user | CPU_system |
+----------------------+----------+----------+------------+
| checking permissions | 0.000040 | 0.000038 | 0.000002 |
| creating table | 0.000056 | 0.000028 | 0.000028 |
| After create | 0.011363 | 0.000217 | 0.001571 |
| query end | 0.000375 | 0.000013 | 0.000028 |
| freeing items | 0.000089 | 0.000010 | 0.000014 |
| logging slow query | 0.000019 | 0.000009 | 0.000010 |
| cleaning up | 0.000005 | 0.000003 | 0.000002 |
+----------------------+----------+----------+------------+
7 rows in set (0.00 sec)
SHOW PROFILE FOR QUERY 2;
SELECT STATE, FORMAT(DURATION, 6) AS DURATION
FROM INFORMATION_SCHEMA.PROFILING
WHERE QUERY_ID = 2 ORDER BY SEQ;
关于
performance_schema
有两个基本概念
performance_schema表的分类
等待时间记录表,与语句事件类型的相关记录表,类似于:
show tables like '%wait%';
阶段事件记录表,记录语句执行的阶段事件表
show tables like '%stage%';
事务事件记录表,记录事务相关的事件表
show tables like '%transaction%';
监控文件系统层调用的表
show tables like '%file%';
监视内存使用的表
show tables like '%memory%';
动态对performance_schema
进行配置的配置表
show tables like ''%setup%;
performance_schema
的简单配置与使用数据库刚刚初始化并启动的时候,并不是所有的
instruments
信息都会被采集。
select * from setup_instruments;
setup_instruments
配置表中对应的采集配置项UPDATE setup_instruments SET ENABLED ='YES' ,TIMED ='YES' WHERE NAME LIKE 'wait%';
UPDATE setup_consumers SET ENABLED ='YES' WHERE NAME LIKE 'wait'
events_waits_current
表来得知,该表中每个线程只包含一行数据,适用于显示每个线程的最新监视事件。select * from events_waits_current\G
SELECT DIGEST_TEXT,COUNT_STAR,FIRST_SEEN,LAST_SEEN FROM
events_statements_summary_by_digest ORDER BY COUNT_STAR DESC
SELECT DIGEST_TEXT,AVG_TIMER_WAIT FROM
events_statements_summary_by_digest ORDER BY COUNT_STAR DESC
SELECT DIGEST_TEXT,SUM_SORT_ROWS FROM
events_statements_summary_by_digest ORDER BY COUNT_STAR DESC
SELECT DIGEST_TEXT,SUM_ROWS_SENT FROM
events_statements_summary_by_digest ORDER BY COUNT_STAR DESC
show processlist
应该尽量使用可以正确存储数据的最小数据类型,更小的数据类型通常更快,因为它们占用磁盘、内存和CPU更少缓存,并且处理时需要的CPU周期更少,但是要确保没有低估需要存储的值的范围,如果无法确认哪个数据类型,就选择你认为不会超过范围的最小类型。
案例:
在工作中会涉及到存储状态1、0,在选择类型的时候我们一般默认选择为INT,这是不对的。不同的类型的数据空间是不一样的。Mysql中int类型有TINYINT
、SMALLINT
、MEDIUMINT
、INT
、BIGINT
mysql的数据类型有哪些?
简单的数据类型的操作通常需要更少的CPU周期,比如:
- 整型比字符操作代价更低,因为字符集和校对规则是字符比较比整型比较更复杂。
- 使用mysql自建类型而不是字符串来存储日期和时间。
- 使用整型存储IP地址。
SELECT INET_ATON('192.189.190.101'); SELECT INET_NTOA(3233660517); 输出结果: INET_ATON('192.189.190.101') 3233660517 SELECT INET_NTOA(3233660517); 192.189.190.101
在数据中
null
不等于null
;
如果查询中包含可为NULL的列,对mysql来说很难优化,因为可为null的列使得索引、索引统计和值比较都更加复杂,坦白来说,通常情况下null列改为not null带来的性能提升比较小,所以没有必要将所有的表的schema进行修改,但是应该尽量避免设计成为null的列。空字符串''
是可以的(使用默认值时候请谨慎)
可以使用的几种整数类型:TINYINT、SMALLINT、MEDIUMINT、INT、BIGINT分别使用8、16、24、32、64位存储空间。尽量使用满足需求的最小数据类型。
MySQL把每个BLOB和TEXT值当作一个独立的对象处理。
两者都是为了存储很大数据而涉及的字符串类型,分别使用二进制和字符方式存储。
- 不要使用字符串来存储日期和时间数据
- 日期时间类型通常比字符串占用的存储空间更小
- 日期时间类型在进行查找过滤时可以利用日期来进行比较
- 日期类型还有着丰富的处理函数,可以方便的对事件类型进行日期计算
- 使用int 存储日期事件不如使用timestamp类型
MySQL存储枚举类型会非常紧凑,会根据列表值的数据压缩到一个或者两个字节中,MySQL在内部会将每个值在列表中的而为之保存为整数,并且在表的.frm文件中保存"数字·字符串"映射关系的查找表
CREATE TABLE `insert_20221123_5` ( `id` char(3) DEFAULT NULL, `gender` enum('FEMAL','MALE') DEFAULT NULL, `name` varchar(3) DEFAULT NULL, `realname` char(3) DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8
存IP推荐使用整型,IP的本质是32位无符号整型,
INET_ATON()
和INET_NTOA()
第一范式(1NF):属性不可分割,即每个属性都是不可分割的原子项。(实体的属性即表中的列)
第二范式(2NF):满足第一范式;且不存在部分依赖,即非主属性必须完全依赖于主属性。(主属性即主键;完全依赖是针对于联合主键的情况,非主键列不能只依赖于主键的一部分)
第三范式(3NF):满足第二范式;且不存在传递依赖,即非主属性不能与非主属性之间有依赖关系,非主属性必须直接依赖于主属性,不能间接依赖主属性。(A -> B, B ->C, A -> C)
在企业中很难做好严格意义上的范式或反范式,一般需要混合使用
在一个网站实例中,这个网站,允许用户发送消息,并且一些用户是付费用户。现在想看付费用户最近的10条信息。在user表和message表中都存储用户类型(account_type)而不用完全的反范式化。这避免了完全反范式化的删除和插入问题,即使没有message表的数据被删除也不会丢失用户信息。这样也不会把user_message表搞的太大,有利于高效地获取数据。
另一个从父表冗余一些数据到子表的理由是排序的需要
示例
代理主键
与业务无关,无意义的数字序列
自然主键
事物属性中的自然唯一标识
推荐使用代理主键
他们不与业务耦合,因此更容易维护;
一个大多数表,最好是全部表,通用的键策略能减少需要编写的代码源数量,减少系统的总体拥有成本。
纯拉丁字符能表示的内容,没必要选择
latin1
之外的其它字符编码,因为这会节省大量的存储空间。utf-8下,1字符=3字节。(uft-8也称之为utf-8mb3) utf-8mb4下,1字符=4字节;latin1 1个字符是1字节。
如果我们可以确定不需要存放多种语言,就没必要非得使用UTF8或其他UNICODE字符类型,会造成大量的存储空间浪费。
MySQL的数据类型可以精确到字段,所以当我们需要大型数据库中粗放多字节数据的时候,可以通过对不同表不同字段使用不同的数据类型来较大程度减少数据库存储量,进而降低IO操作次数并提高缓存命中率。
中文使用
utf8mb4
;存储引擎的选择
存储引擎:数据文件的组织形式
MyISAM | InnoDB | |
---|---|---|
索引类型 | 非聚簇索引 | 聚簇索引(数据文件和索引文件放在一起) |
支持事务 | 否 | 是 |
支持表锁 | 是 | 是 |
支持行锁 | 否 | 是 |
支持外键 | 否 | 是 |
支持全文检索 | 是 | 是(5.6后支持) |
适合操作类型 | 大量SELECT | 大量INSERT、DELETE、UPDATE |
被频繁引用且只能通过
Jion
2张(或者更多)打标的方式才能得到的独立小字段
这样的场景由于每次
Join
仅仅只是为了取得某个小字段的值,Join
到的记录又大,会造成大量不必要的IO,完全可以通过空间换取时间的方式来优化。不过,冗余的同时需要确保数据的一致性不会遭到破坏,确保更新的同时冗余字段也被更新。适当拆分
当我们的表中存在类似于TEXT或者是很大的VARCHAR类型的大字段的时候,如果我们大部分访问这张表的时候都用不到这个字段,我们就该义无反顾的将其拆分到另外独立的表中,以减少常用数据所占用的存储空间。这样做的一个明显好处就是每个数据块中可以存储的数据条数可以大大增加,既减少物理IO次数,也能大大提高内存中的缓存命中率。
MySQL执行计划
Column | JSON Name | Meaning |
---|---|---|
id | select_id | The SELECT identifier |
select_type | None The | SELECT type |
table | table_name | The table for the output row |
partitions | partitions | The matching partitions |
type | access_type | The join type |
possible_keys | possible_keys | The possible indexes to choose |
key | key | The index actually chosen |
key_len | key_length | The length of the chosen key |
ref | ref | The columns compared to the index |
rows | rows | Estimate of rows to be examined |
filtered | filtered | Percentage of rows filtered by table condition |
Extra | None | Additional information |
SELECT查询的序列号,包含一组数字,表示查询中执行SELECT子句或者操作表的顺序;
CREATE TABLE person_20221125 (
id char(3) DEFAULT NULL,
user_name varchar(3) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
CREATE TABLE order_20221125 (
id char(3) DEFAULT NULL,
user_id char(3) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
如果id相同,那么执行顺序从上到下
EXPLAIN SELECT * FROM person_20221125 p
left join order_20221125 o on p.id = o.user_id
如果id不同,如果是子查询,id的序号会递增,id值越大,优先级越高,越先被执行
-- create index person_id_idx on person_20221125(id);
-- create index order_id_idx on order_20221125(id);
EXPLAIN SELECT * FROM
person_20221125 p
where p.id in
(select user_id from order_20221125 where id ='1')
id相同和不同的同时存在,相同的认为是一组,从上往下执行,在所有组中,id值越大,优先级越高,越先执行
主要用来分辨查询的类型,是普通查询还是联合查询。
访问类型,SQL 查询优化中一个很重要的指标,结果值从好到坏依次是:system > const > eq_ref > ref > range > index > ALL。
系统表,少量数据,往往不需要进行磁盘IO
常量连接
主键索引(primary key)或者非空唯一索引(unique not null)等值扫描
非主键非唯一索引等值扫描
索引上的范围扫描
索引上的全扫描
count(*)
全表扫描(full table scan)
包含额外的信息
说明mysql无法利用索引进行排序,只能利用排序算法进行排序,会消耗额外的位置
建立临时表来保存中间结果,查询完成之后把临时表删除
这个表示当前的查询是覆盖索引的,直接从索引中读取数据,而不用访问数据表。如果同时出现
useing where
表明索引被用来执行索引键值的查找,如果没有,表面索引被用来读取数据,而不是真的查找
使用where进行条件过滤
使用连接缓存
where语句的结果总是false;
实例图说明:
每个节点占用一个磁盘块,一个节点上有两个升序排序的关键字和三个指向子树根节点的指针,指针存储的是子节点所在磁盘块的地址。两个关键词划分成的三个范围域对应三个指针指向的子树的数据域。以根节点为例,关键字为16和34,P1指针指向的子树的数据范围为小于16,P2指针指向的子树的数据范围为16~34,P3指针指向的子树的数据范围大于34。
索引的优点
order by
是全排序,效率很低
索引的用处
索引的分类
面试技术名词
在InnoDB中默认会为主键创建索引,更多的情况是为普通列创建索引。
- InnoDB是通过B+Tree结构对主键创建索引,然后叶子节点中存储记录,如果没有主键,那么会选择唯一键,如果没有唯一键,那么会生成一个6位的row_id来作为主键。
- 如果创建索引的键是其他字段,那么在叶子节点中存储的是该记录的主键,然后通过主键索引找到对应的记录叫做回表。
select的数据列只用从索引中就能够取得,不必读取数据行,换句话说查询列要被所建的索引覆盖。
最左匹配原则
索引合并
索引采用的数据结构
索引匹配方式
create table staffs(
id int primary key auto_increment,
name varchar(24) not null default '' comment '姓名',
age int not null default 0 comment '年龄',
pos varchar(20) not null default '' comment '职位',
add_time timestamp not null default current_timestamp comment '入职时间',
) charset utf8 comment '员工记录表';
alter table staffs add index idx_nap(name,age,pos)
全值匹配值得是索引中的所有列进行匹配
explain select * from staffs where name ='Mike' and age =30 and pos='dev'
匹配最左前缀
只匹配前面的几列
explain select * from staffs where name ='Tom' and age =30
explain select * from staffs where name ='Tom'
匹配列前缀
可以匹配某一列的值的开头部分
explain select * from staff where name like 'T%'
explain select * from staff where name like '%om'
匹配范围值
可以查找某一范围的数据
explain select * from staffs where age > 30
精确匹配某一列范围匹配另一列
查询第一列的全部和第二列的部分
explain select * from staffs where name ='Tom' and age >=30
字段类型不一致的时候会导致不使用索引
explain select * from staffs where name ='del' and age = 20 and pos=20;
只访问索引的查询
查询的时候只需要访问索引,不需要访问数据行,本质上就是覆盖索引
explain select name,age,pos from staffs where name ='Mike' and age =30 and pos='dev'
Extra
不为空则说明使用了索引覆盖。
聚簇索引
不是单独的索引类型,而是一种数据存储方式,指的是数据行跟相邻的键值紧凑的存储在一起
- 优点
- 可以把相关数据保存到一起。
- 数据访问更快,因为索引和数据保存在同一个树中。
- 使用覆盖索引扫描的查询可以直接使用页节点中的主键值。
- 缺点
- 聚簇索引最大限度地提高了IO密集型应用的性能,如果数据全部在内存,那么聚簇索引就没什么优势 。
- 插入速度严重依赖插入顺序,按照主键的顺序插入是最快的方式。
- 更新聚簇索引列的代价很高,因为会强制将每个被更新的行移动到新的位置。
- 基于聚簇索引的表在插入新行,或者主键被更新导致需要移动行的时候,可能面临页分裂的问题。
- 聚簇索引可能导致全表扫描变慢,尤其是行比较稀疏,或者是由于页分裂导致数据存储不连续的时候。
非聚簇索引
数据文件根索引文件分开存放
select actor_id from actor where act_id+1 = 5
有时候需要索引很长的字符串,这会让索引变的大且慢,通常情况下可以使用某个列开始的部分字符串,这样大大节约索引空间,从而提高索引效率,但这会降低索引的选择性,索引的选择性是指不重复的索引值和数据表记录总数的比值,范围从1/#T到1之间。索引的选择性越高则查询 效率越高,因为更高的索引可以让MySQL在查找的时候过滤掉更多的行。
一般情况下某个列前缀的选择性也是足够高的,足以满足查询的性能,但是对应BLOB,TEXT,VARCHAR类型的列,必须要使用前缀索引,因为mysql不允许索引这些列的完整长度,使用该方法的诀窍在于要选择足够长的前缀以保证较高的选择性,通过又不能太长。
- 案例演示
create table citydemo(city varchar(50) not null); insert into citydemo(city) select city from city;
重复执行5次下面的sql语句
insert into citydemo(city) select city from citydemo;
更新城市表的名称
update citydemo set city=(select city from city order by rand() limit 1);
查找最常见的城市列表,发现每个值都出现45-56次
select count(*) as cnt,city from citydemo group by city order by cnt desc limit 10;
查找最频繁出现的城市前缀,先从3个前缀字母开始,发现比原来出现的次数更多,可以分别截取多个字符串查看城市出现的次数
select count(*) as cnt,left(city,3) as pref from citydemo group by pref order by cnt desc limit 10; select count(*) as cnt,left(city,7) as pref from citydemo group by pref order by cnt desc limit 10;
还可以通过另外一种计算完整列的选择性,可以看到当前缀长度达到7之后,再增加前缀长度,选择性提升的幅度已经很小了
select count(distinct left(city,3))/count(*) as sel3, count(distinct left(city,4))/count(*) as sel4, count(distinct left(city,5))/count(*) as sel5, count(distinct left(city,6))/count(*) as sel6, count(distinct left(city,7))/count(*) as sel7, count(distinct left(city,8))/count(*) as sel8 from citydemo;
计算完成之后可以创建前缀索引
alter table citydemo add key(city(7));
注意:
前缀索引是一种能使索引更小更快的有效方法,但是也包含缺点:mysql无法使用前缀索引做order by和group by;查看索引
- 关于
Cardinality
基数:该列的唯一值(近似值)。
探索HyperLogLog算法
mysql有两种方式可以生成有序的结果:通过排序操作或者按索引顺序扫描,如果explain出来的type列的值为index,则说明mysql使用了索引扫描来排序;
扫描索引本身是很快的,因为只需要从一条索引记录移动到紧接着的下一条记录。但如果索引不能覆盖查询所需的全部列,那么就不得不每扫描一条记录就得回表查询一次对应的行,这基本都是随机IO,因此按索引顺序读取的速度通常要比顺序地全表扫描慢。
mysql可以使用同一个索引既满足排序,又用于查找行,如果可能的话,设计索引时是应该尽可能地满足这两种任务。
只有当索引的列顺序和order by子句的顺序完全一致,并且所有列的排序方式都一样时,MySQL才能够使用索引来对结果进行排序,如果插叙需要关联多张表,则只有当order by子句引用的字段全部为第一张表时,才能使用索引做排序。order by子句和查找型查询的限制是一样的,需要满足索引的最左前缀的要求,否则,MySQL都需要执行顺序操作,而无法利用索引排序。
explain select rental_id,staff_id from rental where rental_date='2005-05-25' order by inventory_id,customer_id;
order by子句不满足索引最左前缀的要求,也可以用于查询排序,这是因为你的第一列(
rental_date='2005-05-25'
)被指定为一个常数。
该查询为索引的第一列提供了常量条件,而使用第二列进行排序,将两个组合排序在一起,就形成了最左前缀。
explain select rental_id,staff_id from rental where rental_date='2005-05-25' order by inventory_id desc\G
explain select * from actor where actor_id =1
union all
select * from actor where actor_id =2;
explain select * from actor where actor_id in (1,2);
explain select * from actor where actor_id ='1' or actor_id = '2';
后两条SQL比较;
优先使用
in
,如果条件允许的话首先使用exists
范围条件是:
<
、<=
、>
、>=
、boolean
范围列可以用到索引,但是范维列后面的列无法用到索引,索引最多用于一个范围列;
create table user(
id int,
name varchar(10),
phone varchar(11)
);
alter table user add index user_idx_phone(phone);
insert into user values(1,'Tom','17787652467');
count(distinct(列名))/count(*)
来计算r为驱动表,s为匹配表,可以看到从r中分别取出每一个记录去匹配s表的列,然后再合并数据,对s表进行r表的行数次访问,对数据的开销比较大。
这个要求非驱动表(匹配表s)上有索引,可以通过索引来减少比较,加速查询。
在查询时,驱动表®会根据关联字段的索引进行查找,当在索引上找到符合的值,再回表进行查询,也就是只有当匹配到索引以后才会进行回表查询。
如果非驱动表(s)的关联键是主键的话,性能就会非常高,如果不是主键,要进行多次回表查询,先关联索引,然后再根据二级索引的主键ID进行回表操作,性能上索引比主键要慢。
如果有索引,会选取第二种方式进行join,但如果join列没有索引,就会采用Block Nested-Loop join。可以看到中间有个
join buffer
缓冲区,是将驱动表的所有join相关的列都先缓存到join buffer中,然后批量与匹配表进行匹配,将第一种多次比较合并为一次,降低了非驱动表(s)的访问频率。默认情况下join_buffer_size
=256K,在查找的时候Mysql会将所有的需要的列缓存到join buffer当中,包括select的列,而不仅仅只缓存关联列。在一个有N个JOIN关联的SQL当中会在执行时候分配N-1个join buffer。
limit
的时候尽量使用limit
limit
的作用应该是限制输出,而不是分页,分页只是应用场景。
字段值为null,也会占用存储空间。
组合索引
show status like 'Handler_read%'
参数解释
Handler_read_first:读取索引第一个条目(根节点)的次数
Handler_read_key:通过index获取数据的次数
Handler_read_last:读取索引最后一个条目的次数
Handler_read_next:通过索引读取下一条数据的次数
Handler_read_prev:通过索引读取上一条数据的次数
Handler_read_rnd:从固定位置读取数据的次数
Handler_read_rnd_next:从数据节点读取下一条数据的次数
Handler_read_next
注意
重点查看
Handler_read_key
和Handler_read_rnd_next
,这两个值越大越好
时间片
我们需要查询
rental
表的第10000行~第10004行explain select * from rental limit 10000,5;
从截图中可以看出为了获取5条数据而扫描了16008条数据(总共16044行,基本上是进行了全表扫描)。
我们常常会误以为mysql会只返回需要的数据,实际上mysql确实先返回全部结果再进行计算,再日常开发习惯中,进场是先用select语句查询大量的结果,然后获取前面的N行后关闭结果集。
优化方式是在查询后面添加limit
select * from actor inner join file_actor using(actor_id) inner join file using(film_id) where film.title='Academy';
select actor.* from actor....
在实际需求中,禁止使用
select *
,虽然这种方式能简化开发,但是会影响查询的性能,所以尽量不要使用。
如果需要不断的重复执行相同的查询,且每次返回完全相同的数据。因此,基于这样的应用场景,我们可以将这部分数据缓存(Redis)起来,这样的话能够提高查询效率。LRU
8.0已经弃用在解析一个查询语句之前,如果查询缓存是打开的,那么mysql会优先检查这个查询是否命中查询缓存中的数据,如果查询恰好命中了查询缓存,那么会在返回结果之前检查用户权限,如果权限没问题,那么MySQL会跳过所有的阶段,就直接从内存中拿到结果并返回给客户端。
MySQL查询完缓存之后会经过以下几个步骤:解析SQL、预处理、优化SQL执行计划,这几个步骤出现的任何错误,都可能会终止查询。
MySQL通过关键字将SQL语句进行解析,并生成一棵解析树(AST-Tree),MySQL解析器将使用MySQL语法检查规则验证和解析查询,例如验证使用了错误的关键字或者顺序是否正确等等,预处理会进一步检查解析树是否合法,例如表名和列名是否存在,是否有歧义,还会验证权限等等。
当语法树没有问题之后,相应的要由优化器将其转换成执行计划,一条查询语句可以使用非常多的执行方式,最后都可以得到对应的结果,但是不同的执行方式带来的效率是不同的,优化器的最主要目的就是要选择最有效的而执行计划。
mysql使用的是基于成本的优化器(RBO和CBO),在优化的时候会尝试预测一个查询使用某种查询计划时候的成本,并选择其中成本最小的一个。
select count(*) from file_actor; show status like 'last_query_cost';
在很多情况下MySQL会选择错误的执行计划,原因如下:
MySQL因为其
mvcc
的架构,并不能维护一个数据表的行数的精确统计信息
有时候某个执行计划虽然读取更多的页,但是它的成本却更小,因为如果这些页面都是顺序或者这些页面都已经在内存中的话,那么它的访问成本将很小,MySQL层面并不知道哪些页面在内存中,哪些在磁盘,所以查询实际过程中到底需要多少次IO是无法得知的。
MySQL的优化是基于成本模型的优化,但是有可能不是最快的优化。
执行存储过程或者用户自定义函数的成本。
优化器的优化策略
直接对解析树进行分析,并完成优化
动态优化与查询的上下文有关,也可能跟取值、索引对应的行数有关。
MySQL对查询的静态化优化只需要一次,但是对动态优化在每次执行的时候都需要重新评估
优化器的优化类型
数据表的关联并不总是按照在查询中指定的顺序进行,决定关联顺序是优化器很重要的功能
索引和列是否可以为空通常可以帮助MySQL优化这类表达式:列如,要找到某一列的最小值,只需要查询所有的最左端的记录即可,不需要全文扫描比较。
MySQL在某些情况下可以将子查询转换成一种效率更高的形式,从而减少多个查询多次对数据进行访问,列如将经常查询的数据放到缓存中
如果两个列的值通过等式关联,那么MySQL能够把其中一个列的where条件传递到另一个上:
explain select film.film_id from film inner join film_actor using(film_id) where film.film_id > 500;
这里使用film_id字段进行等值关联,film_id这个列不仅适用于film表而且适用于film_actor表
explain select film.film_id from film inner join film_actor using(film_id) where film.film_id > 500 and film_actor.film_id > 500;
关联查询
(1) Join Buffer会缓存所有参与查询的列而不是只有Join的列。(比如select后面的列)
(2)可以通过join_buffer_size缓存大小
(3)join_buffer_size的默认值是256K,join_buffer_size的最大值在MySQL 5.1.222版本之前是4G,而之后的版本才能在64位操作系统虾申请大于4G-1的Join Buffer空间。
(4) 使用Block Nested-Loop Join算法需要开启优化器管理配置的optimizer_switch的设置
block_nested_loop
为on
,默认为开启。show variables like '%optimizer_switch%';
案例演示
查看不同的顺序执行方式对查询性能的影响:
explain select film.film_id,film.title,film.release_year ,actor.actor_id,actor.first_name,actor.last_name from film inner join film_actor using(film_id) inner join actor using(actor_id)
explain select straight_join film.film_id,film.title,film.release_year ,actor.actor_id,actor.first_name,actor.last_name from film inner join film_actor using(film_id) inner join actor using(actor_id);
查看两个结果集的
row
列,可以看出扫描的行数不同。
排序优化
排序的算法
两次传输排序
第一次数据读取是将需要排序的字段读取出来,然后进行排序,第二次是将排好序的结果按照需要去读取数据行。
这种方式效率比较低,原因是第二次读取数据的时候因为已经排好序,需要去读取所有记录而此时更多的是随机IO,读取数据成本会比较高。
两次传输的优势,在排序的时候存储尽可能少的数据,让排序缓冲区可以尽可能多的容纳行数来进行排序操作。
单次传输排序
先读取查询所需要的所有列,然后再根据给定列进行排序,最后直接返回排序结果,此方式只需要一次顺序IO读取所有的数据,而无须任何的随机IO,问题在于查询的列特别多的时候,会占用大量的存储空间,无法存储大量的数据。
当需要排序的列总大小加上
order by
的列的大小超过max_lenght_for_sort_data
定义的字节,MySQL会选择双次排序,反之使用单次排序,当然,用户可以设置此参数的值来选择排序的方式。show variables like '%max_length_for_sort_data%';
count()
是特殊的函数,有两种不同的作用,一种是某个列值 的数量,也可以铜价行数。
使用近似值
在某些应用场景中,不需要完全精确的值,可以参考使用近似值来代替,比如可以使用explain来获取近似的值。
其实在很多OLAP的应用中,需要计算某一个列的基数,有一个计算近似值的算法叫做hyperloglog。
更复杂的优化
一般情况下,count()需要扫描大量的行数据才能获取精确的数据,其实很难优化,在实际操作的过程的时候可以考虑使用索引覆盖扫描,或者添加汇总表,或者增加外部缓存系统。
确保on
或者using
子句的列上有索引,在创建索引的时候就要考虑到关联的顺序
确保任何的group by
和order by
中的表达式只涉及到一个表中 的列,这样Mysql才能使用索引来优化这个过程。
子查询的优化最重要的优化建议是尽可能使用关联查询代替。
很多场景下,mysql使用相同的方法来优化group by和distinct的查询,使用索引是最有效的方式,但是有很多的情况下无法使用索引,可以使用临时表或者文件排序来分组
如果对关联查询做分组,并且是按照查找表中的某个列进行分组,那么可以采用表的标识列分组的效率比其他列更高
select actor.first_name,actor.last_name,count(*) fromfile_actor inner join actor using(actor_id) group by actor.first_name,actor.last_name
select actor.first_name,actor.last_name,count(*) fromfile_actor inner join actor using(actor_id) group by actor.actor_id(未必准确)
在很多应用场景中,我们需要将数据进行分页,一般会使用limit加上偏移量的方法实现,同时加上合适的order by子句,如果这种方式有索引的帮助,效率通常不错,否则的话需要进行大量的文件排序操作,还有一种情况,当偏移量非常大的时候,前面的大部分数据会被抛弃,这样的代价太高。
要优化这种查询的话,要么是在页面中限制分页的数量,要么优化大偏移量的性能。
select film_id,description from film order by title lmit 50,5
select film.film_id,film,description from film inner join (select film_id from film oder by title limit 50,5) as lim using(film_id);