1. 除了不要 SELECT * ,使用数据库还应知道的11个技巧!
技巧
- 比较运算符能使用 = 就不用 <>,= 增加了索引的使用几率
- 明知只有一条查询结构时,请使用 LIMIT 1, LIMIT 1 可以避免全表扫描,找到对应结果就不会再继续扫描了
- 为列选择合适的数据类型,比如能用 TINYINT 就不用 SMALLINT,能用 SMALLINT 就不用 INT,这样会有更小的磁盘和内存消耗
- 将大的 DELETE, UPDATE, INSERT 查询拆分成多个小查询,以达到更好的性能和更好的数据控制
- 使用 UNION ALL 替代 UNION,如果结果集允许重复的话,因为 UNION ALL 不去重,其效率高于 UNION
- 为获得相同结果集的多次执行,请确保 SQL 语句完全一致,这样做的目的是为了充分利用查询缓存(注意列的查询顺序也不能换,否则缓存不会命中)
- 尽量避免使用 SELECT *,尽量只返回需要的数据
- WHERE 子句里面的列尽量被索引,因为查询条件里的列若没有索引,则将导致全表扫描
- JOIN 子句里面的列尽量被索引,同理若 JOIN 的列若没有索引,也将导致全表扫描
- ORDER BY 的列尽量被索引,这样会有更好的性能
- 使用 LIMIT 实现分页逻辑,分页不仅提高了性能,同时减少了不必要的数据库和应用之间的网络传输
- 使用 explain 关键字去查看执行计划,explain 可以检查索引使用情况以及扫描的表
2. 面试官问你 MySQL 的优化,看这篇文章就够了
2.1 使用 EXPLAIN
- explain 详细可参考 mysql explain 笔记
- type : 连接类型,一个好的 sql 语句至少要达到 range 级别,杜绝出现 all 级别
- key : 使用到的索引名,如果没有选择索引,值是 NULL,可以采取强制索引方式
- key_len : 索引长度
- rows : 扫描行数,该值是个预估值
- extra : 详细说明,注意常见的不太友好的值有:Using filesort, Using temporary
2.2 SQL 语句中 IN 包含的值不应过多
- MySQL 对 IN 做了相应的优化,即将 IN 中的常量全部存储在一个数组里面,而且这个数据是排好序的,但如果数值较多,则产生的消耗也是比较大的
- 再比如,
select id from table_name where num in(1, 2, 3)
这样连续的数值,能用 between 就不要用 in 了 - 或者使用连接来替换,子查询有时可能导致性能降低
2.3 SELECT 语句务必指明字段名称
- 使用
SELECT *
会增加很多不必要的消耗(cpu, io, 内存, 网络带宽),增加了使用覆盖索引和全表扫描的可能性的可能性 - 而且若表结构发生变化,前端也需要进行更新,因此不能使用
SELECT *
而是在后面加上字段名
2.4 当只需要一条数据时,使用 LIMIT 1
- 这是为了使 EXPLAIN 中 type 达到 const 类型,即找到一条记录后就不再继续往下扫描
2.5 如果排序字段没有用到索引,就尽量少排序
- 在非索引字段上的排序会降低 sql 语句效率
2.6 如果查询条件中其他字段没有索引,则尽量少使用 or
- or 两边的字段中,如果有一个不是索引字段,将导致该查询不走索引,即使另一个查询条件是索引字段
- 该种情况下,可以考虑使用 union all 和 union 替代 or 进行查询
- 但也许根据具体情况进行判断,使用 union all 的话一个查询走索引,另一个查询走全表扫描,而统一查询也会走一次全表扫描
2.7 尽量使用 UNION ALL 替代 UNION
- union 和 union all 的差异主要是前者需要将结果集合并在进行唯一性过滤,这就会涉及到排序,增加大量的 CPU 运算,加大资源消耗及延迟
- 当然,使用 union all 的前提是两个结果集没有重复数据,否则只能使用 union 剔除重复
2.8 不使用 ORDER BY RAND()
2.9 区分 in, exists, not in, not exists
- select * from tb_A where id in (select id from tb_B)
- 用 exists 改写 : select * from tb_A where exists (select * from tb_B where tb_B.id = tb_a.id)
- in 和 exists 主要的却别是驱动顺序的不同,这是性能优化的关键,如果是 exists,那么那么外层表是驱动表,先被访问,如果是 in 那么先执行子查询
- 所以 in 适合外表大而内表小的情况,而 exists 适合外表小而内表大的情况
2.10 使用合理的分页方式提高分页效率
- 使用 limit 关键字进行分页,第一个参数为偏移量,第二个参数为数据大小
- 当数据量很大时,使用 limit 进行分页查询效率将变得低下,此时可以取前一页 id 的最大值,然后用这个 id 作为查询条件进行过滤,然后返回对应大小的数据
select id, name from table_name limit 866613, 20
select id, name from table_name where id > ... limit 20
- 进行该转换的前提是根据 id 顺序查询
2.11 分段查询
- 在一些用户选择界面中,用户选择的时间范围或者查询范围范围过大,造成查询缓慢
- 主要的原因是扫描行数过多,这个时候可以通过程序,分段进行查询,循环遍历各个分段,然后将结果合并处理进行展示
2.12 避免在爱 where 字句中对字段进行 null 值判断
- 对于 null 的判断会导致引擎放弃使用索引进而导致全表扫描
- 一般建表时建议禁止 null 并提供一个默认值
2.13 不建议使用 % 前缀模糊查询
- 例如
LIKE '%name'
或者Like '%name%'
,这种查询会导致索引失效进而导致全表扫描 - 对于需要模糊查询的字段,建议使用全文索引,全文索引相关内容参考笔记 : mysql 全文索引
2.14 避免在 where 子句中对字段进行表达式操作
- 对字段进行算数运算会导致引擎放弃使用索引
- 例如 :
select * from user where age * 2 = 36;
建议改成select * from user from user where age = 36/2;
2.15 避免隐式类型转换
- where 子句中出现 column 字段的类型和传入的参数类型不一致时会发生类型转换,建议先确定 where 中的参数类型
2.16 对于联合索引来说,要遵守最左前缀法则
- 例如,假设一联合索引包含 id, name, school 三个字段,可以直接使用 id 字段,也可以使用 id, name 这样的顺序,但 name, school 都无法使用这个索引,因此在创建联合索引时要注意索引字段的顺序
2.17 必要时可以使用 force index 来强制查询走某个索引
- 有的时候 MySQL 优化器采取它任何合适的索引来检索 sql 语句,但是可能它所采用的索引并不是我们想要的,这时就可以采用 force index 来强制优化器使用我们指定的索引
2.18 注意范围查询语句
- 对于联合索引来说,如果存在范围查询,比如 between, >, < 等条件时,会造成后面的索引字段失效,因此对于联合索引,除非是最后一个查询条件,否则前面的要采用等值查询
2.19 关于 join 优化
- A left join B : A 为驱动表
- A inner join B : MySQL 会自动找出那个数据少的表用作驱动表
- A right join B : B 为驱动表
- 注意 MySQL 没有 full join
- 尽量使用 inner join,避免使用 left join : 参数关联查询的表至少有两张,一般都有大小之分,如果使用 inner join,在没有其他过滤情况下,MySQL 会自动选择小表作为驱动表,但如果是 left join 则选择左边的作为驱动表,如果左表数据较大则效率会更低
- 注意连接时,连接的字段尽量采用具有索引的字段
- 尽量使用小表去驱动大表,连接时存储引擎采用的策略是用驱动表中的每一行和被驱动表进行连接,若减小驱动表,则可以减少嵌套循环的循环次数,以减少 IO 总量以及 CPU 运算次数
- 巧用straight_join : inner join 是由 mysql 选择驱动表,但有些情况需要自行选择驱动表,比如有 order by, group by 子句时,此时可以采用 straight_join 来强制连接顺序,其左边的即为驱动表,右边的为被驱动表
- 但要注意,使用 straight_join 的前提条件是该查询时内连接,也就是 inner join,其他连接不推荐使用 straight_join,否则可能造成查询结果不准确
3. MySQL 推荐使用规范
3.1 基础规范
- 使用 InnoDB 存储引擎 : 支持事务、行锁、并发性能更好、CPU 及内存缓存页优化使得资源利用率更高
- 推荐使用 utf8mb4 字符集 : 无需转码,无乱码风险,支持 emoji 表情以及部分不常见汉字
- 表、字段必须加注释 : 方便他人理解字段意思
- 不在数据库做计算 :
- 禁止使用存储过程、视图、触发器、Event
- 在并发量大的情况下,这些功能可能将数据库拖垮,业务逻辑放到服务层才具备更好的扩展性,能够轻易实现“增机器就加性能”
- 禁止文件存储 : 文件存储在文件系统,数据库里存 URI
- 控制单表数据量 : 单表的记录数控制在千万级
3.2 命名规范
- 库名、表名、字段名 : 小写,下划线风格,非唯一索引名 idx_xxx,唯一索引名 unq_xxx
- 表必须有主键,例如自增主键
- 主键递增,数据库行写入可以提高性能(高并发下好像不适用)
- 主键要选择较短的数据类型,InnoDB 引擎普通索引都会保存主键的值,较短的数据类型可以有效地减少索引的磁盘空间,提高索引的缓存效率
- 使用主键来保证实体的完整性、唯一性
- 不要使用外键,如果有外键,用应用程序控制
- 外键会导致表与表之间耦合,update 与 delete 操作都会涉及相关联的表,十分影响 sql 的性能,甚至造成死锁
- 高并发情况下外键容易造成数据库性能下降,大数据高并发业务场景数据库使用以性能优先
3.3 字段设计规范
- 把字段定义为 not null 并提供默认值
- null 的列使索引、索引统计、值判断都更加复杂,对 MySQL 来说更难优化
- null 这种类型 MySQL 内部需要进行特殊处理,增加数据库处理记录的复杂性;同等条件下,表中有较多 null 字段时,数据库的处理性能会降低很多
- null 需要更多的存储空间,无论是表、索引中、每行中的 null 的列都需要额外的空间来标识
- 对 null 的判断,只能采用 is null, is not null 而不能采用 =, in, <, <>, !=, not in 这些操作符,例如 : 对于 where name != 'zhangsan',即使存在 name 为 null 值的记录,查询的结果也不会包含 name 为 null 的值的记录
- 不要使用 text, blob 类型
- 会浪费更多的磁盘和内存空间
- 非必要的大量的大字段查询会淘汰掉热数据,导致内存命中率急剧降低,从而影响数据库性能
- 如果必须要使用则独立出来一张表,用主键来对应,避免影响其他字段的索引效率
- 不要使用小数存储货币 : 建议使用整数,小数容易导致钱对不上
- 必须使用 varchar 存储手机号 : 手机号不会去做数学运算
- 为提高效率可以牺牲范式设计,冗余数据
- 不能是频繁修改的字段
- 不能是 varchar 超长字段,更不能是 text 字段
3.4 索引设计规范
- 禁止在更新十分频繁、区分度不高的属性上建立索引
- 数据更新会变更索引的 B+ 数,更新频繁的字段建立索引会大大降低数据库的性能
- 性别这种区分度不大的属性,建立索引没有什么意义,还会浪费空间
- 建立联合索引时,必须把区分度高的放在最左边
- 如果 where a = ? and b = ?,a 的列几乎接近于唯一值,则 a 单独建 idx_a 索引即可
- 对于联合索引,禁止左范围或者全范围
- 索引文件具有 B-Tree 的最左前缀匹配特性,如果左边的值未确定,则无法使用索引
- 即最左的列是一个范围或不确定值,这样会导致不走索引
- 如果有需要可以考虑使用搜索组件来解决
3.5 SQL 使用规范
- 禁止使用
select *
,只获取必要字段,显式说明列属性- 浪费 cpu, io, 内存, 带宽
- 不能有效地利用覆盖索引(覆盖索引为只差索引列,对索引进行全覆盖查询,如果是 * 则直接导致全表扫描)
- 使用
select *
容易在增加或者删除字段后出现程序 bug,不具有扩展性
- 使用 insert into t_xxx(col1, col2) values(xxx) 时必须显式指定插入的列
- 否则容易在增加或者删除字段后出现程序 bug
- where 中的查询条件保证为同类型比较,否则可能导致全表扫描
- 例如 phone 为 varchar 类型,
select name from t_user where phone = 13333
会导致全表扫描,需要改为phone = '13333'
- 例如 phone 为 varchar 类型,
- 禁止在 where 条件的属性列上使用函数或者计算
- 在 where 条件的属性列上使用函数或者计算会导致全表扫描,但属性值的计算是允许的
- 例如 age 列存在索引,但
where age * 2 = 28
将导致全表扫描,而where age = 28/2
则可以走索引
- 禁止负向查询,以及 % 开头的模糊查询
- 负向查询 :
not, !=, <>, !<, !>, not in, no like
等,都会导致全表扫描 - % 开头的模糊查询也会导致全表扫描
- 负向查询 :
- 不要大表使用 join 查询,禁止大表使用子查询
- join 和子查询都会产生临时表,从而导致消耗较多的内存与 CPU,极大影响数据库性能
- in 是内表做驱动表,适合外表大而内表小的情况,而 exists 是外表做驱动表,适合外表小而内标大的情况(同连接一样,更小的驱动表会有更高的性能)
- or 改写为 in() 或者 union all
- or 两边如果有一个非索引,则必定不走索引而导致全表扫描
- 在低版本中,一个表查询一次只能使用一个索引,而如果两端各自存在索引,无法使用多个索引分别进行条件扫描,从而可能导致全表扫描
- 高版本引入了 index merge 优化技术,对于同一个表可以多个索引分别进行条件扫描,然后再合并(intersect, union, sort_union),好像可以用 or 了,MySQL 会自动优化
- 简单的事务 : 事务就像程序中的锁一样,粒度尽可能要小
- 不要一次性更新大量数据 : 数据更新会对行或者表加锁,应该分为多次更新