实践中应该如何优化SQL,本文将介绍数据量较大的场景下的SQL优化策略,如果数据量较小,没必要以此为准
关于SQL优化的成本:硬件>系统配置>数据库表结构>SQL及索引。(优化效果相反,即硬件<系统配置<数据库表结构<SQL及索引)
一、建表优化
从建表开始,我们就要考虑进行SQL的优化了,这会在很大程度上影响到以后使用SQL对数据库进行操作。所以在建表阶段,就要进行字段评估,比如哪些字段是经常要放在where中的,哪些又需要order by排序,哪些使用字符串,哪些使用数字等等,都是要考虑的问题。在以往的个人经验(不具有普遍参考性)中,有过太多的因为数据库的表没有建好,字段的类型没有设置好等原因,数据库进行一次又一次的修改,这些往往是牵一发而动全身的,有可能因为一个字段数据类型的修改,项目中不知道要修改多少代码。磨刀不误砍柴工,在数据库建表阶段进行足够的评估与优化是项目后续能够高效稳定运行的关键。
1.优先考虑在where、order by可能涉及到的字段上建立索引;
2. 尽量使用数字型字段替代字符型字段:
只含数值信息的字段,使用数字字段比字符型更好,字符型会降低查询和连接的性能,并会增加存储开销。这是因为引擎在处理查询和连接时会 逐个比较字符串中每一个字符,而对于数字型而言只需要比较一次就够了。比如性别,男用int型1表示,女用int型2表示,比起使用字符串表示,在后续的SQL查询阶段会有更高的效率。
3. 查询数据量大的表,会造成查询缓慢
主要的原因是扫描行数过多。这个时候可以通过程序,分段分页进行查询,循环遍历,将结果合并处理进行展示。要查询100000到100050的数据,如下:
SELECT * FROM (SELECT ROW_NUMBER() OVER(ORDER BY ID ASC) AS rowid,*
FROM infoTab)t WHERE t.rowid > 100000 AND t.rowid <= 100050
4. 用varchar/nvarchar 代替 char/nchar
两者区别:
1.varchar/nvarchar表示实际存储空间是可变长的,如varchar/nvarchar;char/nchar是存储空间是定长的(长度是固定的)。如varchar(10)/nvarchar(10)的实际存储空间为<=10,而char(10)/nchar(10)的实际存库空间为10,因为当输入长度不够10时,数据库会以英文空格形式在字段后面填充。
2.n表示为Unicode字符,即所有字符都占两个字节,nchar,nvarchar字符中,英文字符只需要一个字节存储,但汉字需要两个字节存储,英文与汉字同时存在时容易造成混乱,Unicode字符集就是为了解决字符集这种不兼容的问题而产生的,它所有的字符都用两个字节表示,即英文字符也是用两个字节表示。
3.varchar/nvarchar比后者节省空间,在效率上会比后者稍微差一些,即要想获得效率,就必须牺牲一定的空间,这也就是在数据库设计上常说的“以空间换效率”。
4.如果一个varchar/nvarchar列经常被修改,而且每次被修改的数据的长度不同,这会引起‘行迁移’(Row Migration)现象,而这造成多余的I/O,是数据库设计和调整中要尽力避免的,在这种情况下用char/nchar代替带varchar/nvarchar列会更好一些。
因此,应尽可能使用 varchar/nvarchar 代替 char/nchar ,因为首先varchar/nvarchar字段存储空间小,可以节省存储空间,其次对于查询来说,在一个相对较小的字段内搜索效率显然要高些。
上边提到了char/nchar在新建字符串的时候输入长度不够存储空间时会填充null,null也是需要空间的,比如:char(100) 型,在字段建立时,空间就固定了, 不管是否插入值(null也包含在内),都是占用 100个字符的空间的,如果是varchar这样的变长字段, null 不占用空间。
五个原则:
减少数据访问: 设置合理的字段类型,启用压缩,通过索引访问等减少磁盘IO
返回更少的数据: 只返回需要的字段和数据分页处理 减少磁盘io及网络io
减少交互次数: 批量DML操作,函数存储等减少数据连接次数
减少服务器CPU开销: 尽量减少数据库排序操作以及全表查询,减少cpu 内存占用
利用更多资源: 使用表分区,可以增加并行操作,更大限度利用cpu资源
关于索引在SQL优化中的重要性:
最大化利用索引;但索引不是越多越好,索引固然可以提高相应的select的效率。但同时也降低了insert和delete的效率。
二、查找优化
SELECT语句 - 执行顺序:
DISTINCT <select_list>
FROM <表名>(<left_table>) # 选取表,将多个表数据通过笛卡尔积变成一个表。
ON <筛选条件>(<join_condition>) # 对笛卡尔积的虚表进行筛选
JOIN <join, left join, right join...>(<join_type> JOIN <right_table>)
<join表> # 指定join,用于添加数据到on之后的虚表中,例如left join会将左表的剩余数据添加到虚表中
WHERE <where条件> # 对上述虚表进行筛选
GROUP BY <分组条件>(<group_by_list>) # 分组
<SUM()等聚合函数> # 用于having子句进行判断,在书写上这类聚合函数是写在having判断里面的
HAVING <分组筛选>(<having_condition>) # 对分组后的结果进行聚合筛选
SELECT <返回数据列表> # 返回的单列必须在group by子句中,聚合函数除外
DISTINCT # 数据除重
ORDER BY <排序条件>(<order_by_condition>) # 排序
LIMIT <行数限制>(<limit_number>)
1. 避免出现select *
在SQL查询中应减少无效数据的查询,使用select * 取出全部列在任何类型数据库中都不是一个好的SQL编写习惯,这会让优化器无法完成索引覆盖扫描这类优化,会影响优化器对执行计划的选择,也会增加网络带宽消耗,更会带来额外的I/O,内存和CPU消耗。应该按照业务实际需要的列数,将指定列名以取代select *。
2. 避免出现不确定结果的函数
特定针对主从复制这类业务场景。由于原理上从库复制的是主库执行的语句,使用如now()、rand()、sysdate()、current_user()等不确定结果的函数很容易导致主库与从库相应的数据不一致。另外不确定值的函数,产生的SQL语句无法利用query cache。
3.多表关联查询时,小表在前,大表在后。
在MySQL中,执行 from 后的表关联查询是从左往右执行的(Oracle相反),第一张表会涉及到全表扫描,所以将小表放在前面,先扫小表,扫描快效率较高,在扫描后面的大表,或许只扫描大表的前100行就符合返回条件并return了。
例如:表1有100条数据,表2有100亿条数据;如果全表扫描表2,那么时间上的成本将会大大增加。
4. 使用表的别名
当在SQL语句中连接多个表时,请使用表的别名并把别名前缀于每个列名上。这样就可以减少解析的时间并减少哪些有列名歧义引起的语法错误。
5. 用where替换having
避免使用having,因为having只会在检索出所有记录之后才对结果集进行过滤,而where则是在聚合前筛选记录,如果能通过where字句限制记录的数目,那就能减少这方面的开销。having的条件一般用于聚合函数的过滤,除此之外,应该将条件写在where字句中。
where和having的区别:where后面不能使用组函数
6.调整Where子句中的连接顺序
MySQL采用从左往右,自上而下的顺序解析where子句。根据这个原理,应将过滤数据多的条件往前放,最快速度缩小结果集,这样后边的where语句查找的结果级就会越来越小,从而提高SQL效率。Where子句中:where 表之间的连接必须写在其他 Where 条件之前,HAVING 最后。
三、增删改 DML 语句优化
1. 大批量插入数据
优化前:
insert into t_product values(1,2);
insert into t_product values(1,3);
insert into t_product values(1,4);
insert into t_product ...;
一般情况下以下批量插入效率有几倍的差别:
insert into T values(1,2),(1,3),(1,4),...;
选择后一种方法的原因:
1.减少SQL语句解析的操作,MySQL没有类似Oracle的share pool,采用后者,只需要解析一次就能进行数据的插入操作;
2.在特定场景可以减少对DB连接次数
3.SQL语句较短,可以减少网络传输的IO。
2. 适当使用commit
适当使用commit可以释放事务占用的资源而减少消耗,commit后能释放的资源如下:
事务占用的undo数据块;
事务在redo log中记录的数据块;
释放事务施加的,减少锁争用影响性能。特别是在需要使用delete删除大量数据的时候,必须分解删除量并定期commit。
3. 避免重复查询更新的数据
针对业务中经常出现的更新行同时又希望获得改行信息的需求,MySQL并不支持PostgreSQL那样的UPDATE RETURNING语法,在MySQL中可以通过变量实现。
例如,更新一行记录的时间戳,同时希望查询当前记录中存放的时间戳是什么,简单方法实现:
update t1 set time=now() where col1=1;
select time from t1 where id =1;
使用变量,可以重写为以下方式:
update t1 set time=now () where col1=1 and @now: = now ();
select @now;
前后二者都需要两次网络来回,但使用变量避免了再次访问数据表,特别是当t1表数据量较大时,后者比前者快很多。
4.设置SQL优先级
MySQL 还允许改变语句调度的优先级,它可以使来自多个客户端的查询更好地协作,这样单个客户端就不会由于锁定而等待很长时间。改变优先级还可以确保特定类型的查询被处理得更快。我们首先应该确定应用的类型,判断应用是以查询为主还是以更新为主的,是确保查询效率还是确保更新的效率,决定是查询优先还是更新优先。下面我们提到的改变调度策略的方法主要是针对只存在表锁的存储引擎,比如 MyISAM 、MEMROY、MERGE,对于Innodb 存储引擎,语句的执行是由获得行锁的顺序决定的。MySQL 的默认的调度策略可用总结如下:
1)写入操作优先于读取操作。
2)对某张数据表的写入操作某一时刻只能发生一次,写入请求按照它们到达的次序来处理。
3)对某张数据表的多个读取操作可以同时地进行。MySQL 提供了几个语句调节符,允许你修改它的调度策略:
LOW_PRIORITY关键字应用于DELETE、INSERT、LOAD DATA、REPLACE和UPDATE;
HIGH_PRIORITY关键字应用于SELECT和INSERT语句;
DELAYED关键字应用于INSERT和REPLACE语句。
如果写入操作是一个 LOW_PRIORITY(低优先级)请求,那么系统就不会认为它的优先级高于读取操作。在这种情况下,如果写入者在等待的时候,第二个读取者到达了,那么就允许第二个读取者插到写入者之前。只有在没有其它的读取者的时候,才允许写入者开始操作。这种调度修改可能存在 LOW_PRIORITY写入操作永远被阻塞的情况。
SELECT 查询的HIGH_PRIORITY(高优先级)关键字也类似。它允许SELECT 插入正在等待的写入操作之前,即使在正常情况下写入操作的优先级更高。另外一种影响是,高优先级的 SELECT 在正常的 SELECT 语句之前执行,因为这些语句会被写入操作阻塞。如果希望所有支持LOW_PRIORITY 选项的语句都默认地按照低优先级来处理,那么 请使用--low-priority-updates 选项来启动服务器。通过使用 INSERTHIGH_PRIORITY 来把 INSERT 语句提高到正常的写入优先级,可以消除该选项对单个INSERT语句的影响。
四、查询条件优化
1. 对于复杂的查询,可以使用中间临时表暂存数据;
2. 优化group by语句
默认情况下,MySQL 会对GROUP BY分组的所有值进行排序,如 “GROUP BY col1,col2,....;” 查询的方法如同在查询中指定 “ORDER BY col1,col2,...;” 如果显式包括一个包含相同的列的 ORDER BY子句,MySQL 可以毫不减速地对它进行优化,尽管仍然进行排序。
因此,如果查询包括 GROUP BY 但你并不想对分组的值进行排序,你可以指定 ORDER BY NULL禁止排序。例如:
SELECT col1, col2, COUNT(*) FROM table GROUP BY col1, col2 ORDER BY NULL;
3. 优化join语句
MySQL中可以通过子查询来使用 SELECT 语句来创建一个单列的查询结果,然后把这个结果作为过滤条件用在另一个查询中。使用子查询可以一次性的完成很多逻辑上需要多个步骤才能完成的 SQL 操作,同时也可以避免事务或者表锁死,并且写起来也很容易。但是,有些情况下,子查询可以被更有效率的连接(JOIN)..替代。
例子:要将所有没有绑定手机号的用户取出来,可以用下面这个查询完成:
SELECT user_name FROM user_info
WHERE user_id NOT in (SELECT user_id FROM phone_t )
如果使用JOIN来完成这个查询工作,速度将会有所提升。尤其是当 user_info表中对 user_id建有索引的话,性能将会更好,查询如下:
SELECT user_name FROM user_info
LEFT JOIN phone_t ON phone_t.user_id = user_info.user_id
WHERE phone_t.user_id IS NULL
JOIN之所以更有效率一些,是因为 MySQL 不需要在内存中创建临时表来完成这个逻辑上的需要两个步骤的查询工作。
4. 优化union查询
MySQL通过创建并填充临时表的方式来执行union查询。除非确实要消除重复的行,否则建议使用union all。原因在于如果没有all这个关键词,MySQL会给临时表加上distinct选项,这会导致对整个临时表的数据做唯一性校验,这样做的消耗相当高。
优化前:
SELECT COL1, COL2, COL3 FROM TABLE WHERE COL1 = 10
UNION
SELECT COL1, COL2, COL3 FROM TABLE WHERE COL3= 'TEST';
优化后:
SELECT COL1, COL2, COL3 FROM TABLE WHERE COL1 = 10
UNION ALL
SELECT COL1, COL2, COL3 FROM TABLE WHERE COL3= 'TEST';
5.拆分复杂SQL为多个小SQL,避免大事务
简单的SQL容易使用到MySQL的QUERY CACHE,减少锁表时间特别是使用MyISAM存储引擎的表,可以使用多核CPU。
6. 使用truncate代替delete
当删除全表中记录时,使用delete语句的操作会被记录到undo块中,删除记录也记录binlog,当确认需要删除全表时,会产生很大量的binlog并占用大量的undo数据块,此时既没有很好的效率也占用了大量的资源。
使用truncate替代,不会记录可恢复的信息,数据不能被恢复。也因此使用truncate操作有其极少的资源占用与极快的时间。另外,使用truncate可以回收表的水位,使自增字段值归零。
7. 使用合理的分页方式以提高分页效率
使用合理的分页方式以提高分页效率 针对展现等分页需求,合适的分页方式能够提高分页的效率。
案例1:
select * from t where thread_id = 10000 and deleted = 0
order by gmt_create asc limit 0, 15;
上述例子通过一次性根据过滤条件取出所有字段进行排序返回。数据访问开销=索引IO+索引全部记录结果对应的表数据IO。因此,该种写法越翻到后面执行效率越差,时间越长,尤其表数据量很大的时候。
适用场景:当中间结果集很小(10000行以下)或者查询条件复杂(指涉及多个不同查询字段或者多表连接)时适用。
案例2:
select t.* from (select id from t where thread_id = 10000 and deleted = 0
order by gmt_create asc limit 0, 15) a, t
where a.id = t.id;
上述例子必须满足t表主键是id列,且有覆盖索引secondary key:(thread_id, deleted, gmt_create)。通过先根据过滤条件利用覆盖索引取出主键id进行排序,再进行join操作取出其他字段。数据访问开销=索引IO+索引分页后结果(例子中是15行)对应的表数据IO。因此,该写法每次翻页消耗的资源和时间都基本相同,就像翻第一页一样。
适用场景:当查询和排序字段(即where子句和order by子句涉及的字段)有对应覆盖索引时,且中间结果集很大的情况时适用。
五、索引优化
1. 避免不使用索引的场景
在SQL操作中有很多可能的不走索引、直接全表扫描的情况,项目中的数据量一般都是百万或者更高的数据量,全表扫描的时间成本可想而知,因此,有效避免SQL进行全表扫描查询的有效途径就是让SQL语句最大化地利用索引进行SQL操作,尽可能避免全表扫描,以提高效率,降低时间成本。
select * from t where 100 <c and c < 100000;
c 字段上没有索引,那么只能走全表扫描,这会导致这条查询语句很慢。但是字段根本没有索引不适用于这种场景,这种场景只讨论某个字段设置了索引但是没有走索引的情况。
会导致数据库引擎放弃索引进行全表扫描的情况 | 优化过程 | SQL | |
尽量避免在字段开头模糊查询 | 优化前 | | |
优化方式("xx%",或者尽量使用索引覆盖,查找的字段都设置成索引) | 尽量在字段后面使用模糊查询: | ||
尽量避免使用in 和not in | 优化前 | | |
优化方法(如果主查询的数据集大,使用in效率高;如果子查询的数据集大,使用exists效率高,因为exists后面的查询是使用索引的。而in会是索引失效,所以适合数据量小的) | 如果是连续数值,可以用between代替: 如果是子查询,可以用exists代替。用 EXISTS 替代 IN、用 NOT EXISTS 替代 NOT IN : 不走索引: 走索引: | ||
尽量避免使用 or | 优化前 | | |
优化方法 | 用union代替or: | ||
尽量避免where 子句中对字段进行null值的判断,(避免在索引列上使用 IS NULL 和 IS NOT NULL) | 优化前 | | |
优化方法:给字段添加默认值0(0值不会进行全局扫描),对0值进行判断 | | ||
尽量避免在where条件中等号的左侧进行表达式操作或函数操作(即避免在索引列上使用计算) | 优化前: 筛选条件右边有运算就能用上索引,数据库不会自动帮助优化转化这样的语句从而把 c - 1=1000 自动转换为 c = 1000+1。 | | |
优化方法:可以将表达式、函数操作移动到等号右侧 | | ||
当数据量大时,避免使用where 1=1的条件。通常为了方便拼装查询条件,我们会默认使用该条件,数据库引擎会放弃索引进行全表扫描。 | SELECT username, age, sex FROM T WHERE 1=1 | 用代码拼装sql时进行判断,没 where 条件就去掉 where,有where条件就加 and。 | |
查询条件不能用 <> 或者 != | 使用索引列作为条件进行查询时,需要避免使用<>或者!=等判断条件。如确实业务需要,使用到不等于符号,需要在重新评估索引建立,避免在此字段上建立索引,改由查询条件中其他索引字段代替。 | ||
where条件仅包含复合索引非前置列: 复合(联合)索引包含col1,col2,col3三列,但SQL语句没有包含索引前置列"col1",按照MySQL联合索引的最左匹配原则,不会走联合索引 | 优化前 | | |
优化方法:建立联合索引,包含col2,col3两列 | 同上 | ||
隐式类型转换造成不使用索引, SQL语句由于索引对列类型为varchar | 优化前:给定的值为数值,涉及隐式类型转换,造成不能正确走索引。 | | |
优化方法:使用字符串类型 | | ||
order by 条件要与where中条件一致,否则order by不会利用索引进行排序。 对查询进行优化,应尽量避免全表扫描, | 不走age索引 SELECT * FROM t order by age; 走age索引 SELECT * FROM t where age > 0 order by age; | 对于上面的语句,数据库的处理顺序是: step1.根据where条件和统计信息生成执行计划,得到数据。 当order by 中的字段出现在where条件中时,才会利用索引而不再二次排序,更准确的说,order by 中的字段在执行计划中利用了索引时,不用排序操作。 这个结论不仅对order by有效,对其他需要排序的操作也有效。比如group by 、union 、distinct等。 |
2、避免数据库选错索引
如下查询语句:
select * from t where 100 < c and c < 100000;
主键索引和非主键索引是有区别的,主键索引存放的值是整行字段的数据,而非主键索引上存放的值不是整行字段的数据,而且存放主键字段的值。不大懂的可以看这篇文章: 【思维导图-索引篇】搞定数据库索引就是这么简单 里面有说到主键索引和非主键索引的区别
索引预测机制:
如果走某字段的索引的话,最后会查询到对应主键的值,然后,再根据主键的值走主键索引,查询到整行数据返回。就算在这个字段上有索引,系统也并不一定会走这个字段上的索引,而是有可能会直接扫描扫描全表,找出所有符合where条件的数据。原因是系统在执行这条语句的时候,会将字段索引扫描的行数与扫描全表扫描的行数进行比较。显然,扫描行数越少,I/O操作次数越少,性能越优。
如果是扫描全表的话,那么扫描的次数就是这个表的总行数了,假设为 n;而如果走索引的话,我们通过索引找到主键之后,还得再通过主键索引来找我们整行的数据,也就是说,需要走两次索引。而且,我们也不知道符合 100 c < and c < 10000 这个条件的数据有多少行,万一这个表是全部数据都符合呢?这个时候意味着,走 c 索引不仅扫描的行数是 n,同时还得每行数据走两次索引。
所以系统是走全表扫描而不走索引的的判断依据系统的预测,即如果要走 c 字段索引的话,系统会预测走 c 字段索引大概需要扫描多少行。如果预测到要扫描的行数很多,它可能就不走索引而直接扫描全表了。
判断原则:
系统是通过索引的区分度来判断的,一个索引上不同的值越多,意味着出现相同数值的索引越少,意味着索引的区分度越高。我们也把区分度称为基数,即区分度越高,基数越大,符合 100 < c and c < 10000 这个条件的行数越少,走索引查询越有优势。
索引基数的计算:
系统当然是不会遍历全部来获得一个索引的基数的,代价太大了,索引系统是通过遍历部分数据,也就是通过采样的方式,来预测索引的基数的。既然是采样,那就有可能出现失误的情况,也就是说,c 这个索引的基数实际上是很大的,但是采样的时候,却很不幸,把这个索引的基数预测成很小。例如你采样的那一部分数据刚好基数很小,然后就误以为索引的基数很小。然后系统就不走 c 索引了,直接走全部扫描了。而这正是导致SQL 语句执行很慢的原因之一。
系统判断是否走索引,扫描行数的预测其实只是原因之一,这条查询语句是否需要使用使用临时表、是否需要排序等也是会影响系统的选择的。
解决措施:
可以通过强制走索引的方式来查询:
select * from t force index(a) where c < 100 and c < 100000;
也可以通过
show index from t;
来查询索引的基数和实际是否符合,如果和实际很不符合的话,我们可以重新来统计索引的基数,可以用这条命令
analyze table t;
来重新统计分析
既然会预测错索引的基数,这也意味着,当我们的查询语句有多个索引的时候,系统有可能也会选错索引哦,这也可能是 SQL 执行的很慢的原因之一。
相关拓展:
如果需求是要在前面使用模糊查询:
1. 使用MySQL内置函数INSTR(str,substr) 来匹配,作用类似于java中的indexOf(),查询字符串出现的角标位置
2. 使用FullText全文索引,用match against 检索
3. 数据量较大的情况,建议引用ElasticSearch、solr,亿级数据量检索速度秒级
4. 当表数据量较少(几千条左右),直接用like '%xx%'。
11. 正确使用hint优化语句
MySQL中可以使用hint指定优化器在执行时选择或忽略特定的索引。一般而言,处于版本变更带来的表结构索引变化,更建议避免使用hint,而是通过Analyze table多收集统计信息。但在特定场合下,指定hint可以排除其他索引干扰而指定更优的执行计划。
USE INDEX 在你查询语句中表名的后面,添加 USE INDEX 来提供希望 MySQL 去参考的索引列表,就可以让 MySQL 不再考虑其他可用的索引。例子: SELECT col1 FROM table USE INDEX (mod_time, name)...
IGNORE INDEX 如果只是单纯的想让 MySQL 忽略一个或者多个索引,可以使用 IGNORE INDEX 作为 Hint。例子: SELECT col1 FROM table IGNORE INDEX (priority) ...
FORCE INDEX 为强制 MySQL 使用一个特定的索引,可在查询中使用FORCE INDEX 作为Hint。例子: SELECT col1 FROM table FORCE INDEX (mod_time) ...
在查询的时候,数据库系统会自动分析查询语句,并选择一个最合适的索引。但是很多时候,数据库系统的查询优化器并不一定总是能使用最优索引。如果我们知道如何选择索引,可以使用FORCE INDEX强制查询使用指定的索引。《MySQL中特别实用的几种SQL语句送给大家》博文建议阅读,干货
例如:
SELECT * FROM students FORCE INDEX (idx_class_id) WHERE class_id = 1 ORDER BY id DESC;
9.尽量避免使用游标,因为游标效率较差。
如果使用到了临时表,在存储过程的最后务必将所有的临时表显式删除,先truncate table,然后drop table,这样可以避免系统的?
优化数据库的方法
1、选取最适用的字段属性,尽可能减少定义字段宽度,尽量把字段设置 NOTNULL,例如’省份’、’性别’最好适用 ENUM
2、使用连接(JOIN)来代替子查询
3、适用联合(UNION)来代替手动创建的临时表
4、事务处理
5、锁定表、优化事务处理
6、适用外键,优化锁定表
7、建立索引
8、优化查询语句
简化查询字段?
减少表之间关联?