2012-07-30 59 views
0

我有两个MySQL数据库表,如下所述。一个表包含设备信息,另一个表是关于每个设备的一对多日志。这个查询是不是复杂的?

CREATE TABLE `device` (
    `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY, 
    `name` VARCHAR(255) NOT NULL, 
    `active` INT NOT NULL DEFAULT 1, 
    INDEX (`active`) 
); 

CREATE TABLE `log` (
    `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY, 
    `device_id` INT NOT NULL, 
    `message` VARCHAR(255) NOT NULL, 
    `when` DATETIME NOT NULL, 
    INDEX (`device_id`) 
); 

我想要做的是在单个查询(如果可能)中抓取设备信息以及每个设备的最新日志条目。到目前为止,我有如下:

SELECT d.id, d.name, l.message 
FROM device AS d 
LEFT JOIN (
    SELECT l1.device_id, l1.message 
    FROM log AS l1 
    LEFT JOIN log AS l2 ON (l1.device_id = l2.device_id AND l1.when < l2.when) 
    WHERE l2.device_id IS NULL 
) AS l ON (d.id = l.device_id) 
WHERE d.active = 1 
GROUP BY d.id 
ORDER BY d.id ASC; 

这些查询都是我的实际设置,在我的日志表是超过10万行(实际上有几个日志表我看看)的简单复制品。查询确实运行,但非常非常缓慢(比如超过两分钟)。我确信有一个更简洁/优雅/“SQL”的方式来形成这个查询来获得我需要的数据,但我还没有找到它。

即使没有丑陋的sub-SELECT和self-JOIN,我想要做什么?我可以用不同的策略完成工作吗?或者,查询的本质是不可复制的?

同样,应用程序的逻辑是这样的,我可以“手动加入”表,如果这不起作用,但我觉得MySQL应该能够处理这样的事情而不窒息 - 但我承认绿色当涉及到这种复杂的集合代数。

编辑:由于这是一个人为的例子,我忘了给索引添加到device.active

+0

如果您在添加索引(DEVICE_ID时)?这可能会使JOIN更有效率。 – 2012-07-30 21:25:18

+0

正如你经历过的第一手MySQL越慢查询越大,你存储的数据越多等等。如果你的情况超过了100k行,我会推荐使用不同的解决方案:NoSQL – libjup 2012-07-30 21:28:51

+7

@libjup,without意思是把OP放到10万行并不是很大,实际上它相当小。建议不仅要改变RDBMS而且要改变数据库管理系统,因为有一个10万个表是一个巨大的过度反应。 – Ben 2012-07-30 21:30:44

回答

3

这里有一个稍微不同的方式来查询,避免自联接:

SELECT d.id, d.name, l.message 
FROM device AS d 
LEFT JOIN (
    SELECT l1.device_id, l1.message 
    FROM log AS l1 
    WHERE l1.when = (
     SELECT MAX(l2.when) 
     FROM log AS l2 
     WHERE l2.device_id = l1.device_id 
) l ON l.device_id = d.id 
WHERE d.active = 1 
ORDER BY d.id ASC; 

由于100k不是一个非常大的表,即使没有适当的索引,我也不会期望这个查询花费几秒钟。但是,如评论所示,您可以考虑根据您的explain plan的结果添加其他索引。

+0

不错,但可能是MAX()而不是MIN()? – KolA 2012-07-31 02:00:37

+0

@KolA哎呀!你是对的......感谢你指出了这一点! – 2012-07-31 03:32:53

0

你的查询,以及下面的策略将从指数ON log(device_id,when)受益。该索引可以取代索引ON log(device_id),因为该索引是多余的。


如果有日志条目的整体一大堆每个设备时,在查询JOIN将会产生良好的中小型中间结果集,这将得到渗透到每个设备一行。我不相信MySQL优化器对于反连接操作有任何“快捷方式”(至少不是5.1)......但是您的查询可能是最有效的。

问:我可以用不同的策略完成工作吗?

是的,还有其他的策略,但我不知道任何这些都比你的查询“更好”。


UPDATE:你可能会考虑是增加另一个表,您的架构,一个适用于每个设备的最新的日志条目

一种策略。这可以通过log表中定义的TRIGGER来维护。如果您只执行插入操作(不更新最新日志条目的UPDATE和DELETE,则非常简单,只要针对log表执行插入操作,就会触发AFTER INSERT FOR EACH ROW触发器,该触发器将插入到日志中的when值device_id的表格与log_latest表格中当前的when值相比较,并插入/更新log_latest表格中的行,以便最新的行始终存在。或者,您可以将latest_whenlatest_message列添加到设备表中,并将其保留在那里。)

但是,这种策略超出了您的原始问题......但是如果您需要频繁运行“针对所有设备的最新日志消息”查询,这是一个可行的策略。缺点是你有一张额外的表格,并且在执行插入log表格时性能受到影响。这个表格可以使用类似你的原始查询或下面的替代方法完全刷新。


一种方法是查询,做了简单的devicelog表的加盟,获得由设备和下降when命令行。然后使用一个内存变量来处理行,过滤除“最新”日志条目以外的所有行。请注意,此查询返回一个额外的列。 (这额外的一列可以通过包裹整个查询作为内嵌视图中删除,但你可能会得到更好的性能,如果你可以返回一个额外的列活:

SELECT IF(s.id = @prev_device_id,0,1) AS latest_flag 
    , @prev_device_id := s.id AS id 
    , s.name 
    , s.message 
    FROM (SELECT d.id 
      , d.name 
      , l.message 
      FROM device d 
      LEFT 
      JOIN log l ON l.device_id = d.id 
     WHERE d.active = 1 
     ORDER BY d.id, l.when DESC 
     ) s 
    JOIN (SELECT @prev_device_id := NULL) i 
HAVING latest_flag = 1 

什么在选择第一表达列表正在做的是“标记”一行,只要该行上的设备标识值与前一行中的设备标识差异HAVING子句过滤掉所有未标记为1的行(可以省略HAVING子句来看看这个表达式是如何工作的。)

(我没有测试过这个语法错误,如果你有错误,让我知道,我会仔细看看,我的桌面检查说没关系...但我可能错过了一个paren或comm一,)

(您可以通过包装,在另一个查询“摆脱”额外列

SELECT r.id,r.name,r.message FROM (
/* query from above */ 
) r 

(但同样,这可能会影响性能,你可能会得到,如果你能更好的性能与额外的列一起生活)

当然,在最外层的查询中添加一个ORDER BY,以确保您的结果集按您需要的方式排序。

这种方法对于一大堆设备来说工作得很好,而且在日志中只有几个相关的行。否则,这将产生大量的中间结果集(按照日志表中的行数),该结果集将被转移到临时的MyISAM表中。

UPDATE:

如果从device基本上让所有的行(其中谓词是不是非常有选择性的),你也许可以得到通过获得在每一个DEVICE_ID最新的日志条目更好的性能log表,并推迟加入device表。 (但注意,指数将不提供设置为做好加入该中间结果,所以它真的需要测试来衡量性能。)

SELECT d.id 
    , d.name 
    , t.message 
    FROM device d 
    LEFT 
    JOIN (SELECT IF(s.device_id = @prev_device_id,0,1) AS latest_flag 
      , @prev_device_id := s.device_id AS device_id 
      , s.messsage 
      FROM (SELECT l.device_id 
        , l.message 
        FROM log l 
       ORDER BY l.device_id DESC, l.when DESC 
       ) s 
      JOIN (SELECT @prev_device_id := NULL) i 
     HAVING latest_flag = 1 
     ) t 
    ON t.device_id = d.id 

注:我们指定两个降序内联视图的ORDER BY子句中的device_idwhen列别名为s,这不是因为我们需要降序device_id顺序的行,而是允许MySQL通过允许MySQL执行“反向扫描”操作来避免文件操作操作带有前导列的索引(device_id,when)。

NOTE:该查询仍然会将中间结果集作为临时MyISAM表进行假脱机,并且不会有任何索引。所以它的可能性不如原来的查询。


另一种策略是在SELECT列表中使用相关子查询。你只返回从日志表中的单个列,所以这是很容易查询到理解:

SELECT d.id 
    , d.name 
    , (SELECT l.message 
      FROM log l 
      WHERE l.device_id = d.id 
      ORDER BY l.when DESC 
      LIMIT 1 
     ) AS message 
    FROM device d 
WHERE d.active = 1 
ORDER BY d.id ASC; 

注:由于id是在device表的主键(或唯一键),和由于您没有执行任何会生成额外行的JOIN,因此可以省略GROUP BY子句。

注:此查询将使用“嵌套循环”操作。也就是说,对于从device表返回的每一行,(实质上)需要运行单独的查询以从日志中获取相关行。对于只有少数device行(如将与在device表更具选择性的谓词被退回),并为每个设备日志条目的一大堆,性能不会太差。但对于很多设备,每个设备只有几条日志消息,其他方法很可能会更加高效。)

另请注意,使用此方法时请注意,您可以轻松地将其扩展为也返回第二个最新的日志消息作为一个单独的列,通过向SELECT列表添加另一个子查询(就像第一个子查询),只需更改LIMIT子句跳过第一行,然后获取第二行。

 , (SELECT l.message 
      FROM log l 
      WHERE l.device_id = d.id 
      ORDER BY l.when DESC 
      LIMIT 1,1 
     ) AS message_2 

对于从设备获得基本上都行,你可能会得到使用JOIN操作的最佳性能。这种方法的一个缺点是,当有两个(或更多)行与设备的最新when值匹配时,它有可能为设备返回多行。 (基本上,这种做法是保证返回一个“正确”的结果集的时候,我们有一个保证log(device_id,when)是唯一

有了这个查询作为内嵌视图,以获得“最新的”当值:

SELECT l.device_id 
    , MAX(l.when) 
    FROM log l 
GROUP BY l.device_id 

我们可以加入此将日志和设备表。

SELECT d.id 
    , d.name 
    , m.messsage 
    FROM device d 
    LEFT 
    JOIN (
     SELECT l.device_id 
       , MAX(l.when) AS `when` 
      FROM log l 
      GROUP BY l.device_id 
     ) k 
    ON k.device_id = d.id 
    LEFT 
    JOIN log m 
    ON m.device_id = d.id 
     AND m.device_id = k.device_id 
     AND m.when = k.when 
ORDER BY d.id 

所有这些都是备选策略(我相信是你问的问题),但我也不清楚个中ose将会更好地满足您的特殊需求。 (但它总是好的有几个不同的工具,在工具带酌情使用。)

+0

相关的子查询几乎总是表现最差的代码。你永远不应该建议他们替换派生表。它是逐行运行的东西和作为数据集运行的东西之间的差异。最好养成使用正确技术的习惯,而不是像这样使用劣质技术。 – HLGEM 2012-07-30 21:59:30

+2

@HLGEM:有时,相关的子查询是最有效的方法。实际上,在某些情况下,它是返回指定结果集的最有效方法。 (我相信我在回答中包含了关于这种方法对性能问题的注意事项。)派生表不是一个神奇的子弹,它们也有一些性能方面的考虑。当然,你可以自由地相信相关的子查询是一种诅咒,你可以自由地认为这是一种“不良技术”,并且“从不建议”它们。 OP要求采取替代策略。相关的子查询就是这样。 – spencer7593 2012-07-30 22:17:58

+0

在我看来,这些解决方案都不如原来那么复杂? – 2012-07-31 17:40:27

1

这里的,只需要一个日志表的实例替代:

SELECT d.id, d.name, 
      SUBSTRING_INDEX(
       GROUP_CONCAT(
        l.message 
        SEPARATOR '~' 
        ORDER BY l.when DESC 
      ) 
      , '~' 
      , 1 
     ) 
FROM  device d 
LEFT JOIN log l 
ON  d.id = l.device_id 
WHERE  d.active = 1 
GROUP BY d.id 

此查询通过创建消息的波浪线分隔的列表,通过按照从大到小的顺序日期排序查找最近的日志信息。这由GROUP_CONCAT完成。该列表的第一个条目的SUBSTRING_INDEX芯片。

有2个缺点,这种方法:

  • 它使用GROUP_CONCAT。如果该函数的结果变得太长,结果将被截断。您可以弥补,如果你在运行查询之前做

    SET @@group_concat_max_len = @@max_allowed_packet;

。您甚至可以做得更好:因为您只想获取一条消息,所以您可以将group_concat_max_len设置为与message列的最大字符长度一样大。与使用@@max_alowed_packet相比,这将节省大量内存。

  • 它依赖于一个不能出现在消息文本中的特殊分隔符(在本例中是tilde('~'))。只要您确定它不出现在消息文本中,就可以将其更改为您喜欢的任何分隔符字符串。

如果你能忍受这些限制,那么这个查询可能是最快的。

以下是更多与您的选择一样复杂的替代方案,但性能可能会更好。

SELECT d.id 
,   d.name 
,   l.message 
FROM  (
      SELECT d.id, d.name, MAX(l.when) lmax 
      FROM  device d 
      LEFT JOIN log l 
      ON  d.id = l.device_id 
      WHERE  d.active = 1 
      GROUP BY d.id 
     ) d 
LEFT JOIN log  l 
ON  d.id = l.device_id 
AND  d.lmax = l.when 
ORDER BY d.id ASC; 

另一种选择:

SELECT d.id 
,   d.name 
,   l2.message 
FROM  device d 
LEFT JOIN (
      SELECT l.device_id 
      ,  MAX(l.when) lmax 
      FROM  log l 
      GROUP BY l.device_id 
     ) l1 
ON  d.id = l1.device_id 
LEFT JOIN log  l2 
ON  l1.device_id = l2.device_id 
AND  l1.lmax  = l2.when 
WHERE  d.active  = 1 
ORDER BY d.id ASC; 
+0

GROUP_CONCAT查询很聪明。至少,我认为你打算在GROUP_CONCAT函数中包含'SEPARATOR'〜''这就是我阅读它的方式。 – spencer7593 2012-07-31 18:34:02

+0

@ spencer7593谢谢!确实很好的电话,我忘了分隔条款!编辑以反映这一点。 – 2012-07-31 19:20:30