我正在使用PHP和MYSQL来绘制来自Asterisk CDR数据库的调用concurenncy,
我目前使用以下准备好的声明:
$query=$cdrdb->prepare('select count(acctid) from cdr where calldate between ? and ? or DATE_ADD(calldate, INTERVAL duration SECOND) between ? and ?');
然后使用以下foreach循环输入变量:
foreach ($timerange as $startdatetime){
$start=$startdatetime->format("Y-m-d H:i:s");
$enddatetime=new DateTime($start);
$enddatetime->Add($interval);
$end=$enddatetime->format("Y-m-d H:i:s");
if(!$query->execute(array($start, $end, $start, $end))){
echo "Execute failed: (" . $stmt->errno . ") " . $stmt->error;
}
if (!($res = $query->fetchall())) {
echo "Getting result set failed: ";
}
array_push($callsperinterval,$res[0][0]);
}
时间范围可以是每天一小时,一天一个月或一周一年。
calldate列被标记为索引列。
该表目前拥有122000条记录。
在查询上运行EXPLAIN的结果:
mysql> explain select count(acctid) from cdr where calldate between '2014-10-02 23:30:00' and '2014-11-03 00:00:00' or DATE_ADD(calldate, INTERVAL duration SECOND) between '2014-10-02 23:30:00' and '2014-11-03 00:00:00';
+----+-------------+-------+------+---------------+------+---------+------+--------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+------+---------+------+--------+-------------+
| 1 | SIMPLE | cdr | ALL | calldate | NULL | NULL | NULL | 123152 | Using where |
+----+-------------+-------+------+---------------+------+---------+------+--------+-------------+
查询的单次运行大约需要0.14秒,因此对于24小时的每小时间隔,脚本应该在大约3.36秒内完成,但最终需要大约12秒
目前整个过程最多需要20秒才能运行24小时,有人可以帮我提高查询速度吗?
答案 0 :(得分:1)
这部分是您查询的瓶颈:
DATE_ADD(calldate, INTERVAL duration SECOND)
这是因为MySQL在的第一个子集的每一行上执行“数学”,这是从你的第一个 整个表格中不匹配的每一行您WHERE
条件确定WHERE
语句的第一部分,因为您使用的是WHERE OR
,而不是WHERE AND
。
我认为你的桌子看起来有点像:
acctid | calldate | duration
========================================
1 | 2014-12-01 17:55:00 | 300
... etc.
考虑重写你的模式,这样你就不会使用MySQL必须为每一行计算的时间间隔,而是MySQL可以立即进行比较的完整DateTime列:
acctid | calldate | duration_end
==================================================
1 | 2014-12-01 17:55:00 | 2014-12-01 18:00:00
要重写此架构,您可以创建新列然后执行(这可能需要一段时间才能处理,但从长远来看会很好地为您服务):
UPDATE cdr SET duration_end = DATE_ADD(calldate, INTERVAL duration SECOND);
然后废弃duration
列并重写您的应用程序以保存到新列中!
您的结果查询将是:
select count(acctid) from cdr where calldate > ? and (calldate < ? or duration_end between ? and ?)
假设架构中没有任何内容可以改变,那么你就会被这个函数所困扰。但是,您可以尝试让MySQL使用子集,以便它不会在如此多的行上进行数学运算:
select
count(acctid)
from
cdr
where
calldate > ? and
(calldate < ? or DATE_ADD(calldate, INTERVAL duration SECOND) between ? and ?)
我无法保证此解决方案的性能提升很多,尽管根据您的数据集可能会显着提升。
答案 1 :(得分:0)
对于星号cdrs,你可以这样做
假设您使用过:
$query=$cdrdb->prepare('select count(acctid) from cdr where calldate between ? and ? or DATE_ADD(calldate, INTERVAL duration SECOND) between ? and ?');
$query->execute(array($start, $end, $start, $end))
你有这样的用途
$query=$cdrdb->prepare('select count(acctid) from cdr where calldate between ? and DATE_ADD(?, interval ? SECOND) and (calldate between ? and ? or DATE_ADD(calldate, INTERVAL duration SECOND) between ? and ?)
');
$MAX_CALL_LENGHT_POSIBLE = 60*60*10; # usualy 10 hr is not reachable on most calls. If you limit it in call, you can decrease to even less values
$query->execute(array($start, $end,$MAX_CALL_LENGHT_POSIBLE,$start,$end $start, $end))
因此,首先将查询限制为stop_time可以的间隔。
但很简单的是添加列call_end_time并创建触发器
DROP TRIGGER IF EXISTS cdr_insert_trigger;
DELIMITER //
CREATE TRIGGER cdr_insert_trigger BEFORE INSERT ON cdr
FOR EACH ROW BEGIN
Set NEW.call_end_time=DATE_ADD(OLD.calldate,interval OLD.duration second);
END//
DELIMITER ;
当然你需要在BOTH calldate和call_end_time列上创建索引并使用Union而不是OR(否则一部分不会使用索引)
答案 2 :(得分:0)
如果磁盘空间不如速度重要,请尝试:
ALTER TABLE cdr ROW_FORMAT = FIXED;