高效查询

时间:2016-01-27 23:00:36

标签: php mysql sql query-performance

我有两个表:考试(ExamID,日期,模态)和CT(ctdivol,ExamID(FK)),括号中的属性。

注意:CT表有大约10万个条目。

我想计算特定日期间隔内ctdivol的平均值。

我有这个代码可行,但速度太慢了:

function get_CTDIvolAVG($min, $max) {

$values = 0;
$number = 0;

$query = "SELECT  (unix_timestamp(date)*1000), examID
    from  exam use index(dates)
    where  modality = 'CT'
      AND  (unix_timestamp(date)*1000) between '" . $min . "' AND '" . $max . "';";

$result = mysql_query($query) or die('Query failed: ' . mysql_error());

while($line = mysql_fetch_array($result, MYSQL_ASSOC)) {

    $avg = "SELECT  SUM(ctdivol_mGy), count(ctdivol_mGy)
    from  ct use index(ctd)
    where  examID ='" . $line["examID"] ."'
      AND  ctdivol_mGy>0;";
    $result1 = mysql_query($avg) or die('Query failed: ' . mysql_error());
    while ($ct = mysql_fetch_array($result1, MYSQL_ASSOC)) {

        $values = $values + floatval($ct["SUM(ctdivol_mGy)"]);
        $number = $number + floatval($ct["count(ctdivol_mGy)"]);

    }
}
if ($number!=0) {
    echo $values/$number;

}

}

如何让它更快?

3 个答案:

答案 0 :(得分:3)

使用EXPLAIN查看查询执行计划。

对于第一个查询,MySQL无法有效地使用索引范围扫描操作。必须针对表中的每个行评估WHERE子句中的表达式。当我们与 bare 列进行比较时,我们会获得更好的性能。在文字方面进行操作......将这些值转换为您要比较的列的数据类型。

WHERE e.date BETWEEN expr1 AND expr2 

对于expr1,您需要一个将$min值转换为日期时间的表达式。请注意时区转换。我认为这可能会满足你对expr1的需求:

 FROM_UNIXTIME( $min /1000)

类似的东西:

WHERE e.date BETWEEN FROM_UNIXTIME( $min /1000) AND FROM_UNIXTIME( $max /1000)

然后我们应该看到MySQL能够有效地使用带有日期前导列的索引。 EXPLAIN输出应显示访问类型range

如果返回的列数是一个小子集,请考虑覆盖索引。然后EXPLAIN将显示“Using index”,这意味着可以完全从索引中查询查询,而不查找基础表中的页面。

其次,避免在循环中多次运行查询。运行返回单个结果集的单个查询通常更有效,因为将SQL发送到数据库的开销,解析SQL文本的数据库,有效语法(正确位置的关键字),有效语义(标识符)参考有效对象),考虑可能的访问路径并确定哪个是最低成本,然后执行查询计划,获取元数据锁,生成结果集,将其返回给客户端,然后清理。单个语句并不明显,但是当你开始在紧密循环中运行大量语句时,它就开始加起来。再加上效率低下的查询,它开始变得非常明显。

如果examID中的exam列是唯一且不为空(或者是exam的PRIMARY KEY,那么看起来您可以使用单个查询,如下所示:

SELECT UNIX_TIMESTAMP(e.date)*1000 AS `date_ts`
     , e.examID                    AS `examID`
     , SUM(ct.ctdivol_mGy)         AS `SUM(ctdivol_mGy)`
     , COUNT(ct.ctdivol_mGy)       AS `count(ctdivol_mGy)`
  FROM exam e
  LEFT
  JOIN ct
    ON ct.examid = e.examID
   AND ct.ctdivol_mGy > 0
 WHERE e.modality = 'CT'
   AND e.date >= FROM_UNIXTIME(  $min  /1000)
   AND e.date <= FROM_UNIXTIME(  $max  /1000)
 GROUP
    BY e.modality
     , e.date
     , e.examID
 ORDER
    BY e.modality
     , e.date
     , e.examID

为了获得最佳性能,您需要覆盖索引:

  ... ON exam (modality, date, examID)
  ... ON ct (examID, ctdivol_mGy)

我们希望看到EXPLAIN输出;我们希望MySQL可以利用考试中的索引来完成GROUP BY(并避免使用“使用文件排序”操作),并且还可以对{{1}的索引使用ref操作}。

重申......该查询要求ctexamID表的主键(或者至少保证是唯一且非空)。否则,该结果可能与原始代码不同。如果没有该保证,我们可以使用exam列表中的内联视图或子查询。但就性能而言,我们不希望没有充分的理由去那里。

这只是一些一般性的想法,而不是一个坚硬而快速的“这会更快”。

答案 1 :(得分:0)

您可以通过exam_id在第一个表上将连接写入子查询表:

$query = "SELECT (unix_timestamp(date)*1000) as time_calculation, ed.examID, inner_ct.inner_sum, inner_ct.inner_count "
" FROM exam ed,"
. " ( SELECT SUM(ctdivol_mGy) as inner_sum, count(ctdivol_mGy) as inner_count, examID"
. "   FROM ct"
. "   WHERE  ctdivol_mGy>0 ) inner_ct"
. " WHERE ed.modality = 'CT' AND time_calculation between"
. " '$min' and '$max'"
. " AND ed.examId = inner_ct.examID";

( SELECT . . .) inner_ct创建一个可以加入的内存表。如果您在连接中选择合成数据(在您的情况下为总和),则非常有用。

相反,您可以使用以下语法:

$query = "SELECT (unix_timestamp(date)*1000) as time_calculation, ed.examID, inner_ct.inner_sum, inner_ct.inner_count "
" FROM exam ed,"
. " LEFT JOIN ( SELECT SUM(ctdivol_mGy) as inner_sum, count(ctdivol_mGy) as inner_count, examID"
. "   FROM ct"
. "   WHERE  ctdivol_mGy>0 ) inner_ct"
. " ON ed.examID = inner_ct.examID"
. " WHERE ed.modality = 'CT' AND time_calculation between"
. " '$min' and '$max'";

答案 2 :(得分:0)

您尚未在问题中提供样本数据,因此我们采用假设来尝试回答。如果exam中的许多行只有一个ct行 - 但是可以存在根本没有ct行的检查行 - 则此单个查询应该提供所需的结果。

SELECT
      exam.examID
    , (unix_timestamp(exam.date) * 1000
    , SUM(ct.ctdivol_mGy)
    , COUNT(ct.ctdivol_mGy)
FROM exam
LEFT OUTER JOIN ct on exam.examID = ct.examID AND ct.ctdivol_mGy > 0
WHERE exam.modality = 'CT'
      AND exam.date >= @min AND exam.date < @max
GROUP BY
      exam.examID
    , (unix_timestamp(exam.date) * 1000)
      ;

注意我没有尝试PHP代码,只专注于SQL。我使用@min@max来表示where子句中需要的2个日期。它们应与列exam.date具有相同的数据类型,因此在添加到查询字符串之前,请在PHP中进行这些计算。

  

我想计算特定时间间隔内ctdivol的平均值   日期。

如果你想要返回一个数字,那么这应该有所帮助:

SELECT
      AVG(ct.ctdivol_mGy)
FROM exam
INNER JOIN ct on exam.examID = ct.examID AND ct.ctdivol_mGy > 0
WHERE exam.modality = 'CT'
      AND exam.date >= @min AND exam.date < @max
      ;

请注意,对于此变体,我们可能不需要左连接(但由于缺少样本数据和预期结果,这是一个假设)。