【问题标题】:Optimizing an SQL query with generated GROUP BY statement使用生成的 GROUP BY 语句优化 SQL 查询
【发布时间】:2011-05-07 21:13:06
【问题描述】:

我有这个问题:

   SELECT ROUND(AVG(temp)*multT + conT,2) as temp,
          FLOOR(timestamp/$secondInterval) as meh
     FROM sensor_locass
LEFT JOIN sensor_data USING(sensor_id)
    WHERE sensor_id = '$id'
      AND project_id = '$project'
 GROUP BY meh
 ORDER BY timestamp ASC

目的是选择数据来绘制图形,我使用一个像素值的数据的平均值来使图形忠实于数据。

到目前为止,优化包括添加索引、在 MyISAM 和 InnoDB 之间切换,但没有运气。

由于时间间隔随图形缩放和数据收集周期而变化,我无法为GROUP BY 语句创建单独的列,但是查询速度很慢。有没有人有想法优化这个查询或表以使这个分组更快,我目前在timestampsensor_idproject_id 列上有一个索引,但是没有使用timestamp 索引。

当使用查询运行explain extended 时,我得到以下信息:

1   SIMPLE  sensor_locass   ref     sensor_id_lookup,project_id_lookup  sensor_id_lookup    4   const                               2       100.00  Using where; Using temporary; Using filesort
1   SIMPLE  sensor_data     ref     idsensor_lookup idsensor_lookup                         4   webstech.sensor_locass.sensor_id    66857   100.00

sensor_data 表目前包含 270 万个数据点,这只是我最终必须处理的数据量的一小部分。任何有用的想法、cmets 或解决方案都将受到欢迎

编辑表定义:

CREATE TABLE `sensor_data` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 `gateway_id` int(11) NOT NULL,
 `timestamp` int(10) NOT NULL,
 `v1` int(11) NOT NULL,
 `v2` int(11) NOT NULL,
 `v3` int(11) NOT NULL,
 `sensor_id` int(11) NOT NULL,
 `temp` decimal(5,3) NOT NULL,
 `oxygen` decimal(5,3) NOT NULL,
 `batVol` decimal(4,3) NOT NULL,
 PRIMARY KEY (`id`),
 KEY `gateway_id` (`gateway_id`),
 KEY `time_lookup` (`timestamp`),
 KEY `idsensor_lookup` (`sensor_id`)
) ENGINE=MyISAM AUTO_INCREMENT=2741126 DEFAULT CHARSET=latin1

CREATE TABLE `sensor_locass` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 `project_id` int(11) NOT NULL,
 `sensor_id` int(11) NOT NULL,
 `start` date NOT NULL,
 `end` date NOT NULL,
 `multT` decimal(6,3) NOT NULL,
 `conT` decimal(6,3) NOT NULL,
 `multO` decimal(6,3) NOT NULL,
 `conO` decimal(6,3) NOT NULL,
 `xpos` decimal(4,2) NOT NULL,
 `ypos` decimal(4,2) NOT NULL,
 `lat` decimal(9,6) NOT NULL,
 `lon` decimal(9,6) NOT NULL,
 `isRef` tinyint(1) NOT NULL,
 PRIMARY KEY (`id`),
 KEY `sensor_id_lookup` (`sensor_id`),
 KEY `project_id_lookup` (`project_id`)
) ENGINE=MyISAM AUTO_INCREMENT=238 DEFAULT CHARSET=latin1

【问题讨论】:

  • 发布您的表定义。 SHOW CREATE TABLE sensor_locass.

标签: php mysql optimization query-optimization


【解决方案1】:

尽管每个人都给出了答案,但更改主键以优化具有 238 行的表的搜索并不会改变任何事情,尤其是当 EXPLAIN 显示单个键将搜索范围缩小到两行时。并且将timestamp 添加到sensor_data 上的主键也不起作用,因为没有任何东西查询时间戳,只是对其进行计算(除非您可以按照 galymzhan 的建议限制时间戳值)。

哦,您可以在查询中删除LEFT,因为匹配project_id 无论如何都会使其无关紧要(但不会减慢任何速度)。如果这些变量来自客户输入,请不要将变量直接插入到查询中,以避免 $project_id = "'; DROP TABLES; --" 类型的 sql 注入漏洞。

调整堆大小可能会奏效一段时间,但如果需要扩展,则必须继续调整。

vdrmrt 建议的答案可能有效,但是您需要使用 $secondInterval 的每个可能值填充聚合表,鉴于您所说的灵活性,我认为这不是很合理。同样,您可以考虑rrdtool,或者直接使用它,或者以与它相同的方式修改您的数据。我具体指的是它在给定的时间段(通常是几天)内保留原始数据,然后在越来越长的时间段内将数据点平均在一起。最终结果是您可以放大最近一段时间的高细节,但如果您进一步回顾,数据已被有效地有损压缩为长时间内的平均值(例如,一天内每秒一个数据点,一周每分钟一个数据点,一个月每小时一个数据点,等等)。您最初可以自定义这些平均值,但除非您同时保留原始数据和汇总数据,否则您将无法返回并进行调整。特别是,您无法动态放大某个较旧的任意点的高细节(例如查看六个月前发生的 1 小时时间的每秒数据)。

因此,根据您的要求,您必须决定此类限制是否合理。

如果不是,那么我会争辩说您正在尝试在 MySQL 中做一些它不是为之设计的事情。我建议提取您需要的原始数据并在 php 中取平均值,而不是在您的查询中。正如已经指出的那样,您的查询需要很长时间的主要原因是因为GROUP BY 子句迫使 mysql 处理内存中的所有数据,但由于它的数据太多,它实际上将这些数据临时写入磁盘。 (因此using filesort)。但是,您可以在 php.ini 中使用多少内存方面具有更大的灵活性。此外,由于您正在组合附近的行,您可以逐行提取数据,动态组合它,从而永远不需要将所有行保留在您的 php 进程中的内存中。然后您可以删除GROUP BY 并避免文件排序。请改用ORDER BY timestamp,如果mysql没有正确优化它,请确保使用FORCE INDEX FOR ORDER BY (timestamp)

【讨论】:

  • 理论上,MySQL 应该能够遍历时间戳索引并为您进行动态聚合(我几乎敢打赌,如果 group by 在列上,它会这样做它有一个索引)。我猜问题是 MySQL 没有分析能力来意识到如果 col 已排序,那么 floor(col/const) 也是如此。这是否超出了 MySQL 旨在处理的范围?
  • @BCS 我找不到这方面的参考,但如果您在计算中使用列,mysql 不会使用索引。事实上,我不知道有任何 rdbms 可以做到这一点,而无需明确要求使用表达式预先计算索引(mysql 也不能这样做)。如果表达式在 where 子句中,有时您可以通过切换某些内容来优化(例如,将 col / const1 < const2 更改为 col < const1 * const2(只要您的 const 是正数))但是这个技巧在这里不起作用,因为 FLOOR是不可逆的,因为该表达式未被用于搜索。
  • @BCS 为了解决您的第一点问题,mysql 理论上可以猜测 floor(con/const) 正在增加,但通常很难猜测(即对于所有可能的此类表达式)结果行数因此猜测所需的临时表是否适合内存。
  • 为什么需要猜测行数?我认为他们有他们的理由,但在我看来,更好的解决方案是开始在内存中构建表,并且当/如果它增长到给定大小时,开始将其溢出到磁盘。如果行生成和溢出是异步的(例如,放置在不同的线程中),这应该相当有效。这也将允许结果总是溢出到磁盘的情况,除非用户检索结果足够快(假设 MySQL 可以在完成生成所有行之前开始返回行)。
【解决方案2】:

如果您想使用 时间戳索引,您必须明确告知要使用该索引。 MySQL 5.1 支持USE INDEX FOR ORDER BY/FORCE INDEX FOR ORDER BY。看看这里http://dev.mysql.com/doc/refman/5.1/en/index-hints.html

【讨论】:

    【解决方案3】:

    我同意第一步应该定义sensor_id,project_id作为sensor_locass的主键。 如果这还不够,并且您的数据是相对静态的,您可以创建一个聚合表,例如您可以每天刷新,然后从那里查询。 您仍然需要为 secondInterval 定义一个范围,将其存储在新表中并将该字段添加到聚合表的主键中。

    填充聚合表的查询将是这样的:

    INSERT INTO aggregated_sensor_data (sensor_id,project_id,secondInterval,timestamp,temp,meh)
    SELECT
        sensor_locass.sensor_id,
        sensor_locass.project_id,
        secondInterval,
        timestamp,
        ROUND(AVG(temp)*multT + conT,2) as temp,
        FLOOR(timestamp/secondInterval) as meh
    FROM
        sensor_locass
        LEFT JOIN sensor_data
        USING(sensor_id)
        LEFT JOIN secondIntervalRange
        ON 1 = 1
    WHERE
            sensor_id = '$id'
        AND
            project_id = '$project'
    GROUP BY
        sensor_locass.sensor_id,
        sensor_locass.project_id,
        meh
    ORDER BY
        timestamp ASC
    

    您可以使用此查询来提取聚合数据:

    SELECT
        temp,
        meh
    FROM
        aggregated_sensor_data
    WHERE
         sensor_id = '$id'
        AND project_id = '$project'
        AND secondInterval = $secondInterval
    ORDER BY
        timestamp ASC   
    

    【讨论】:

      【解决方案4】:

      正如 Joshua 建议的那样,您应该将 (sensor_id, project_id) 定义为 sensor_locass 表的主键,因为此时表的每一列都有 2 个单独的索引。根据 mysql 文档,SELECT 将只从它们中选择 一个 索引(最严格,找到更少的行),而主键允许使用两个列来索引数据。

      但是,EXPLAIN 显示 MySQL 检查了连接表上的 66857 行,因此您也应该以某种方式对其进行优化。也许您可以查询给定时间间隔的传感器数据,例如timestamp BETWEEN (begin, end)

      【讨论】:

        【解决方案5】:

        我建议您找到表的自然主键并切换到 InnoDB。这是对您的数据的猜测:

        传感器数据: PRIMARY KEY (sensor_id, timestamp)

        sensor_locass: PRIMARY KEY (sensor_id, project_id)

        InnoDB 将以这种方式对所有数据进行排序,因此您可能会SELECT 一起的行将一起在磁盘上。我认为你分组总是会引起一些麻烦。如果您可以将其保持在切换到文件排序的大小以下(tmp_table_sizemax_heap_table_size),它会快得多。

        您通常返回多少行?现在需要多长时间?

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2011-08-29
          • 2012-12-31
          • 2020-08-23
          相关资源
          最近更新 更多