一、引言
本规范基于PHP PEAR编码规范及PHPDocumentor注释规范等编程原则组成,融合并提炼了开发人员长时间积累下来的成熟经验,意在帮助形成良好一致的编程风格。以达事半功倍的效果。为了与时俱进,根据客观需求,本文档会不定期更新。
作者:tommy < [email protected] >
版权:DoitPHP Group < www.doitphp.com >
更新日期:2010年9月25日
二、适用范围
如无特殊说明,以下规则要求完全适用于DoitPHP项目(注:Doitphp的PHP框架文件,而非使用DoitPHP所开发的PHP项目)。如果你喜欢以下编码规范,也可以用在其它PHP开发项目。
三、 标准化的重要性和好处
“不以规矩,不成方圆”,当一个软件项目的开发遵守公共一致的标准时,整个团队成员形成并保持一致的编码风格,整个项目文件如同一人所写。每个程序员的代码都易于为他人所理解,提高了代码的可维护性,从而减少了软件的维护成本。同时使新的参与者可以很快的适应环境,从而最大限度的提高团队开发的合作效率。长期的规范性编码还可以让开发人员养成好的编码习惯,可以减少编码出错的机会,甚至锻炼出更加严谨的思维。将规范“令之以文”是为了明确我们的编码标准。我们相信:标准不是项目成功的关键,但可以帮助我们在团队协作中有更高的效率并且更加顺利的完成既定的任务。
四、 PHP编码规范与原则
1、文件格式
1.1、文件编码:文件编码统一为UTF-8(注:非UTF-8+BOM)。
1.2、PHP代码标记:任何时候都要使用“<?php ?>”来定义你的PHP代码。而“<? ?>”将禁止使用。对于只含有php的代码文件,建议将文件结尾处的“?>”忽略掉,防止多余空格或其它字符影响代码。
1.3、缩进规则:缩进使用4个空格,而不是 TAB。这已经是PHP业界的标准了,我们也不会“逆历史潮流而动”。使用空格主要是为了代码美观整齐。因为在不同的编辑器里, TAB 制表符的长度是不一样的,而空格则是一样的。使用TAB按下TAB键能解决的问题,使用空格则要按四次空格键,明显影响开发效率。目前很多编辑器默认一个TAB占用四个空格位置(占用几个空格,对此编辑器可以调节),如果是这样,为了提高开发效率,可以放心大胆地使用TAB。本缩进规范也适用于JavaScript中的函数、类、逻辑结构、循环等。
1.4、代码内容:每行结尾不允许有多余的空格或TAB制表符(确保你的编辑器保存文件为 Unix 格式,这意味着行是以换行符终止的)。除了语言包注释配置文件,其它地方不能有中文。
1.5、代码注释:文件要有清晰的代码注释,注释风格采用phpDocumentor标准(相关网址:http://www.phpdoc.org/)
2、命名约定
命名是程序规划的核心。古人相信只要知道一个人真正的名字就会获得凌驾于那个人之上的不可思议的力量。只要你给事物想到正确的名字,就会给你以及后来的人带来比代码更强的力量。
名字就是事物在它所处的生态环境中一个长久而深远的结果。总的来说,只有了解系统的程序员才能为系统取出最合适的名字。如果所有的命名都与其自然相适合,则关系清晰,含义可以推导得出,一般人的推想也能在意料之中。
就一般约定而言,类、函数和变量的名字应该总是能够描述让代码阅读者能够容易的知道这些代码的作用。形式越简单、越有规则,就越容易让人感知和理解。应该避免使用模棱两可,晦涩不标准的命名。
2.1、文件名:文件名由字母数字和下划线组成,为方便和兼容不同操作系统,推荐字母统一小写。短横线 ("-")和空格是绝对不允许的。
2.2、文件扩展名:php文件扩展名为".php"(这个地球人都知道),不过下面几种情况例外:
类文件扩展名为“.class.php”且文件命名和类名一致(字母大小写也要一致);
配置文件扩展名为“.ini.php”;
函数文件扩展名为“.fun.inc.php”;
不能通过浏览器直接访问的php文件(用于被incldue调用的)扩展名建议为“.inc.php”。
2.3、类名:类名只允许有字母,数字字符和下划线,在大部分情况下不鼓励使用数字和下划线。如果类名包含多个单词,每个单词的第一个字母必须大写(“驼峰”命名规则),连续的大写是不允许的。接口类 (interface) 的定义必须遵循类名的定义规范,不同的是必须要以 _Interface 作为结尾。
注:对于类名中使用下划线,在Zend framework的编码规范中,类名中使用“_”则表示一个目录。class Db_Mysql表示该类文件存放于Db目录下,类名中多一个“_”,意味着多一个目录。对此,我们也应推荐这个类的命名规范,所以不鼓励类名中使用下划线。
2.4、函数名:函数名只允许由字母,数字或下划线组成,字母应当全部小写。函数名应该具有描述性,当由多个单词组成时,单词之间以单个下划线分隔(首字母要小写,其后每个单词首字母要大写,即所谓的 “camelCaps” 命名规则。此规则是允许的,为了统一规范,我们对此不推荐)。函数名越详细越好,名称中某处最好有一个动词。较好的函数名称如print_login_status(), get_user_data(),等等。
2.5、变量名:变量名只允许由字母和下划线组成,数字是不允许的,变量名应当全部小写,并且词语之间以单个下划线分隔(“camelCaps” 规则的变量名当然也是合法的,但非常不推荐。因为大多中国人对大小写字母混合的变量名很不习惯,《卓有成效的程序员》一书也是十分推崇下划线式的命名规则。使用下划线命名规则,也使用“camelCaps”规则,非常不利于编码规范的统一)。变量名应当是描述性的,并且简明。禁止使用巨大的句子作为变量名。
例如: $current_user 是正确的, 但是 $currentuser 和 $currentUser 就不正确。
2.6、常量名:常量名必须仅包括字母,数字和下划线,而且必须全部大写,为了便于理解,使用下划线分割每个单词。推荐用常量所在的包名或者是类名作为前缀。比如,对于Bug类中常量应该以Bug_开始。常量应该在类中由 const 声明并定义,全局范围内的 define 是不鼓励使用的。
2.7、函数参数:函数或方法的参数命名原则和变量名的命名规范相同。我们不希望这样的函数参数:do_it($a, $b, $c)。在大部分情况下,我们愿意仅仅看看函数声明就知道怎样使用它。
2.8、PHP的内建值:英文字母统一小写(__LINE__, __FILE__等特殊内建值除外),boolean 值和 null 值统一小写(即null, false, true,而不是NULL, FALSE, TRUE)。退出关键字统一为exit(),禁用exit;
3、编程风格
3.1、文件头的注释声明:
所有DoitPHP的文件,在文件开始的时候,你必须加入以下的注释声明:
/**
* 文件名
*
* 文件内容简介
* @package DoitPHP
* @author 开发者 <开发者邮箱>
* @copyright Copyright (c) 发行年份
* @license New BSD License {@link http://www.doitphp.com/license/}
* @version $id: 版本号
*/
3.2、代码注释:
代码注释符必须用 "/*" 或者 "/**" 作为开头。"#" 是不允许的,而 “//” 的使用仅限于函数内部。
注释风格使用phpDocumentor标准。
下面是类的注释模板
/**
* 类的作用等详细描述,
*
* @package 所在的包名
* @author 开发者
* @version $id:版本号
*/
注:至少类的详细描述不能少,@package, @author, $version可以省略。
下面是函数或类的方法注释模板
/**
* 函数名或函数的简短简介
*
* 函数内容的详细说明,描述函数的作用,用法及内部
* 操作原理等
*
* @params 参数1的数据类型 参数1 参数1的简短描述
* @params 参数2的数据类型 参数2 参数2的简短描述
* @return 返回结果的数据类型 返回结构的描述
*/
注:return 共分为boolean, string, mixed, integer, null。当没有数据返回时应是return void。
"void" 和 "null" 是不一样的。"null" 意味着返回一个空的变量。而 "void" 则表示什么都没有返回。
当或返回boolean,或返回string时,应是return boolean|string。
3.3、字符串引用(单引号与双引号):
在 PHP 中有两种不同的方式引用字符串,使用单引号或使用双引号。主要区别是解析器在双引号括起的字符串中执行变量替换,却不在单引号括起的字符串中执行。单引号中,任何变量($var)、特殊转义字符(如“\t \r \n”等)不会被解析,因此PHP的解析速度更快,转义字符仅仅支持“\’”和“\\”这样对单引号和反斜杠本身的转义。因此,应当始终使用单引号除非你明确地需要对那个字符串进行变量替换。这样,我们可以节省解析器解析一堆不需要执行替换的字符串的麻烦。同样,如果你使用字符串变量作为函数调用的一部分,你不需要用引号把那个变量括起来,那样只会给解析器增加不必要的工作。无论如何,要注意几乎所有双引号中的转义序列在单引号中都不会起作用。
具体方法:
当一个字符串是纯文字组成的时候(即不含有变量),则必须总是以单引号(')为定界符;当一个字符串含有撇号(`)或单引号的时候,我们允许使用双引号(")来定界字符串,特别是在些 SQL 语句的时候。
注:变量替换中的变量只允许用 $+变量名 的形式。
例如:
$greeting="Hello $name, welcome back!"; // 允许使用
$greeting="Hello {$name}, welcome back!";// 鼓励使用
$greeting="Hello ${name}, welcome back!";// 不允许使用
/* 错误 */
$str = "This is a really long string with no variables for the parser to find.";
do_it("$str");
/* 正确 */
$str = 'This is a really long string with no variables for the parser to find.';
do_it($str);
注:在绝大多数可以使用单引号的场合,禁止使用双引号。可以或必须使用单引号的情况包括但不限于下述:
(1)字符串为固定值,不包含“\t”等特殊转义字符。
(2)数组的固定下标,例如$array[‘key’]。
(3)表达式中不需要带入变量,例如$string = ‘test’;,而非$string = “test$var”。
3.4、数组:
数组元素间要有逗号和空格隔开,便于阅读。数组的键值不推荐使用负数。
/*错误*/
$abc = array(1,2,3);
/*正确*/
$abc = array(1, 2, 3);
/*鼓励*/
$data_array = array(
'name'=>'streen003',
'age'=>'27',
);
当数组元素为数组时,其它元素要另起一行。
$demo_array = array(
array(12, 23, 35),
array(33, 45, 'abc'),
);
注:虽然在 PHP 中,使用一个不用引号括起来的字符串作为一个相关数组的键值是合法的。但是我们还是要求:数组中,如果下标不是整型,而是字符串类型,请务必用单引号将下标括起,正确的写法为$array[‘key’],而不是$array[key],因为不正确的写法会使PHP解析器认为key是一个常量,进而先判断常量是否存在,不存在时才以“key”作为下标带入表达式中,同时出发错误事件,产生一条Notice级错误。
/* 错误 */
$foo = $assoc_array[blah];
/* 正确 */
$foo = $assoc_array['blah'];
3.5、控制结构:
大括号(“{}”):首括号与关键词同行,尾括号与关键字同列。
(1)条件语句(if,switch,,while,: ?):
/*正确*/
If ($a > $b) {
echo $a, '大于', $b;
} else {
echo $a, '不大于', $b;
}
/*错误*/
if($a > $b)
{
echo $a, '大于', $b;
}
else
{
echo $a, '不大于', $b;
}
/*错误*/
if($a > $b)
echo $a, '大于', $b;
else
echo $a, '不大于', $b;
/*推荐*/
echo ($a > $b) ? $a.'大于'.$b : $a.'不大于'.$b;
在if结构中,在关键字(如if )后面要空一个格,然后再跟控制的左括号“(”。if和else if与前后两个圆括号同行。大括号“{”和右括号")"间有一个空格。左右两个括号“()”,与括号内的代码是紧贴的,没有空格。另外,即便if后只有一行语句,仍然需要加入大括号,以保证结构清晰。(注:在if结构中,没有关键词elseif,只有else if)。
对于简单的if结构,推荐使用三元符(? :),减少代码编写。当有三个或三个以上的条件判断时,尽可能的使用switch()结构来处理。
在switch结构中,通常当一个case块处理后,将跳过之后的case块处理,因此大多数情况下需要添加break。break的位置视程序逻辑,与case同在一行,或新起一行均可,但同一switch体中,break的位置格式应当保持一致。"switch" 语句中应该总是包括 "default" 控制。
例如:
switch ($var)
{
case 1: echo ‘var is 1’; break;
case 2: echo ‘var is 2’; break;
default: echo ‘var is neither 1 or 2’; break;
}
或
switch ($var)
{
case 1: echo ‘var is 1’;
break;
case 2: echo ‘var is 2’;
break;
default: echo ‘var is neither 1 or 2’;
break;
}
(2)循环语句(foreach, for):
在循环语句中,只要有可能就采用foreach()结构(在使用foreach结构之前最好对所要进行遍历的数组进行is_array()的判断,以防对非数组类型的变量报错)。
foreach结构和for结构的编码规范与上文if结构的编码规则相同。
例如:
foreach($data as $key=>$value) {
echo $key, ':', $vaule;
}
在for()结构时,循环计数器允许使用一个单字符变量的唯一情形是当它作为一个循环结构的计数器的时候。在这种情况下,外层循环的计数器应当始终是 $i。如果有一个循环处于那个循环的内部,它的计数器应当是 $j,继而是 $k,等等。如果这个循环是用某个已经存在并且具备有意义的名字的变量计数的,本规范并不适用。
例如:
for($i=0; $i<23; $i++){
...
for($j=1; $j=5; $j++){
...
}
...
}
3.6、运算符:
运算符之间使用空格,这是不用太多努力而保持代码可读性的另一个简单,容易的步骤。 无论何时你写一个赋值,表达式,等等,始终在符号之间保留一个空格。基本上,把代码当作英语来写。在变量名和运算符之间插入空格。不要在左括号“(”后或者右括号“)”前加空格。不要在逗号或者分号之前加空格。
例如:
/* 每一对给出了错误方式,紧跟正确方式*/
$i=0;
$i = 0;
if($i<7) ...
if ($i < 7) ...
if ( ($i<7)&&($j>) ...
if (($i < 7) && ($j > ) ...
do_it( $i, "foo", $b );
do_it($i, "foo", $b);
for($i=0;$i<$size;$i++) ...
for($i = 0; $i < $size; $i++) ...
$i=($j<$size)?0:1;
$i = ($j < $size) ? 0 : 1;
对于运算符优先级,始终通过用括号强制一个表达式的优先级来使优先级明显。
例如:
/* 结果是什么?谁知道? */
$bool = ($i < 7 && $j > 8 || $k == 4);
/* 现在你确定这里我在做什么了 */
$bool = (($i < 7) && (($j < || ($k == 4)))
3.7、连接符:
多个字符串必须用点号 "." 来连接,且字符串与点号间必须用一个空格隔开。为了增强代码的可读性,我们推荐当用点号 "." 连接各字符串的时候,我们允许把它分割成多行以增强可读性。在这种情况下,点号 "." 必须与等于号 "=" 对齐。
例如:
$sql = "SELECT `id`, `name` FROM `people` "
. "WHERE `name` = 'Susan' "
. "ORDER BY `name` ASC ";
3.8、简化运算符:
自增($i++)和自减($i--)运算符
/* 错误 */
$array[++$i] = $j;
$array[$i++] = $k;
/* 正确 */
$i++;
$array[$i] = $j;
$array[$i] = $k;
$i++;
/* 允许 */
$i = $i + 3;
/* 推荐 */
$i += 3;
3.9、函数调用:
对于函数调用,函数名和左括号“(”之间不应该有空格,对于函数参数必须用 逗号+空格 来分隔,最后一个参数和右括号“)”之间不能有空格。下面是一个标准的函数调用:
/* 正确 */
$result = foo($param1, $param2, $param3);
/* 不正确 */
$result=foo ($param1,$param2,$param3);
$result=foo( $param1,$param2, $param3 );
此外,如果要将函数的返回结果赋值,那么在等号和所赋值的变量之间要有空格,同时,如果是一系列相关的赋值语句,你添加适当的空格,使它们对齐。
例如:
$result1 = $foo($param1, $param2, $param3);
$var2 = $foo($param3);
$var3 = $foo($param4, $param5);
3.10、函数定义:
左大括号“{”必须与函数名同行。推荐“{”与函数名间隔一空格,这样代码更加清晰。函数定义中的左括号“(”,与函数名紧挨,中间无需空格。
函数参数的名字和变量的命名规范一致,参数必须用逗号+空格来分隔。具有默认值的参数应该位于参数列表的后面。若具有默认值的参数数据类型不为boolean时,默认值为空时不允许用false,而是用null。
引用只允许定义在函数参数中,实时传递引用是禁止的。其实“&”引用,我们是非常不推崇的,这样会给别人阅读、理解代码无端地增加了难度。
左右小括号"()",与参数紧贴,不允许有空格。函数返回值不可以用括号包住,这样做除了增加阅读代码时不必要的麻烦外,没有其它益处。如外必须仔细检查并切实杜绝函数起始缩进位置与结束缩进位置不同的现象。
/* 正确 */
function 函数名(参数1, 参数2, 参数3) {
....
}
/* 错误 */
function 函数名(参数1,参数2,参数3){
....
}
3.11、类的定义:
左大括号"{"必须与类名同行,且与类名间隔一空格。
/*正确*/
class demo {
...
}
/*错误*/
class demo
{
...
}
类定义必须拥有符合 phpDocumentor 标准的注释块。
例如:
/**
* 类定义注释
*/
class DoitClass {
}
任何类变量的声明都必须放在类顶部,先于任何函数的声明。
不允许用 var 符号来声明变量,类成员变量必须以 private,protected 和 public 来声明。
其次,把类成员声明为 public 而直接引用虽然是允许的,但通常更好的方法是使用 get 和 set 方法来访问类成员。作用域为private,protected的变量,推荐变量名以“_”为前缀。例如:private $_vars;
方法必须总是用 private,protected 或者 public 来声明其作用域。静态 static 方法也应该声明其作用域。
方法的编码规范与函数的规范相同,可以参考上文提及的函数的编码规范。
3.12、SQL 语句:
所有SQL语句中,除了表名、字段名称以外,全部语句和函数均需大写,应当杜绝小写方式或大小写混杂的写法。
对于很长的SQL语句不要尝试去做诸如在 SQL 代码中实现列对齐此类的麻烦事。推荐把语句断行到它们单独的行上去,注意在哪里断行,大写,和括号的用法。
例如:
SELECT field1 AS something, field2, field3
FROM table a, table b
WHERE (this = that) AND (this2 = that2)
SQL insert 语句:SQL INSERT 语句可以写成两种不同方式。或者你明确说明要被插入的行,或者你已经知道数据中各行的顺序并且不用详细指定它们。我们希望使用前一种方法,也就是详细说明哪些行将被插入。这意味着我们的应用程序级代码不会依赖于数据库中字段的顺序,也不会因为我们增加另外的字段而崩溃(当然,除非它们被指定为 NOT NULL)。
例如:
/*这不是我们想要的*/
INSERT INTO mytable
VALUES ('something', 1, 'else')
/*这是正确的*/
INSERT INTO mytable (column1, column2, column3)
VALUES ('something', 1, 'else')
在SQL语句中,所有int数据都不得加单引号,但是在进行sql查询之前都必须经过intval函数处理;所有字符串都必须加单引号,以避免可能的注入漏洞和SQL错误。正确的写法为:
$catid = intval($catid);或 $catid = (int)$catid;
SELECT * FROM phpcms_member WHERE username=’$_username’ AND catid=$catid;
所有数据在插入或更改数据库之前,均需要进行addslashes()处理,以免特殊字符未经转义在插入数据库的时候出现错误(如果进行model操作,model的insert与update方法则会自动进行addslashes()处理)。
注:调用SQL语句时,SQL语句要加双引号。
例如:
$sql = "SELECT field1 AS something, field2, field3....";
$update_sql = "UPDATE table_name SET name='{$name}' WHERE id={$id}";
五、数据库设计
注:以下内容摘自《PHPCMS编码规范》,略有改动。
1.字段
1.1. 表和字段命名
表和字段的命名以上文的命名约定为基本准则。
所有数据表名称,只要其名称是可数名词,则必须以复数方式命名,例如:doit_member(用户表);存储多项内容的字段,或代表数量的字段,也应当以复数方式命名,例如:hits(查看次数)、items(内容数量)。
当几个表间的字段有关连时,要注意表与表之间关联字段命名的统一,如doit_article_1表中的articleid与doit_article_data_1表中的articleid。
代表id自增量的字段,通常用以下几种形式:
一般情况下,使用全称的形式,例如userid、articleid;
没有功能性作用,只为管理和维护方便而设的id,可以使用全称的形式,也可只将其命名为id。
篇幅所限,无法一一赘述,但所有与表、字段相关的命名,请务必大量参考doit现有字段的命名方式,以保证命名的系统性和统一性。
1.2. 字段结构
允许NULL值的字段,数据库在进行比较操作时,会先判断其是否为NULL,非NULL时才进行值的必对。因此基于效率的考虑,所有字段均不能为空,即全部NOT NULL;
预计不会存储非负数的字段,例如各项id、发帖数等,必须设置为UNSIGNED类型。UNSIGNED类型比非UNSIGNED类型所能存储的正整数范围大一倍,因此能获得更大的数值存储空间;
存储开关、选项数据的字段,通常使用tinyint(1)非UNSIGNED类型,少数情况也可能使用enum()结果集的方式。tinyint作为开关字段时,通常1为打开;0为关闭;-1为特殊数据,例如N/A(不可用);高于1的为特殊结果或开关二进制数组合(详见doit中相关代码);
MEMORY/HEAP类型的表中,要尤其注意规划节约使用存储空间,这将节约更多内存。例如cdb_sessions表中,就将IP地址的存储拆分为4个tinyint(3) UNSIGNED类型的字段,而没有采用char(15)的方式;
任何类型的数据表,字段空间应当本着足够用,不浪费的原则,数值类型的字段取值范围见下表:
字段类型 存储空间(b) UNSIGNED 取值范围
tinyint 1 否 -128~127
是 0~255
smallint 2 否 -32768~32767
是 0~65535
mediumint 3 否 -8388608~8388607
是 0~16777215
int 4 否 -2147483648~2147483647
是 0~4294967295
bigint 8 否 -9223372036854775808
~9223372036854775807
是 0
~18446744073709551615
2.SQL语句
所有SQL语句中,除了表名、字段名称以外,全部语句和函数均需大写,应当杜绝小写方式或大小写混杂的写法(这点在上文已提及过)。例如select * from doit_member;是不符合规范的写法。
很长的SQL语句应当有适当的断行,依据JOIN、FROM、ORDER BY等关键字进行界定。
通常情况下,在对多表进行操作时,要根据不同表名称,对每个表指定一个1~2个字母的缩写,以利于语句简洁和可读性。
如下的语句范例,是符合规范的:
$result = $db->query(”SELECT m.*, i.*
FROM “.TABLE_MEMBER.” m, “.TABLE_MEMBERINFO.” i
WHERE m.userid=i.userid AND m.userid=’$_userid’);
3.性能与效率
3.1. 定长与变长表
包含任何varchar、text等变长字段的数据表,即为变长表,反之则为定长表。
对于变长表,由于记录大小不同,在其上进行许多删除和更改将会使表中的碎片更多。需要定期运行OPTIMIZE TABLE以保持性能。而定长表就没有这个问题;
如果表中有可变长的字段,将它们转换为定长字段能够改进性能,因为定长记录易于处理。但在试图这样做之前,应该考虑下列问题:
使用定长列涉及某种折衷。它们更快,但占用的空间更多。char(n) 类型列的每个值总要占用n 个字节(即使空串也是如此),因为在表中存储时,值的长度不够将在右边补空格;
而varchar(n)类型的列所占空间较少,因为只给它们分配存储每个值所需要的空间,每个值再加一个字节用于记录其长度。因此,如果在char和varchar类型之间进行选择,需要对时间与空间作出折衷;
变长表到定长表的转换,不能只转换一个可变长字段,必须对它们全部进行转换。而且必须使用一个ALTER TABLE语句同时全部转换,否则转换将不起作用;
有时不能使用定长类型,即使想这样做也不行。例如对于比255字符更长的串,没有定长类型;
在设计表结构时如果能够使用定长数据类型尽量用定长的,因为定长表的查询、检索、更新速度都很快。必要时可以把部分关键的、承担频繁访问的表拆分,例如定长数据一个表,非定长数据一个表。例如doit的doit_member表等。因此规划数据结构时需要进行全局考虑;
进行表结构设计时,应当做到恰到好处,反复推敲,从而实现最优的数据存储体系。
3.2. 运算与检索
数值运算一般比字符串运算更快。例如比较运算,可在单一运算中对数进行比较。而串运算涉及几个逐字节的比较,如果串更长的话,这种比较还要多。
如果串列的值数目有限,应该利用普通整型或emum类型来获得数值运算的优越性。
更小的字段类型永远比更大的字段类型处理要快得多。对于字符串,其处理时间与串长度直接相关。一般情况下,较小的表处理更快。对于定长表,应该选择最小的类型,只要能存储所需范围的值即可。例如,如果mediumint够用,就不要选择bigint。对于可变长类型,也仍然能够节省空间。一个TEXT 类型的值用2 字节记录值的长度,而一个LONGTEXT 则用4字节记录其值的长度。如果存储的值长度永远不会超过64KB,使用TEXT 将使每个值节省2字节。
3.3. 结构优化与索引优化
索引能加快查询速度,而索引优化和查询优化是相辅相成的,既可以依据查询对索引进行优化,也可以依据现有索引对查询进行优化,这取决于修改查询或索引,哪个对现有产品架构和效率的影响最小。
索引优化与查询优化是多年经验积累的结晶,在此无法详述,但仍然给出几条最基本的准则。
首先,根据产品的实际运行和被访问情况,找出哪些SQL语句是最常被执行的。最常被执行和最常出现在程序中是完全不同的概念。最常被执行的SQL语句,又可被划分为对大表(数据条目多的)和对小表(数据条目少的)的操作。无论大表或小表,有可分为读(SELECT)多、写(UPDATE/INSERT)多或读写都多的操作。
对常被执行的SQL语句而言,对大表操作需要尤其注意:
写操作多的,通常可使用写入缓存的方法,先将需要写或需要更新的数据缓存至文件或其他表,定期对大表进行批量写操作。同时,应尽量使得常被读写的大表为定长类型,即便原本的结构中大表并非定长。大表定长化,可以通过改变数据存储结构和数据读取方式,将一个大表拆成一个读写多的定长表,和一个读多写少的变长表来实现;
读操作多的,需要依据SQL查询频率设置专门针对高频SQL语句的索引和联合索引。
而小表就相对简单,加入符合查询要求的特定索引,通常效果比较明显。同时,定长化小表也有益于效率和负载能力的提高。字段比较少的小定长表,甚至可以不需要索引。
其次,看SQL语句的条件和排序字段是否动态性很高(即根据不同功能开关或属性,SQL查询条件和排序字段的变化很大的情况),动态性过高的SQL语句是无法通过索引进行优化的。惟一的办法只有将数据缓存起来,定期更新,适用于结果对实效性要求不高的场合。
MySQL索引,常用的有PRIMARY KEY、INDEX、UNIQUE几种,详情请查阅MySQL文档。通常,在单表数据值不重复的情况下,PRIMARY KEY和UNIQUE索引比INDEX更快,请酌情使用。
事实上,索引是将条件查询、排序的读操作资源消耗,分布到了写操作中,索引越多,耗费磁盘空间越大,写操作越慢。因此,索引决不能盲目添加。对字段索引与否,最根本的出发点,依次仍然是SQL语句执行的概率、表的大小和写操作的频繁程度。
3.4. 查询优化
MySQL中并没有提供针对查询条件的优化功能,因此需要开发者在程序中对查询条件的先后顺序人工进行优化。例如如下的SQL语句:SELECT * FROM table WHERE a>’0’ AND b<’1’ ORDER BY c LIMIT 10;
事实上无论a>’0’还是b<’1’哪个条件在前,得到的结果都是一样的,但查询速度就大不相同,尤其在对大表进行操作时。
开发者需要牢记这个原则:最先出现的条件,一定是过滤和排除掉更多结果的条件;第二出现的次之;以此类推。因而,表中不同字段的值的分布,对查询速度有着很大影响。而ORDER BY中的条件,只与索引有关,与条件顺序无关。
除了条件顺序优化以外,针对固定或相对固定的SQL查询语句,还可以通过对索引结构进行优化,进而实现相当高的查询速度。原则是:在大多数情况下,根据WHERE条件的先后顺序和ORDER BY的排序字段的先后顺序而建立的联合索引,就是与这条SQL语句匹配的最优索引结构。尽管,事实的产品中不能只考虑一条SQL语句,也不能不考虑空间占用而建立太多的索引。
同样以上面的SQL语句为例,最优的当table表的记录达到百万甚至千万级后,可以明显的看到索引优化带来的速度提升。
依据上面条件优化和索引优化的两个原则,当table表的值为如下方案时,可以得出最优的条件顺序方案:
字段a 字段b 字段c
1 7 11
2 8 10
3 9 13
-1 0 12
最优条件:b<’1’ AND a>’0’
最优索引:INDEX abc (b, a, c)
原因:b<’1’作为第一条件可以先过滤掉75%的结果。如果以a>’0’作为第一条件,则只能先过滤掉25%的结果
注意1:字段c由于未出现于条件中,故条件顺序优化与其无关
注意2:最优索引由最优条件顺序得来,而非由例子中的SQL语句得来
注意3:索引并非修改数据存储的物理顺序,而是通过对应特定偏移量的物理数据而实现的虚拟指针
EXPLAIN语句是检测索引和查询能否良好匹配的简便方法。在phpMyAdmin或其他MySQL客户端中运行EXPLAIN+查询语句,例如EXPLAIN select * FROM table WHERE a>’0’ AND b<’1’ ORDER BY c;这种形式,即使得开发者无需模拟上百万条数据,也可以验证索引是否合理,相关细节请参考MySQL说明。
值得提出的是,Using filesort是最不应当出现的情况,如果EXPLAIN得出此结果,说明数据库为这个查询专门建立了一个用以缓存结果的临时表文件,并在查询结束后删除。众所周知,硬盘I/O速度始终是计算机存储的瓶颈,因此,查询中应当尽全力避免高执行频率的SQL语句使用filesort。尽管,开发者永远都不可能保证产品中的全部SQL语句都不会使用filesort。
限于篇幅,本文档远远没有涵盖数据库优化的方方面面,例如:联合索引与普通索引的可重用性、JOIN连接的索引设计、MEMORY/HEAP表等。数据库优化实际上就是在很多因素和利弊间不断权衡、修改,惟有在成功与失败经验中反复推敲才能得出的经验,这种经验往往就是最难能可贵和价值连城的。
3.5,数据库编码:统一使用UTF-8编码。