【问题标题】:Querying efficiently高效查询
【发布时间】:2016-01-27 23:00:35
【问题描述】:

我有两个表:Exam (ExamID, Date, Modality) 和 CT(ctdivol, ExamID(FK)),括号中的属性。

注意:CT 表有大约 100 000 个条目。

我想计算特定日期间隔内 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;

}

}

我怎样才能让它更快?

【问题讨论】:

  • 这最好在我们的会员网站上专门针对dba experts 询问,哦,我没有投反对票。
  • 您是否有sqlfiddle.com 的示例日期和预期结果
  • 避免SQL内部循环变慢;使用 SQL 代替 PHP 进行聚合
  • 请停止使用已弃用的 mysql_ 函数,改用 pdo 或 mysqli。

标签: php mysql sql query-performance


【解决方案1】:

使用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 文本、有效语法(正确位置的关键字)、有效语义(标识符引用有效对象),考虑可能的访问路径并确定哪个成本最低,然后执行查询计划,获取元数据锁,生成结果集,将结果集返回给客户端,然后进行清理。单个语句并不明显,但是当您开始在紧密循环中运行大量语句时,它开始加起来。再加上一个低效的查询,它开始变得非常引人注目。


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

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(并避免“使用文件排序”操作),并且还可以对 ct 的索引使用 ref 操作。

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

这只是一些一般性的想法,而不是一成不变的“这会更快”。

【讨论】:

  • 感谢您的解释@spencer7593。它对我有用!
【解决方案2】:

您可以通过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'";

【讨论】:

  • 它推荐使用新的连接语法。它也更好地阅读/理解
  • 不确定我是否关注。我个人觉得在写子查询表的时候join别名更清晰。但是,在我看来,这完全是个人喜好。
  • 并且不要在 WHERE 子句中使用这样的字段内容进行计算 (unix_timestamp(date)*1000) = ** 最好在其他站点上进行计算 ** min/ 1000 。当您使用字段计算时,MySQL 必须读取所有行并计算以查看 WHERE 是否为真
  • 对不起,我不是指带有别名的派生表。 SELECT * from tab1 LEFT JOIN (SELECT ......) AS inner_ct 只是更好地阅读 MySQL 的推荐
【解决方案3】:

您没有在问题中提供示例数据,因此我们采用假设来尝试回答。如果 ct 中的许多行只有一个 exam 行 - 但可以存在一个根本没有 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
      ;

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

【讨论】:

  • 又一个问题@Used_By_Already,有没有办法计算第90个百分位数而不是平均值?
  • 现在无法添加太多。但是读这个。 rpbouman.blogspot.com.au/2008/07/…
  • 对于第 90 个百分位数也可以阅读 rpbouman.blogspot.com.au/2008/07/… 这个效率更高,因为它不依赖 group_concat,如果您无法让它工作,请打开一个新问题 - 但请包括“示例数据”和 sql 问题的“预期结果”
猜你喜欢
  • 2022-01-04
  • 2011-09-03
  • 2021-01-22
  • 1970-01-01
  • 2011-09-19
  • 2018-08-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多