一、前言
此处为博主在开发中遇到的优化mysql
的笔记,记录下来也是为了方便自己查阅,在每次写完或者要写sql之前都看一看,仅作记录。以下也都是博主在开发中遇到并优化的过程,踩坑不计其数,也许只有这样才能变得强大吧。
二、正文
1、不要对索引的字段进行函数操作,例如:
$start_time是时间戳格式
log_time是格式化后的日期格式
select xxx from xx where 1 and unix_timestamp(log_time) between {$start_time}) and {$lt_end_time} and drr_weekly = 0
这里直接转换log_time字段的格式,虽然sql短了,但是实际上并没有用到log_time索引,sql执行速度缓慢
修改后:
用到了log_time索引,sql执行速度明显上升
select xxx from xx where 1 and log_time between from_unixtime({$start_time}) and from_unixtime({$lt_end_time}) and drr_weekly = 0
第一个sql因为对索引字段进行了函数操作,所以并没有用到索引,此处需要谨记。如果对数据库不熟悉,那么请先查看数据库的结构,了解哪个字段有索引,这样写sql的时候才能写的更精准。
2、 所有的where条件查询的时候,都要确保查询的where字段不为空,要有这个判断的步骤,不然sql会报错。其次是要尽可能的处理数据库中的字段,因为有的时候数据库存储的字段可能不规范,所以要尽量处理这些不规范的字段,保证程序的稳定性
3、 mysql查询的where条件是大小写不敏感的,如果需要在查询的时候区分大小写,那么可以加上额外的一些参数:
参考:https://www.cnblogs.com/yeahwell/p/6904310.html
4、 新建数据表的时候,对于一些需要联表的字段,比如user_id之类的,一定要加上索引。还有会经常出现到where条件中的字段,也要加上索引,这样提前加好索引,能省很多事情
5、 get和post拿到的值, 绝对不能直接放在sql里,参数需要转义, 不然sql会出错, 而且会被sql注入,如果确定类型的,需要格式化一下,比如是int的, 直接intval()一下,字符串的话,要使用转义函数addslashes()
转义一下,还需要判断是否为空, 空的话, 就没有必要查询sql了
关于字符串的转义参考:https://blog.csdn.net/LJFPHP/article/details/86549582
6、 数据库建表的时候,要先看一下项目已存在的表是什么编码格式,然后保持编码格式一致。
比如目前数据库的编码格式都是utf8(utf8_general_ci)
,如果我们在建表的时候选择字段或者数据表的编码格式是(utf8_unicode_ci),
那么在mysql 5.7的严格模式下,插入就会报错:1267 Illegal mix of collations (utf8_general_ci,IMPLICIT) and (utf8_unicode_ci,IMPLICIT) for operation
修改数据表的编码格式:alter table `table_name` convert to character set utf8
7、 每次写完功能之后,要对复杂的sql进行explain
查看,明确索引有没有用到,extra
里面提示信息是什么,是否需要优化,一般来说索引能用到,type
能达到range
以上就可以,如果Extra
里面含有 Using filesort
或者 Using temporary
则代表该sql需要进行优化。
关于explain,可以参考博客:https://www.cnblogs.com/yycc/p/7338894.html
============================ 2019年5月10日13时更新 ==========================
8、 确定表中字段的类型,和get/post
传过来的参数类型。在操作sql
的时候,字段类型和参数类型要保持一致,保持不一致则不能用到索引。比如我这边是字符串的字段,version = 2.0
,死活用不到索引,当改为:version = '2.0'
的时候,类型一致,就成功用到了索引。
9、 此处接上面第七条
有时候我们本地的数据库数据很少,用explain测试毫无意义,因为mysql在数据量小的时候,是不会选择使用索引的,只有当数据量达到一定程度才会选择使用索引。所以我们在测试之前可以先往数据库循环插入一些数据,方便测试。
测试插入数据(循环插入一万条,方便查询)
/* for($i=0;$i<10000;$i++)
{
$sql = "INSERT INTO `xxxx` ( `buyer_id`, `xxxx`, `xxxx`, `xxxxx`, `transaction_id`, `xxxx`, `pay_money`, `order_money`, `xxxx`, `pay_date`, `remark`, `order_ip`, `xxxx`, `transaction_type`, `server_unique_flag`, `order_time`, `pay_time`) VALUES
( 1, 6, 6, '2219F75724AE60AC', '7A1E0AA39766D615', 1, 99.9000, 99.9000, '2019-03-24 23:35:08', '2019-03-25 23:35:08', '第一条记录', '192.168.1.223', 'cn', 404, '1', 1553002641, 1553002741)";
$data = G_FUNCTION::getProjectDb("slave")->createCommand($sql)->execute();
}*/
10、 表设计时需要考虑到这样的问题。像这种存放大数据的字段需要拆分到其他 表中。或者是考虑细化存储其中的数据。因为innodb
引擎中。对于大字段如text blob
等,只会存放768
个字节在数据页中,而剩余的数据会存储在溢出段中,在查询这些大字段的时候,会去访问很多页,从而影响sql
的性能。 最大768字节的作用是便于创建前缀索引/prefix index
,其余更多的内容存储在额外的page
里,哪怕只是多了一个字节。因此,所有列长度越短越好(能用 varchar
就别用text
)。
看这个博客了解原理:http://www.cnblogs.com/chenpingzhao/p/6719258.html
影响sql速度的原因: mysql
的 io
以page
为单位,因此不必要的数据(大字段)也会随着需要操作的数据一同被读取到内存中来,这样带来的问题由于大字段会占用较大的内存(相比其他小字段),使得内存利用率较差,造成更多的随机读取。从上面的分析来看,我们已经看到性能的瓶颈在于由于大字段存放在数据页中,造成了内存利用较差,带来过多的随机读。
记录这条的目的是网上很多人的sql很慢都是由于这种大字段引起的,所以在建表时进行优化十分必然。
扩展:关于innode存储的page介绍,大家可以参考这篇:https://blog.csdn.net/voidccc/article/details/40077329
11、 新建一个表,表的结构和内容都使用在另一张表中查询出来的数据
create table table2 select * from table1;
12、 InnoDB
使用的是聚簇索引,将主键组织到一棵B+树中,而行数据就储存在叶子节点上,若使用"where id = 14"
这样的条件查找主键,则按照B+树的检索算法即可查找到对应的叶节点,之后获得行数据。(查找主键是一次索引)
若对Name
列进行条件搜索,则需要两个步骤:第一步在辅助索引B+树中检索Name
,到达其叶子节点获取对应的主键。第二步使用主键在主索引B+树种再执行一次B+树检索操作,最终到达叶子节点即可获取整行数据。(查找其他字段是遍历两次b+tree
)
13、 order by要求排序字段必须是唯一?
答案: 是的,如果order by的字段不是唯一的,那么比如排序字段是time,那么当time相同的时候,就可能会出现遗漏数据的情况。所以针对这种情况,我们在写sql的时候一定要注意,可以考虑使用:xxxx order by time desc,id desc;
还要注意:order by time,id desc;
等同于: order by time asc,id desc。
因为排序默认是使用升序。
不明白的话,可以参考博客:https://blog.csdn.net/tsxw24/article/details/44994835
============================ 2019年6月02日20时更新 ==========================
1、 循环里面执行sql
的,如果实在不能把sql
拿出来,那应该判断循环条件,符合条件的再执行逻辑,不符合的就不执行逻辑,避免无谓的循环。比如你一上来就循环10次处理数据,如果这些数据有些是不需要处理的,那么就白白浪费了性能
2、 对于子查询来说,子查询里面的句子如果不加where
条件的话,代表全表扫描,非常耗费性能,所以要把外面的where
条件加进去。外面本来的where
条件还是要保留的,防止关键的字段可以有重复值的话,会造成数据重复
3、 group by也可以group by多个字段
,有时候有奇效。最后要检查sql
查询的是否准确,group by
的时候,查询的字段如果不是聚合字段,那么这个值是随机给出的,并不是你想要的最大或者做小的值。
4、 单个sql
的速度永远比多个sql
的速度快。单个sql
更方便优化和维护,明确功能之前,先尽量写一个sql
来实现复杂的操作。多个sql
的话,势必会有循环,对于大数据来说,循环更致命
5、 如果要处理的数据量很大,可以在sql
刚开始的地方,就用 limit 0,500
来处理,每次查询都保证只处理500
条,剩下的用limit 500,500
来处理
6、 如果要在where
条件中判断字段值是否为null
,例如:is_master = null
这种方式是查不出来东西的,必须换成is_master is null
这种方式。
7、 当sql
中存在子查询的时候,把order by
和lim
it都加入到子查询,减少子查询的数据量。这样主表和子查询的结果集关联查询的时候,效率会更高。
======================== 2019/12/1=============================
1、 建表的时候,像created_at
,updated_at
等字段尽量是直接创建,因为随着业务的提升,根据创建时间,修改时间进行一些业务操作是很正常的场景,后续再添加的话,麻烦,而且可能会引起其他的问题 。参考CSDN的博客时间,每次更新,发布时间都会变成当前的,这是为啥呢,因为他们没有记录创建时间,只记录的修改时间。。。
2、 当sql
中存在in(str)
查询的时候,需要判断是否存在str
的值,如果不进行判断的话,sql
就会报错。虽然根据业务逻辑,str
为空的概率很小,但是还是要加上这部分判断
3、 善用mysql
的函数,比如我们的需求是,当某个字段可能为null
时,默认给该字段赋值为1
,则使用方式如下:
coalesce(f.city_level,1) ,这样当查询到city_level的时候,就正常显示city_level的值,当查询不到的时候,默认为1
函数解释:coalesce(a,b,c) 函数范围参数abc中第一个非null的值,比如coalesce(1,null,null)返回1, coalesce(null,null,2)返回2
适用环境:左连接或者右连接的时候,匹配不到的行记录为null
值,此时我们需要给字段默认值,则可以用到该函数
4、 循环插入,sql
拼接的批量插入方式
$sqlpre = "INSERT IGNORE INTO xxx(xxx,xxx) VALUES ";
$sqlVal = "";
$i = 0;
foreach ($arrDevice as $k => $v) {
$sqlVal .= "('xxx','xxx'),";
if ($i >= 500) {
$sqlVal = substr($sqlVal, 0, -1);
if (!empty($sqlVal)) {
$sql = "{$sqlpre} {$sqlVal}";
Yii::$app->db->createCommand($sql)->execute(); // 500条就插入一次
$sqlVal = '';
$i = 0;
}
$i++;
}
}
这个应用场景就是循环批量插入的时候,我们可以通过计数,每500次插入一次,避免每次只插一条的大开销行为。当然,这里的每次插500条也只是为了举例,如果你要想每次插入很极限的话,参考我的另一篇博客:mysql批量插入数据,一次插入多少行数据效率最高?
======================== 2020/01/20=============================
1、 使用INT UNSIGNED
存储IPv4
,不要用char(15)
2、 使用varchar(20)
存储手机号,不要使用整数
解读:
(1)牵扯到国家代号,可能出现+/-/()等字符,例如+86
(2)手机号不会用来做数学运算
(3)varchar可以模糊查询,例如like ‘138%’
3、 单张表索引数量建议控制在5
个以内,互联网高并发业务,太多索引会影响写性能
4、 组合索引字段数不建议超过5
个
解读: 如果5
个字段还不能极大缩小row
范围,八成是设计有问题
来自mysql建表军规
每次更新的时候,都要从头看一遍这些优化记录,每次都有新的收货,简直太棒了!
end