提高此MYSQL查询的速度

时间:2014-12-02 13:38:32

标签: php mysql asterisk

我正在使用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小时,有人可以帮我提高查询速度吗?

3 个答案:

答案 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;