今天,数据库的操作越来越成为整个应用的性能瓶颈了,这点对于Web应用尤其明显。关于数据库的性能,这并不只是DBA才需要担心的事,而这更是我们程序员需要去关注的事情。当我们去设计数据库表结构,对操作数据库时(尤其是查表时的SQL语句),我们都需要注意数据操作的性能。这里,我们不会讲过多的SQL语句的优化,而只是针对MySQL这一Web应用最多的数据库。希望下面的这些优化技巧对你有用。
1. 为查询缓存优化你的查询
大多数mysql服务器都开起了查询缓存。这时提高性能最有效的方法之一,而且这是被mysql数据库引擎处理的,当有很多相同的查询被执行很多次以后,这些查询结果会被放到一个缓存中去,这样,后续的相同的查询就不用操作表而直接访问缓存中的结果了。
// 查询缓存不开启$r = mysql_query("SELECT username FROM user WHERE signup_date >= CURDATE()");
// 开启查询缓存$today = date("Y-m-d");
$r = mysql_query("SELECT username FROM user WHERE signup_date >= '$today'");
上面两条sql语句的差别就是CURDATE(),mysql的查询缓存对这个函数不起作用。所以,像NOW()和RAND()或是其他的诸如此类的sql函数都不会开启查询缓存,因为这些函数的返回是会不定的易变的。所以,你所需要的就是用一个变量来代替mysql的函数,从而开启缓存。
2. EXPLAIN你的SELECT查询
使用EXPLAIN关键字可以让你知道mysql是如何处理你的sql语句的。这可以帮你分析你的查询语句或是表结构的性能瓶颈。
EXPLAIN的查询结果还会告诉你你的索引主键被如何利用的,你的数据表是如何被搜索和排序的,,等等。
挑一个你的SELECT语句(推荐复杂的,有多表关联的),把关键字EXPLAIN加到前面,你可以使用phpmyadmin来做这个事,然后你会看到一张表格。下面的示例中,我们没有加上group_id索引,并且有表连接:
当我们为group_id字段加上索引后:
我们可以看到,前一个显示了7883行,而后一个只是搜索了两个表的9行和16行,查看rows列可以让我们看到潜在的性能问题,这些都是影响数据库查询速度的重要因素。
3. 当只要一行数据时使用LIMIT 1
当你查询表的时候,知道结果只有一条结果,但因为你可能去fetch游标,或者是去检查返回的记录数。
在这种情况下,加上LIMIT 1可以增加性能,这样一来,mysql数据库引擎会在找到一条数据后停止搜索,而不是继续往后查找下一条符合条件的记录。
下面的示例,只是为了找一下是否有“中国”用户,很明显,后面的会比前面的更有效率(第一条是select *,第二条是select 1)
// 没有效率的:$r = mysql_query("SELECT * FROM user WHERE country = 'China'");
if (mysql_num_rows($r) > 0) {
// ...
}// 有效率的:$r = mysql_query("SELECT 1 FROM user WHERE country = 'China' LIMIT 1");
if (mysql_num_rows($r) > 0) {
// ...
}4. 为搜索字段建索引
索引并不一定是给主键或是唯一的字段,如果在你的表中,有某个字段你总会经常用来做搜索,那么,请为其建立索引。
从上图可以看出那个搜索字符串“last_name LIKE 'a%'”,一个建了索引,一个没有索引,性能差了四倍左右。
另外,你应该知道什么样的搜索是不能正常使用索引的,如当在一个大文章中搜索一个词时,如:“WHERE post_content LIKE '%apple%'”,索引是没有意义的,你可能需要使用全文索引或是自己做一个索引(如:搜索关键词或是Tag什么的)。
5. 在join表的时候使用相同类型,并将其索引
如果你的应用程序有很多JOIN查询,你应该确认两个表中join的字段是被建过索引的。这样mysql内部会启动为你优化join的sql语句的机制。
而且,这些被用来join的字段,应该是相同的类型的,例如:如果你要把DECIMAL字段和一个INT字段join在一起,mysql就无法使用它们的索引,对于那些STRING类型,还需要有相同的字符集才行(两个表额字符集可能不一样)。
// 在state中查找company$r = mysql_query("SELECT company_name FROM users
LEFT JOIN companies ON (users.state = companies.state)
WHERE users.id = $user_id");
// 两个 state 字段应该是被建过索引的,而且应该是相当的类型,相同的字符集。6. 千万不要ORDER BY AND()
想打乱返回的数据行?随机挑一个数据?这样做存在很大性能问题。
如果你真想把返回的数据行打乱了,你有N种方法可以达到此目的。这里的问题是:mysql会不得不去执行RAND()函数(很消耗cpu时间),而且这是为了每一行数据去记行,然后再对其排序,就算是你用了limit也无济于事(因为要排序)
下面的示例是随机挑一条记录
|
1
2
3
4
5
6
7
8
9
|
// 千万不要这样做:$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");
|
7. 避免select *
从数据库中读出越多的数据,那么查询就会变得越慢,并且,如果你的数据库服务器和WEB服务器是两台独立的服务器的话,这还会增加网络传输的负载。
所以,应该养成需要什么就取什么的习惯。
// 不推荐$r = mysql_query("SELECT * FROM user WHERE user_id = 1");
$d = mysql_fetch_assoc($r);
echo "Welcome {$d['username']}";
// 推荐$r = mysql_query("SELECT username FROM user WHERE user_id = 1");
$d = mysql_fetch_assoc($r);
echo "Welcome {$d['username']}";
8. 永远为每张表设置一个ID
我们应该为数据库中的每张表设置一个ID作为其主键,而且最好是一个INT型的(推荐使用UNSIGNED),并且设置上自动增加的AUTO_INCREMENT标志。
就算你的user表有一个主键叫“email”的字段,也别让他成为主键,使用varchar类型来当主键会使得性能下降。另外,在你的程序中,你应该使用表的ID来构造你的数据结构。
而且,在mysql数据引擎下,还有一些操作需要使用主键,这样的话,主键的性能设置就变的非常重要,如集群,分区,,
在这里,只有一种情况是例外,那就是‘关联表’的‘外键’,也就是说,这个表的主键,通过若干个别的表的主键构成,我们把这个情况叫做‘外键’,如一个学生表有学生的ID,有一个课程表有课程ID,那么成绩表就是关联表了,其关联了学生表和课程表,在成绩表中,学生ID和课程ID叫外键,其共同组成主键。
9. 使用ENUM而不是VARCHAR
enum类型是非常快和紧凑的。实际上,其保存的是TINYINT,但其外表显示为字符串。这样一来,用这个字段来做一些选项列表就变得相当完美。
如果你有一个字段,如“性别”,“国家”,“名族”,“状态”或“部门”,知道这字段的取值是有限的而且固定的,那么你应该使用enum而不是varchar。
mysql也有一个建议(见第十条)告诉你怎么去重新组织你的表的结构,当你有一个varchar字段时,这个建议会告诉你改成enum类型,使用PROCEDURE ANALYSE()你可以得到相关的建议。
10. 从procedure analyse()取得建议
procedure analyse()会告诉你分析你的字段和其实际的数据,并会给你一些有用的建议,只有表中有实际的数据,这些建议才会变的有用,因为操作是需要数据的。
例如,如果你创建了一个INT字段作为你的主键,然而并没有太多的数据,那么,procedure analyes()会建议你把这个字段类型改成mediumint或是你使用了一个varchar字段,因为数据不多,你可能会得到一个让你把它改成enum的建议。这些建议,都是因为数据不够多,所以决策做的不够准。
只有当你的数据越来越多的时候,建议才会变得精准。
11. 尽可能的使用NOT NULL
除非你有一个很特别的原因使用NULL,你应该总是让你的字段保持NOT NULL。
首先,“empty”和“NULL"有多大的区别,(如果是0和NULL),如果你觉得他们之间没有什么区别。那么你就不要使用NULL。
NULL需要额外的存储空间,并且,在你比较的时候,你的程序会更复杂,当然,这里并不是说你就不能使用NULL了,现实情况很复杂,依然会有些情况需要使用NULL。
“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.”。
Mysql难以优化引用可空列查询,它会使索引、索引统计和值更加复杂。可空列需要更多的存储空间,还需要mysql内部进行特殊处理。可空列被索引后,每条记录都需要一个额外的字节,还能导致MYisam 中固定大小的索引变成可变大小的索引。
12. Prepared Statements
Prepared Statement很像存储过程,是一种运行在后台的sql语句集合,我们可以从使用prepared statements获得很多好处,无论是性能问题还是安全问题。
Prepared Statement可以检查一些你绑定好的变量,这样就可以保护你的程序不会受到sql注入的攻击,当然,你也可以手动检查你的这些变量,然而,手动的检查容易出现问题,而且经常被程序员遗忘。当我们使用framework或是ORM的时候,这样的问题会好一些。
在性能方面,当一个相同的查询使用很多次的时候,你可以给这些Prepared Statement定义一些参数,而mysql只会解析一次。
最新版本的mysql在传输prepared statements是使用二进制形式,这会使得网阔传输非常有效率。
13. 把IP地址存成UNSIGNED INT
很多程序员都会创建一个varchar(15)字段来存储字符串形式的ip而不是整型的ip,如果用整型来存储,只需要4个字节,并且你可以有定长的字段。这回为你带来查询上的优势。尤其当你需要使用这样的where条件:ip between ip1 and ip2。
我们必须使用UNSIGNED INT ,因为ip地址会使用整个32位的无符号整形。
14. 固定长度的表会更快
如果表中的所有字段都是“固定长度的”,整个表会被认为是“static”或“fixed-length”,如,表中没有如下类型的字段:VARCHAR,TEXT,BLOB,只要你使用了其中一个这些字段,那么这个表就不是固定长度了。这样,mysql引擎会使用另一种方法来处理。
固定长度的会提升性能,因为mysql搜寻会更快一点,因为这些固定长度是很容易计算下一个数据的偏移量的,所以读取自然就会很快,而如果不是定长的,每一次要找下一条的话,需要程序找到主键。
并且,固定长度的表也更容易被缓存和重建,固定长度的字段会浪费一些空间,因为你用不用,他都会分配那么长空间。
使用“垂直分割技术”,你可以分割你的表成为两个一个是定长的,一个则是不定长的。
15. 垂直分割
“垂直分割”是一种把数据库中的表按列变成几张表的方法。这样就可以降低表的复杂度和字段的数目,从而达到优化的目的。
示例一:在user表中有一个字段是家庭地址,这个字段是可选字段,相比起,并且你在数据库操作的时候除了个人信息外,并不需要经常读取或是改写这个字段,那么为什么不把它放到另一个表中?小一点的表总会有好的性能。
示例二:你有一个“last_login”字段,在你每次登陆的时候都会刷新该字段,但是,每次更新都会导致该表的查询缓存被清空。所以可以把这个字段放到另一个表中,这样就不会应影响你的查询缓存了。
另外,需要注意的是。这些被分出去的字段形成的表,你不会经常去join他们,不然这样的话,性能比不分割之前还要差。
17. 拆分大的DELETE 或INSERT语句
如果需要执行一个很大的delete或者insert操作,要小心,这些操作是会锁表的,别的操作都进不来。这样对并发访问这些表的操作将带来很大的影响,所以我们可以查分成多个delete或分多次insert。
18. 越小的列会越快
对于大多数数据库引擎来说,硬盘操作都是最重大的瓶颈。所以把你的数据变得紧凑会对这种情况非常有帮助,这样将会减少对硬盘的访问。
19. 选择正确的存储引擎
在mysql中有两个存储引擎mylsam和innodb,每个存储引擎都有利有弊,可以查看:《mysql:InnoDB还是MylSAM》讨论这个事情。
MYlSAM适合一些需要大量查询的应用,但其对于有着大量写操作并不是很好,甚至你只需要update一个字段,整个表都会被锁住,而别的进程,就算是读进程都无法操作直到update操作完成,另外,没有了SAM对于select count(*)是非常快的。
InnoDB的趋势是一个非常复杂的存储引擎,对于一些小的应用,他会比mylSAM还慢,它支持行级锁,于是在写操作较多的时候,会更加优秀,并且他还支持更高的应用,如事务。
20. 使用一个对象关系映射器(Object Relational Mapper)
使用ORM,你能获得可靠的性能增长,一个ORM可以做的所有事情,也能被手动的编写出来。但是这需要一个高级专家。
ORM的最重要的是“lazy loading”(延迟加载),只有在你需要的时候才去取值,但这种机制也有副作用,因为很有可能会去创建很多小的查询反而会降低性能。