SQL规范与优化

一 SQL设计规范

1 数据库设计思想

基本思想:OLTP数据库只做数据“存与取”,尽量小而简单。

  •  小:字段类型选择小的,事务要避免大的
  • 简单:能在应用实现的,不要放到数据库

2 schema设计

2.1 字段类型选择

 2.2 必备字段

每个表含以下字段:

`id` bigint(18) NOT NULL AUTO_INCREMENT COMMENT '主键id',
 ....
`creator_id` varchar(32) NOT NULL DEFAULT '' 表必须包COMMENT '创建人id',
`modifier_id` varchar(32) NOT NULL DEFAULT '' COMMENT '修改人id',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`modify_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`is_delete` tinyint(2) NOT NULL DEFAULT '0' COMMENT '删除标识:0未删除1已删除',

 说明:

  • 主键id:独立的自增的bigint,与业务无关
  • 创建人id:记录创建人
  • 修改人id:记录修改人
  • 创建时间:程序维护、不修改,建索引
  • 更新时间:数据维护 删除标识:软删除

2.3 主键使用自增int/bigint

主键id:独立的自增的bigint,与业务无关,禁止修改

  • 避免插入数据造成数据分页
  • 提高二级索引的性能,二级索引包含主键
  • 大表一定要bigint,防止int不够用(大约21亿左右),溢出后会导致报错。

为什么不用UUID?

  1. uuid是无序的,插入数据时,页的位置会发生变化,页分裂,速度慢
  2. uuid占的空间大,并且innodb中,二级索引包含主键的值,那么每个索引的空间也都会增大,占的空间大,需要读数据时io次数就多

2.4 禁止使用default NULL

所有字段均定义为NOT NULL:

  • 字符为default ‘’
  • 数值为default 0
  • 时间为default 0000-00-00 00:00:00

为什么不用Null

Conceptually, NULL means “a missing unknown value” and it is treated somewhat differently from other values. 严格来说,NULL是一种状态,而不是一个值。

  • 存储空间优化
  • 查询效率优化
  • 联表查询问题
  • count(NULL  column)
  • 子查询 NOT IN

 2.5 禁止使用存储过程等

禁用procedure、function、trigger、views、event、外键约束。推荐都在程序端实现,应用横向扩展容易。

  • 避免一些bug
  • 消耗数据库更多的计算资源
  • 降低数据库实例可扩展性
  • 增加数据库复杂度
  • 增加数据库自动化运维难度

例如:外键约束容易导致数据批量同步出错,存储过程的存在可能会导致拆库迁移有问题。

3 索引设计规范

  • 索引名称必须使用小写
  • 非唯一索引建议使用“idx_表缩写名称_字段缩写名称”进行命名。
  • 唯一索引建议使用“uk_表缩写名称_字段缩写名称”进行命名。
  • 索引字段的顺序需要考虑字段值去重之后的个数(基数),个数多的放在前面
  • 使用EXPLAIN判断SQL语句是否合理使用索引,尽量避免extra列出现:Using File Sort,Using Temporary。
  • UPDATE、DELETE语句需要根据WHERE条件添加索引。
  • 单个表上的索引个数不能超过7个。
  • 合理创建联合索引(避免冗余),(a,b,c) 相当于 (a) 、(a,b) 、(a,b,c)。
  • 合理利用覆盖索引。比如SELECT email,uid FROM user_email WHERE uid=xx,如果uid不是主键,适当时候可以将索引添加为index(uid,email),以获得性能提升。
     

SQL规范:访问量很大的SQL不要在数据库里做排序

  • 禁止在DB里排序,请在web server上排序(对并发量特别大的SQL),web server有上百台,而db仅仅只有个位数的数量,排序都在db,会把db压垮。
  • 禁止在where子句中对字段施加函数,如to_date(add_time)>xxxxx, 应改为: add_time >= unix_timestamp(date_add(str_to_date('20220227','%Y%m%d'),interval - 29 day))
  • 禁止使用%做前缀模糊查询,例如LIKE “%weibo”,无法使用索引。
  • 禁止使用select *,兼容性差,使用更多的网络流量,不能使用覆盖索引,必须回表
  • 禁止3个以上的表join,join的字段数据类型必须绝对一致
  • in列表元素不能超过1000个

二 慢查询优化

什么是慢查询,慢查询的定义是啥?

执行时间超过1s的语句(参数long_query_time), 全表扫描超过1w行的语句(参数log_queries_not_using_indexes、min_examined_row_limit)。

 

1 没加索引,索引不生效

加索引的两种语法:
CREATE INDEX idx_name ON table(column1,column2);
alter table table_name add index idx_name(column1,column2);

区别:
create 一次只能创建一个索引;
alter 一次可以添加多个索引。

索引不生效案例1

有表:

CREATE TABLE tb_test (
	id BIGINT auto_increment NOT NULL COMMENT '主键id',
	test_name varchar(20) DEFAULT '' NOT NULL COMMENT '名称',
	`creator_id` varchar(32) NOT NULL DEFAULT '' COMMENT '创建人id',
    `modifier_id` varchar(32) NOT NULL DEFAULT '' COMMENT '修改人id',
    `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `modify_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    `is_delete` tinyint(2) NOT NULL DEFAULT '0' COMMENT '删除标识:0未删除1已删除',
     PRIMARY KEY (`id`)
)
ENGINE=InnoDB
DEFAULT CHARSET=utf8mb4
COLLATE=utf8mb4_general_ci;

查询条件带or导致索引失效

SELECT
   *
FROM
   tb_test 
WHERE
   1 = 1
   AND test_name = 'xxx'
   AND(
     creator_id = '123456'
     Or modifier_id = '123456'
   )
   AND create_time >= '2022-04-29 00:00:00'
   AND '2022-05-05 23:59:59' >= create_time 

 explain 查看执行计划,可知 type = ALL

解决方案1

在or的两个字段上分别建索引,mysql可以使用index merge的功能,可以检索多个范围扫描并将结果合并。

 index merge 如果在WHERE语句中,存在多层嵌套的AND/OR,MySQL可能不会选择最优的方案。

解决方案2

将or改写成union,注意去重;

SELECT
   *
FROM
   tb_test 
WHERE
   1 = 1
   AND test_name = 'xxx'
   AND creator_id = '123456'
   AND create_time >= '2022-04-29 00:00:00'
   AND '2022-05-05 23:59:59' >= create_time 
union
SELECT
   *
FROM
   tb_test 
WHERE
   1 = 1
   AND test_name = 'xxx'
   AND modifier_id = '123456'
   AND create_time >= '2022-04-29 00:00:00'
   AND '2022-05-05 23:59:59' >= create_time 

该方法要注意去重,不然结果会有偏差。

解决方案3

使用覆盖索引

覆盖索引会使索引比较臃肿,增加了dml的开销,非特别情况,不建议使用该方法。

当or条件的两个字段索引选择率不高的情况下,可以考虑覆盖索引。

索引不生效案例2

## 在查询字段上加函数会导致索引失效
select * from tb_test where left(test_name,length('张三')) = '张三'

## 改写为左匹配查询可以使用到索引
select * from tb_test where test_nam like '张三%'

索引不生效案例3

in元素过多可能会导致索引失效,超过500个可能就会失效。

解决方案1 

in元素超过500个,建议分组,结果用union all合并

实际过程中有测试原sql执行需要2.35s,通过union all 方法 执行时间为0.67s。

解决方案2

调整eq_range_index_dive_limit参数的值,默认是200

设置会话级别的参数后,执行时间为0.66s。

扩展:

  1. index dive: 到index中即利用索引完成元组数的估算,速度慢,但能得到精确的值
  2. index statistics: 使用索引的统计数值,进行估算,速度快,但得到的值未必精确,例如索引统计信息计算出每个等值影响100条数据,那么IN条件中包含5个等值则影响5*100条记录

强制索引

已经创建索引,收集统计信息,而且索引选择率更好的情况下就是走错索引。 可以使用OPTIMIZER_TRACE方法来查看mysql走每个索引,使用各种连接顺序的一个分析情况。

官网的一个例子: http://dev.mysql.com/doc/internals/en/tracing-example.html

走错索引应该怎么解决:

1、使用force index 强行选择某个索引;
2、修改你的SQl,引导它使用我们期望的索引;
3、优化你的业务逻辑;
4、优化你的索引,新建一个更合适的索引,或者删除误用的索引(慎用)。

强制索引方法

mysql 5.7支持query_rewrite的功能,可以直接在数据库层支持sql改写,而不用再应用侧修改sql。 改写后的执行计划跟之前的一样,唯一的区别就是执行完sql之后会有个warning告警。

limit深度分页

select productid,categoryid,deptid from product where checktime > '2020-01-01' limit 200000,10;

sql流程:

  1. 通过普通二级索引树idx_checktime,过滤checktime条件,找到满足条件的主键id。
  2. 通过主键id,回到id主键索引树,找到满足记录的行,然后取出需要展示的列(回表过程)。
  3. 扫描满足条件的200010行,然后扔掉前200000行,返回。

解决方法: 减少回表的次数,降低io

## 原sql:
select productid,categoryid,deptid from product where checktime > '2020-01-01' limit 200000,10;
## 改写后的sql:
select  a.productid,a.categoryid,a.deptid from product a INNER JOIN( select c.productid from product c where c.checktime > '2020-01-01' limit 200000,10)b on a.productid=b.productid;

 优化思路: 先通过idx_checktime二级索引树查询到满足条件的主键ID,再与原表通过主键ID内连接,这样后面直接走了主键索引了,同时也减少了回表。

原sql执行0.83s,优化后的sql自行0.07s,效果相差10倍。

多表join

数据库有个规范约定就是:尽量不要有超过3个以上的表连接。

MySQL中,join的执行算法,分别是:Simple Nested-Loop Join、Index Nested-Loop Join、Block Nested-Loop Join。优先级:Index Nested-LoopJoin > Block Nested-Loop Join > Simple Nested-Loop Join

Index Nested-Loop Join:索引进行连接的算法,索引是基于内层表的,通过外层表匹配条件直接与内层表索引进行匹配,避免和内层表的每条记录进行比较, 从而利用索引的查询减少了对内层表的匹配次数。
Block Nested-Loop Join:把参与查询的列缓存到Join Buffer 里,然后拿join buffer里的数据批量与内层表的数据进行匹配,从而减少了内层循环的次数(遍历一次内层表就可以批量匹配一次Join Buffer里面的外层表数据)

join过多的问题:

1、过多的表连接,会大大增加SQL复杂度。
2、如果使用小表来做驱动表,匹配的数据量比较小或者join_buffer设置的比较大,速度也不会太慢。
但是,如果join的数据量比较大时,mysql会采用在硬盘上创建临时表的方式进行多张表的关联匹配,这种显然效率就极低。
 

多表join优化思路

匹配次数 = 外层表行数 * 内层表行数

1、用小表驱动大表,减少外层循环的连接,从而减少内层循环次数;
2、join字段都加上索引,争取使用INLJ,减少内层表的循环次数;
3、增大join buffer size的大小:缓存数据越多,内层表循环次数越少;
4、减少不必要字段查询:
     (1)用INLJ时,如果可以用覆盖索引,减少回表次数。
     (2)用BNLJ时,查询字段越少,join buffer 缓存的数据就越多,内层表循环次数越少。

案例1

难点:多表join,模糊查询
解决:先只取该字段+主键的值,查询 出来的结果再与自身表做关联取出其他字段的值。
效果对比:优化前2.87s,优化后0.82s,性能提升了3倍。

案例2

更改业务逻辑,去掉多余的连接

案例3

返回数据量太大,更改业务逻辑,比如改为分页

子查询案例

SELECT  coupon_name,  grant_record_id  FROM  coupon_grant_record  WHERE  grant_record_id IN ( SELECT max( grant_record_id )  FROM  coupon_grant_record  WHERE  use_state IN (0,1)  AND valid_end_date > now()  AND TIMESTAMPDIFF( HOUR, now(), valid_end_date ) <= 24 GROUP BY grant_record_id );   ---字段grant_record_id为主键

由于子查询的结果是未知的,不能作为外层索引的判断条件,由此导致外层直接全表扫描。

子查询优化

将子查询中的结果作为结果集,通过主键与表连接,这样就能用到主键索引。
原来执行需要93s,改写成join后执行需要3s

 单表数据量太大

1、单表数据量太大为什么会变慢?

MySQL 为了提高性能,会将表的索引数据装载到内存中。InnoDB buffer size 足够的情况下,其能完成全加载进内存,查询不会有问题。但是,当单表数据量好几千万或者上亿时,导致内存无法存储其索引,使得之后的 SQL 查询会产生磁盘 IO,从而导致性能下降。性能之所以会变差,是因为维护索引的B+树结构层级变得更高了,查询一条数据时,需要经历的磁盘IO变多,因此查询性能变慢。

解决方案

1、事务优化
sql语句尽量简化,最好是基于主键索引查询,尽可能的避免多表复杂连接,olap事务在从库查询
2、表结构优化
将大表改造成分区表,查询条件都要带分区字段查询
3、加缓存
读多写少的场景,采用redis缓存,将读请求转到缓存层面
4、业务层优化
业务拆分,将结合业务拆分为独立业务,把历史数据分离出来   
5、垂直拆分
6、水平拆分
7、架构调整
使用兼容mysql协议的newdb,如:tidb、oceanbase
 

系统网络资源

CPU:
在同一时间,一个 CPU 可以处理一条 SQL,那么要是有 10 个 CPU 就可以同时处理 10 条 SQL,如果并发很高,sql获取不到cpu资源,只能处于等待状态。

内存:
InnoDB 使用了一种缓冲池的技术,也就是把磁盘读到的页放到一块内存区域里面。读取相同的页,先判断是不是在缓冲池里面,如果是,就直接读取,不用再次访问磁盘,内存的大小直接影响可以缓存页的大小。

IO、网络:
磁盘io的性能、网络传输的速度直接影响数据库的响应速度。
 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值