最近发现有个服务器上的网站打开速度很慢,有时候甚至出现完全打不开的情况。查看了一下服务器状态,发现服务器负载很大,最高时候,5分钟平均 load average 达到了 20,而服务器是 8 核 CPU。接着查看了一下服务的 IO 情况,发现 MySQL 读写非常频繁,如下图:

这显然是不正常的情况。接着,我又查看了一下 MySQL 的状态,发现出现了 Copying to tmp table 查询语句为:
SELECT ID, post_title, guid
问题应该就出现在这里了。大量临时表的读写显然会导致磁盘 IO 增加。按理说,WordPress 官方应该对这些查询做过一些优化,如果一个 WordPress 网站本身就很容易导致如此大的服务器压力,它应该也不会如此流行。于是百度搜索了一下:
SELECT ID, post_title, guid
发现网上有很多获取 WordPress 随机文章的教程,都包含类似的 SQL 查询,完整的语句大致是:
SELECT ID, post_title, guid
FROM XXX
WHERE post_status = 'publish'
AND post_title != ''
AND post_password = ''
AND post_type = 'post'
ORDER BY RAND()
LIMIT XXX
这里有一个 RAND() 函数,非常显眼。我猜想,当查询记录数量比较大的时候,使用:
ORDERBY RAND()
很可能会导致查询速度变慢。查询了一下相关资料,也发现 ORDER BY RAND() 在数据量比较大的情况下,确实容易产生比较大的查询开销。所以,服务器 IO 过高的问题基本已经找到了原因。但是,随机文章这个功能本身还是需要的。既然直接使用这个查询会影响性能,那么有没有其他方法呢?
肯定是有的。
这里我把原来的方法和我重新做的方法进行一下对比。下面是具体的函数。实验一,是网上比较常见的获取随机文章的方法;实验二,是我重新设计的方法。整个数据表大约有 9 万条记录,下面是测试代码。
实验一 ORDER BY RAND查询
$host = "localhost";
$username = "******";
$passwd = "******";
$dbname = "******";
$table = "en_posts";
$cono = mysql_pconnect(
$host,
$username,
$passwd
);
if(!$cono){
exit("数据库连接失败");
}
mysql_query("set names utf8");
mysql_select_db($dbname);
// 实验一,原来的查询
$begin = microtime(TRUE);
$sql = "SELECT ID, post_title, guid
from $table
where post_status = 'publish'
and post_title != ''
AND post_password = ''
AND post_type = 'post'
ORDER BY RAND()
LIMIT 0,5";
$result = mysql_query($sql, $cono);
while($list = mysql_fetch_row($result)){
print_r($list);
}
// 结束执行
$end = microtime(TRUE);
echo "\n<br/>实验一,执行时间:" .
($end - $begin) .
"秒";
实验二多次查询
// 实验二,新的查询
$begin = microtime(TRUE);
function getlist($num){
global $table;
$arr = array();
$count = mysql_fetch_row(
mysql_query(
"SELECT MIN(ID),MAX(ID) from $table"
)
);
$startid = $count[0];
$endid = $count[1];
$querycount = 0;
for($i = 0; $i < $num; $i++){
// 安全设置,保证不出现死循环
if($querycount - $num > 5){
break;
}
$id = rand(
$startid,
$endid
);
$sql = "SELECT ID, post_title, guid
from $table
where ID = $id
and post_status = 'publish'
and post_title != ''
AND post_password = ''
AND post_type = 'post'";
$result = mysql_query($sql);
if(!$result){
$i--;
continue;
}
$arr[] = mysql_fetch_row($result);
$querycount++;
}
return $arr;
}
print_r(getlist(5));
$end = microtime(TRUE);
echo "\n<br/>实验二,执行时间:" .
($end - $begin) .
"秒";
mysql_close($cono);
两次查询的结果如下:

可以看到,同样是随机取 5 条记录:实验一用了 35 秒多;实验二只用了 不到 60 毫秒。两者之间的差距非常大。
WordPress中的随机文章函数
经过测试之后,我把新的随机文章方法写成了一个 WordPress 函数。把下面的函数放到主题的 functions.php 中,就可以直接在页面中调用:
function random_posts(
$posts_num = 5,
$before = '<li>',
$after = '</li>'
){
global $wpdb;
$count = $wpdb->get_results(
"SELECT MIN(ID) as min,
MAX(ID) as max
from $wpdb->posts"
);
$startid = $count[0]->min;
$endid = $count[0]->max;
$querycount = 0;
for($i = 0; $i < $posts_num; $i++){
// 安全设置,保证不出现死循环
if($querycount - $posts_num > $posts_num){
break;
}
$id = rand(
$startid,
$endid
);
$sql = "SELECT ID, post_title, guid
from $wpdb->posts
where ID = $id
and post_status = 'publish'
and post_title != ''
AND post_password = ''
AND post_type = 'post'";
$randposts = $wpdb->get_results($sql);
if(!$randposts){
$i--;
continue;
}
$output = '';
foreach($randposts as $randpost){
$post_title =
stripslashes($randpost->post_title);
$permalink =
get_permalink($randpost->ID);
$output .=
$before .
'<a href="' .
$permalink .
'" rel="bookmark" title="' .
$post_title .
'">' .
$post_title .
'</a>' .
$after;
}
echo $output;
$querycount++;
}
}
再去测试,发现速度要快很多,服务器的 IO 也降低了不少。当然,这种方法也有一个问题:
在文章数量比较少的时候,可能会出现重复文章。
因为这里是根据文章 ID 的范围随机生成一个 ID,如果随机到的 ID 并不存在,或者不是符合条件的文章,就会重新进行查询。但是对于文章数量比较多的情况,例如上面测试的 9 万条记录,这种方式在实际使用中效果还是比较明显的。
总结
通过这次问题,我主要得到两个结论:
- MySQL 在执行某些大查询时,如果临时表无法完全放入内存,可能会使用磁盘临时表,从而增加磁盘 IO,并导致查询速度变慢。
- 使用网上找到的代码时一定要谨慎。
特别是一些看起来非常简单的 SQL,在数据量比较小的时候可能完全感觉不到问题,但是当数据量增加之后,就可能给服务器带来很大的压力。这次就是一个比较典型的例子。
评论0
欢迎分享你的看法,也欢迎补充不同的实践经验。