1.对查询进行优化,应尽量避免全表扫描,首先应考虑在WHERE
及ORDER BY
涉及的列上建立索引。
缺省情况下建立的索引是非群集索引,但有时它并不是最佳的。在非群集索引下,数据在物理上随机存放在数据页上。合理的索引设计要建立在对各种查询的分析和预测上。一般来说:
a.有大量重复值,且经常有范围查询(>,<,>=,<=)和ORDER BY
、 GROUP BY
发生的列,可考虑建立群集索引;
b.经常同时存取多列,且没列都含有重复值,可考虑建立组合索引,选择度高的列建议作为索引的第一个字段;
c.组合索引要尽量使关键查询形成索引覆盖,其前导列一定是使用最频繁的列。索引虽有助于提高性能,但不是索引越多越好,恰好相反,过多的索引会导致系统低效。用户在表中没加进一个索引,维护索引集合就要做相应的更新工作。
2.应尽量避免在WHERE
子句中对字段进行NULL
值判断,否则将导致引擎放弃使用索引而进行全表扫描;
SQL代码:SELECT id FROM t WHERE num IS NULL;
可以在num上设置默认值0,确保表中num列没有null值,然后这样查询:
SQL代码:SELECT id FROM t WHERE num = 0;
3.应尽量避免在WHERE
子句中使用!=
或<>
操作符,否则引擎将放弃使用索引而进行全表扫描。
4.应尽量避免在WHERE
子句中使用OR
来连接条件,否则将导致引擎放弃使用索引而进行全表扫描。
SQL代码:SELECT id FROM t WHERE num = 10 OR num = 20;
可以这样查询:
SQL代码:SELECT id FROM t WHERE num = 10 UNION ALL SELECT id FROM t WHERE num = 20;
5.IN
和NOT IN
也要慎用,否则会导致全表扫描,如:
SQL代码:SELECT id FROM t WHERE num IN(1,2,3);
对于连续的数值,能用BETWEEN
就不要用IN
了
SQL代码:SELECT id FROM t WHERE num BETWEEN 1 AND 3;
6.下面的查询也将导致全表扫描:
SQL代码:SELECT id FROM t WHERE name LIKE '%c%';
若要提高效率,可以考虑全文检索。
7.如果在WHERE
子句中使用参数,也会导致全表扫描。因为SQL只有在运行时才会解析局部变量,但优化程序不能将访问计划的选择推迟到运行时,它必须在编译时进行选择。然而,如果在编译时建立访问计划,变量的值还是未知的,因而无法作为索引选择的输入项。如下面语句将进行全表扫描:
SQL代码:SELECT id FROM t WHERE num = @numl;
可以改为强制查询使用索引:
SQL代码:SELECT id FROM t WITH(INDEX(索引名)) WHERE num = @num;
8.应尽量避免在WHERE
子句中对字段进行表达式操作,这将导致引擎放弃使用索引而进行全表扫描:
SQL代码:SELECT id FROM t WHERE num / 20 = 100;
可以这样查询:
SQL代码:SELECT id FROM t WHERE num = 100 * 2;
9.应尽量避免在WHERE
子句中对该字段进行函数操作,这将导致引擎放弃使用索引而进行全表扫描,如:
SQL代码:SELECT id FROM t WHERE SUBSTRING(name, 1, 3) = 'name';
应改为:
SQL代码:SELECT id FROM t WHERE name LIKE 'abc%';
10.不要在WHERE
子句中的=
左边进行函数、算数运算或其他表达式运算,否则系统将可能无法正确使用索引。
11.在使用索引字段作为条件时,如果该索引是复合索引,那么必须使用到该索引中的第一个字段作为条件时才能保证系统使用该索引,否则该索引将不会被使用,并且应尽可能的让字段顺序与索引顺序一致、
12.不要写一些没有意义的查询,如需要生成一个空表结构:
SQL代码:SELECT col1, col2 INTO #t FROM t WHERE 1 = 0;
这类代码不会返回任何结果集,但是会消耗系统资源的,应改成这样:
SQL代码:CREATE TABLE #t(...);
13.很多时候用EXISTS
代替IN
是个好的选择:
SQL代码:SELECT num FROM a WHERE num IN (SELECT num FROM b);
用下面的语句替换:
SQL代码:SELECT num FROM a WHERE EXISTS (SELECT 1 FROM b WHERE num = a.num);
14.并不是所有的索引都对查询有效,SQL是根据表中数据来进行查询优化的,当索引列有大量数据重复时,SQL查询可能不会去利用索引,如一表中有字段male、female几乎各占一半,那么即使在这一列上建立了索引也对查询效率起不了作用。
15.索引并不是越多越好,索引固然可以提高相应的SELECT
的效率,但同时也降低了INSERT
及UODATE
的效率,因为INSERT
或UPDATE
时有可能会重建索引,索引怎样检索因需要慎重考虑,视具体情况而定。一个表的索引数最好不要超过6个,若太多则应该考虑一些不常用到的列上建的索引是否有必要。
16.应尽可能的避免更新CLUSTERED索引数据列的顺序就是表记录的物理存储顺序,一旦该列值改变将导致整个表记录的顺序的调整,会耗费相当大的资源。所应用系统需要频繁更新CLUSTERED索引数据列,那么需要考虑是否应将该索引建为CLUSTERED索引。
17.尽量使用数字型字段,若只含数值信息的字段尽量不要设计为字符型,这会降低查询和连接的性能,并会增加存储开销。这是因为引擎在处理查询和连接时会逐个比较字符串中每一个字符,而对于数字型而言,只需要比较一次就够了。
18.尽可能的使用VARCHAR/NVARCHAR
代替CHAR/NCAHR
,因为首先变长字段存储空间小,可以节省存储空间,其次对于查询来说,在一个相对较小的字段内搜索效率显然是要高一些。
19.任何地方都不要使用SELECT * FROM t;
,用具体的字段列表代替“*”,不要返回用不到的任何字段。
20.尽量使用表变量来代替临时表。如果表变量包含大量数据,请注意索引非常有限(只有主键索引)。
21.避免频繁创建和删除临时表,以减少系统表资源的消耗。
22.临时表并不是不可使用,适当地使用塔门可以使某些例程更有效,例如,当需要重复引用大型表或常用表中的某个数据集时。但是,对于一次性事件,最好使用导出表。
23.在新建临时表时,如果一次性插入数据量很大,那么可以使用SELECT INTO
代替CREAT TABEL
,避免造成大量LOG,以提高速度,如果数据量不大,为了缓和系统表的资源,应先CREATE TABLE
,然后INSERT
。
24.如果使用到了临时表,在存储过程的最后务必将所有临时表显式删除,先TRUNCATE TABLE
,然后DROP TABLE
,这样可以避免系统表的较长时间锁定。
25.尽量避免使用游标,因为游标的效率较差,如果游标操作的数据超过1万行,那么就应该考虑该下改写。
26.使用基于游标的方法或临时表方法之前,应先寻找基于集的解决方案来解决问题,基于集的方法通常更有效。
27.与临时表一样,游标并不是不可使用,对小型数据集使用FAST_FORWARD
游标通常要优于其他逐行处理方法,尤其是在必须引用几个表才能获得所需的数据时。在结果集中包括“合计”的例程通常要比使用游标执行的速度快。如果开发时间允许,基于游标的方法和基于集的方法都可以尝试一下,看哪一种方法的效果更好。
28.在所有的存储过程和触发器的开始处设置SET NOCOUNT ON
,在结束时设置SET NOCOUNT OFF
。无需在执行存储过程和触发器的每个语句后向客户端发送DONE_IN_PROC
消息。
29.尽量避免大事务操作,提高系统并发能力。
30.顶起分析表和检查表。
分析表的语法:ANALYZE [LOCAL | NO_WRITE_TO_BINLOG] TABLE tb1_name[, tb1_name]...
以上语句用于分析和存储表的关键字分布,分析的结果将可以使得系统得到准确的统计信息,使得SQL能够生成正确的执行计划。如果用户感觉实际执行计划并不是预期的执行计划,执行一次分析表可能会解决问题。在分析期间,使用一个读取锁定对表进行锁定。这对于MyISAMDB
、BDB
和InniDB
有作用。
例如分析一个数据表:ANALYZE TABLE table_name
检查表的语法:CHECK TABLE tb1_name[, tb1_name]...[option]...option = {QUICK | FAST | MEDIUM | EXTENDED | CHANGED}
检查表的作用是检查一个或者多个表是否有错误,CHECK TABLE
对MyISAM
和InnoDB
表有作用,对于MyISAM
表,关键字统计数据被更新。
CHECK TABLE
也可以检查视图是否有错误,比如在视图定义中被引用的表不存在。
31.定期优化表
优化表的语法:OPTIMIZE [LOCAL | NO_WRITE_TO_BINLOG] TABLE tb1_name [, tb1_name]...
如果删除了标的一大部分,或者如果已经对含有可变长度行的表(含有VARCHAR
、BLOB
或者TEXT
列的表)进行更多更改,则应使用OPTIMIZE TABLE
命令来进行表优化。这个命令可以将表中的空间碎片进行合并,并且可以消除由于删除或者更新造成的空间浪费,但OPTIMIZE TABLE
命令只对MyISAM
、BDB
和InnoDB
表起作用。
例如:OPTIMIZE TABLE table_name;
注意:ANALYZE
、CHECK
、OPTIMIZE
执行期间将对表进行锁定,因此一定注意要在MySQL数据库不繁忙的时候执行相关的操作。
32.存储引擎的选择。如果数据表需要事务处理,应该考虑使用InnoDB
,因为它完全符合ACID特性。如果不需要事务处理,使用默认存储引擎MyISAM
是比较明智的。
MyISAM
适用于一些需要大量查询的应用,但其对于有大量写操作并不是很好。设置你只是需要UPDATE
一个字段,整个表都会被锁起来,而别的进程,就算是读进程都无法操作直到读操作完成。另外,MyISAM
对于SELECT COUNT(*)
这类的计算是超快无比的。
33.InnoDB
的趋势会是一个非常复杂的存储引擎,对于一些小的应用,它会比MyISAM
还慢。但是它支持行锁
,于是在写操作比较多的时候,会更优秀。并且,它还支持更多的高级操作,比如,事务。
34.EXPLAIN
你的SELECT
查询
使用EXPLAIN
关键字可以让你知道MySQL是如何处理你的SQL语句的。这可以帮助你分析你的查询语句或是表结构的性能瓶颈。
EXPLAIN
的查询结果还会告诉你你的索引主键如何被利用,你的数据表是如何被搜索和排序的……等等,等等。
挑一个你的SELECT
语句(推荐挑选那个最复杂的,有多表级联的),把关键字EXPLAIN
加到前面。你可以使用phpmyadmin
来做这个事。然后,你会看到一张表格。下面的这个示例中,我们忘记加上了GROUP_ID
索引,并且有表连接:
35.当是要一行数据是使用LIMIT 1
当你查询表的有些时候,你已经知道结果只会有一条,但因为你可能需要去FETCH
游标,或是你也许会去检查返回的记录数。在这种情况下,加上LIMIT 1
可能增加性能。这样MySQL引擎会在找到一条数据后停止搜索,而不是继续往后查找下一条符合记录的数据。
没有效率的:
$r = MYSQL_QUERY("SELECT * FROM user WHERE country = 'China'");IF(MYSQL_NUM_ROWS($r) > 0){//...}
有效率的:
$r = MYSQL_QUERY("SELECT * FROM user WHERE country = 'China' LIMIT 1");IF(MYSQL_NUM_ROWS($r) > 0){//...}
36.在JION
表的时候使用相同类型的列,并将其索引
如果你的应用程序有很多JION
查询,你应该确认两个表中JION
字段是被建过索引的。这样MySQL内部会启动为你优化JION
的语句的机制。
而且,这些被用来JION
的字段,应该是相同的类型的。例如:如果你要把DECIMAL
字段和一个INT
字段JION
在一起,MySQL就无法使用它们的索引。对于那些STRING
类型,还需要有相同的字符集才行。
//在STATE中查找company
$r = MYSQL_QUERY("SELECT company_name
FROM users
LEFT JION companies ON (users.state = companies.state)
WHERE users.id = $user_id");
37.千万不要ORDER BY RAND()
想打乱返回的数据行?随机挑一个数据?真不知道谁发明了这种用法,但很多新手很喜欢这样用。但你却不了解这样做有多么可怕的性能问题。
=如果你真的想把返回的数据行打乱了,你有N种方法可以达到这个目的,这样使用只让你的数据库性能惩治书记的下降。这里的问题是:MySQL会不得不去执行RAND()
函数(很耗CPU时间),而且这是为了每一行汲取去记行,然后再对其排序。就算是你用了LIMIT 1
也无济于事(因为要排序)
下面的示例是随机挑一条记录
//千万不要这样做:
$r = MYSQL_QUERY("SELECT username FROM user ORDER BY RAND() LIMIT 1");
//这样做会更好:
$r = MYSQL_QUERY("SELECT COUNT(*) FROM user");
$d = MYSQL_FETCH_ROW($r);
$rand = MT_RAND(0, $d[0] - 1);
$r = MYSQL_QUERY("SELECT username FROM user LIMIT $rand, 1");
38.永远为每一张表设置一个ID
我们应该为数据库里的每张表都设置一个ID作为其主键,而且最好的是一个INT
型的(推荐使用UNSIGNED
),并设置上自动增加的AUTO_INCREMENT
标志。
就算是你users表有一个主键叫“email”的字段,你也别让它成为主键。使用VARCHAR
类型来当主键会使性能下降。另外,在你的程序中,你应该使用表的ID来构造你的数据结构。
而且,在MySQL数据引擎下,还有一些操作需要使用主键,在这些情况下,主键的性能和设置变得非常重要,比如,集群,分区……
在这里,只有一个情况是例外,那就是“关联表”的“外键”。比如:有一个“学生表”有学生的ID,有一个“课程表”有课程ID,那么“成绩表”就是“关联表”了,其关联了学生表和课程表吗,在成绩表中,学生ID和课程ID叫做“外键”其共同组成主键。
39.从PROCEDURE ANALYSE()
取得建议
PROCEDURE ANALYSE()
会让MySQL帮你去分析你的字段和其实际的数据,并会给你一些有用的建议。只有表中有实际的数据,这些建议才会变得有用,因为要做一些大的决定是需要有数据作为基础的。
例如,如果你创建一个INT
字段作为你的主键,然而并没有太多的数据,那么,PROCEDURE ANALYSE()
会建议你把这个字段的类型改为MEDIUMINT
。或是你使用了一个VARCHAR
字段,因为数据不多,你可能会得到一个让你把它改成ENUM
的建议。这些建议可能因为数据不够多,所以决策做得不够准。
一定要注意,这些只是建议,只有当你的表里的数据越来越多是,这些建议才会变得准确。一定要记住,你才是最终做决定的人。
40.字段尽可能的使用NOT NULL
约束
除非你有一个很特别的原因去使用NULL
值,你应该总是让你的字段保持NOT NULL
。这看起来好像有点争议,请往下看:
首先,问问自己“Empty”和“NULL”有多大区别(如果是INT
,那就是0和NULL
)?如果你觉得它们之间没什么区别,那么你就不要使用NULL
。(你知道吗?在Oracle里,NULL
和Empty
的字符串是一样的!)
不要以为NULL
不需要空间,其需要额外的空间,并且,在你进行比较的时候,你的程序会更复杂。当然,这里并不是说你就不要使用NULL
了,现实情况是很复杂的,依然在有些情况下,你需要使用NULL
值。
下面摘自MySQL自己的文档:
“NULL columns require additional space in the row to record whether their values are NULL. For MyISAM tables, each NULL column takes one bit extra, rounded up to the nearest byte.”
如果你要保存NULL
,手动去设置它,而不是把它设为默认。建议使用0、特殊值或者空串代替NULL
值
41.Prepared Statements
Prepared Statements
很像存储过程,是一种运行在后台的SQL语句集合,我们可以从使用Prepared Statements
获得很多好处,无论是性能问题还是安全问题。
Prepared Statements
可以检查一些你绑定好的变量,这样可以保护你的程序不会受到“SQL注入式攻击”,当然,你也可以手动地检查你的这些变量,然而,手动的检查容易出问题,而且很经常会被程序员忘了。当我们使用一些framework或是ORM的时候,这样的问题会好一些。
在性能方面,当一个相同的查询被使用多次的时候,这回为你带来客观的性能优势。你可以给这些Prepared Statements
定义一些参数,而MySQL只会解析一次。
因为最新版本的MySQL在传输Prepared Statements
时使用二进制形式,所以这会使得网络传输非常有效率。
当然,也有一些情况下,我们需要避免使用Prepared Statements,因为其不支持查询缓存。但据说版本5.1后支持了。
42.把IP地址存成UNSIGNED INT
很多程序员都会创建一个VARCHAR(15)
字段来存放字符串形式的IP而不是整型的IP。如果你用整型来存放,只需要4个字节,并且你可以有定长的字段。而且,这会为你带来查询上的优势,尤其是当你需要使用这样的WHERE
条件:IP BETWEEN ip1 AND ip2
。
我们必需要使用UNSIGNED INT
因为IP地址会使用整个32位的无符号整型。
而你的查询,你可以使用INET_NTOA()
把一个整型转成一个字符串IP。在PHP中,也有这样的函数IP2LONG()
和LONG2IP()
。
43.固定长度的表会更快
如果表中所有字段都是“固定长度”的,整个表会被认为是“static”或“fixed-length”。例如,表中没有如下类型的字段:VARCHAR
,TEXT
,BLOB
。只要你包括了其中一个这些字段,那么这个表就不是“固定长度静态表”了,这样,MySQL引擎会用另一种方法来处理。
固定长度的表会提高性能,因为MySQL搜索得会更快一些,因为这些固定的长度是很容易计算下一个数据的偏移量的,所以读取的自然也会很快。而如果字段不是定长的,那么,每一次要找下一条的话,需要程序找到主键。
而且,固定长度的表也更容易被缓存和重建。不过,唯一的副作用是,固定长度的字段会浪费一些空间,因为定长的字段无论你用不用,他都要分配那么多的空间。
使用“垂直分割”技术,你可以分割你的表成为两个:一个是定长的,一个是不定长的。
45.垂直分割表
“垂直分割”是一种把数据库中的表按列变成几张表的方法,这样可以降低表的复杂度和字段的数目,从而达到优化的目的。
示例一:在users表中有一个字段是家庭住址,这个字段是可选字段,相比起,而且你在数据库操作的时候除了个人信息外,你并不需要经常读取或是改写这个字段。那么,你为什么不把他放在另一张表中呢?这样会让你的表有更好的性能,大家想想是不是,大量的时候,我对于用户表来说,只有用户ID、用户名、口令和用户角色等会被经常使用。小一点的表总会有好的性能。
示例二:你有一个叫“last_login”的字段,它会在每次用户登录时被更新。但是,每次更新时会导致该表的查询缓存被清空。所以,你可以把这个字段放在另一个表中,这样就不会影响你对用户ID、用户名和用户角色的不停读取了,因为查询缓存会帮你增加很多性能。
另外,你需要注意的是,这些被分出去的字段所形成的表,你不会经常性地去JION
他们,不然的话,这样的性能会比不分割时还要差,而且,会是指数级地下降。
46.拆分大的DELETE
或INSERT
语句
如果你需要在一个在线的网站上去执行一个大的DELETE
或INSERT
查询,你需要非常小心,要避免你的操作让你的整个网站停止服务,因为这两个操作是会锁表的,表一锁住了,别的操作都进不来了。
Apache会有很多的子进程或线程。所以,其工作起来想当有效率,而我们的服务器也不希望有太多的子进程、线程和数据库链接,这是极大的占服务器资源的事情,尤其是内存。
如果你把你的表锁上一段时间,比如30秒钟,那么对于一个有很高访问量的站点来说,这30秒所积累的访问进程、线程、数据库链接、打开的文件数,可能不仅仅会让你的WEB服务Crash,还可能会让你整台服务器马上挂了。
所以,如果你有一个很大的处理,你一定要把其拆分,使用LIMIT
条件是一个很好的办法。下面是一个示例:
WHILE(1){
//每次只做1000条
MYSQL_QUERY("DELETE FROM logs WHERE log_date <= '2009-11-01' LIMIT 1000");
IF(MYSQL_AFFECTED_ROWS() == 0){
//没得删了,退出!
//BREAK;
}
//每次都要休息一会儿
USLEEP(50000)
}
47.越小的列会越快
对于大多数的数据库引擎来说,硬盘操作可能是最重大的瓶颈。所以,把你的数据变得紧凑会对这种情况非常有帮助,因为这减少了对硬盘的访问。
参看MySQL的文档Storage Requirements查看所有的数据类型。
如果一个表只有几列罢了(比如说字典表,配置表),那么,我们就没理由使用INT
来做主键,使用MEDIUMINT
、SMALLINT
、或是更小的TINYINT
会更经济一些。如果你不需要记录时间,使用DATE
要比DATETIME
好很多。
当然,你也需要留够足够的扩展空间,参看Slashdot的例子(2009年11月06日),一个简单的ALTER TABLE
语句花了3个多小时,因为里面有一千六百万条数据。
48.使用一个对象关系映射器(Object Relational Mapper)
使用ORM你能获得可靠的性能增长。一个ORM可以做的所有事情,也能被手动地边写出来,但是这需要一个高级专家。
ORM的最重要的是“LAZY LOADING”,也就是说,只有在需要去取值的时候才会去真正地去做。但你也需要小心这种机制的副作用,因为这很有可能会因为要去创建很多很多小的查询,反而会降低性能。ORM还可以把你的SQL语句打包成一个事务,这会比单独执行它们快得多得多。
49.小心“永久链接”
“永久链接”的目的是用来减少重新创建MySQL链接的次数。当一个链接被创建,它会永远处在连接的状态,就算是数据库操作已经结束了。而且,自从我们的Apache开始重用它的子进程后——也就是说,下一次的HTTP请求会重用Apache的子进程,并重用相同的MySQL链接。
50.范围列(>
,<
,BETWEEN AND
)可以用到索引,但是范围列后面的列无法用到索引。同时,索引最多用于一个范围列,因此如果查询条件中有两个范围列则无法全用到作引。
51.如果需要在大字段上建立索引,可以考虑使用前缀索引。
建立前缀索引的语法为:
ALTER TABLE table_name ADD KEY(column_name(prefix_length));
52.将大字段、访问频率低的字段拆分到单独的表中存储。分离冷热数据,有利于有效利用缓存,防止读入无用的冷数据,较少磁盘IO,同时保证热数据常驻内存提高缓存命中率。
53.MySQL的新增和修改列的操作相当于重建表,表设计要一步到位,尽量避免大表的DDL操作。(TIPS:可以预定义一些列留作将来业务扩展,如:当前只需要10个字段,考虑到未来发展,可以预留10个字段,表上总共创建20个字段)
54.为了降低索引维护成本,禁止冗余索引,增大IO压力。(a,b,c)、(a,b),后者为冗余索引。可以利用前缀索引来达到加速目的,减轻维护负担。
55.WHERE
子句中的数据扫描不超过表总数据量的30%。
如何选择prefic_length的长度,具体参考:前缀索引,一种优化索引大小的解决方案
补充:
在海量查询时尽量少用格式转换。
任何对列的操作都将导致表扫描,它包括数据库教程函数、计算表达式等等,查询时要尽可能将操作移至符号右边。
IN
、OR
子句常会使用工作表,是索引失效。如果不产生大量重复值,可以考虑把子句拆开。拆开的子句中应该包含索引。尽量少用
CLOB
、TEXT
、BLOB
大类型。-
如果你的数据只有你所知的少量的几个量,最好使用ENUM类型。
ENUM
类型是非常快和紧凑的。在实际上,其保存的是TINYINT
,但其外表上显示为字符串。这样一来,用这个字段来做一些选项列表变得相当完美。如果你有一个字段,比如“性别”,“国家”,“民族”,“状态”或者“部门”,你知道这些字段的取值是有限而且固定的,那么,你应该使用
ENUM
而不是VARCHAR
。MySQL也有一个“建议”(见第十条)告诉你怎么去重新组织你的表结构。当你有一个
VARCHAR
字段时,这建议会告诉你把其改成ENUM
类型。使用PROCEDURE ANALYSE()
你可以得到相关的建议。 合理运用库、分表、与分区表提高数据存放和提取速度。具体参考:MySQL分表和分区的区别、分库分表区别