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;

这些查询是我实际设置的简化复制品,其中我的日志表超过 100k 行(实际上我查看了几个日志表)。该查询确实运行,但是非常非常缓慢(例如,超过两分钟)。我确信有一种更简洁/优雅/“SQL”的方式来形成这个查询来获取我需要的数据,但我还没有找到它。

如果没有丑陋的 sub-SELECT 和 self-JOIN,我想做的事情是否可行?我可以用不同的策略完成工作吗?或者,查询的本质是不可简化的复杂吗?

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

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

4

3 回答 3

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.

于 2012-07-30T21:39:39.157 回答
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列表的第一个条目的筹码。

这种方法有两个缺点:

  • 它使用GROUP_CONCAT. 如果该函数的结果变得太长,则结果将被截断。如果你这样做,你可以补救

    SET @@group_concat_max_len = @@max_allowed_packet;

在运行查询之前。您可以做得更好:由于您只对收到一条消息感兴趣,因此您可以将其设置group_concat_max_len为与列的最大字符长度一样大message。与使用@@max_alowed_packet.

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

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

这里有更多与您的一样复杂但性能更好的替代方案。

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;
于 2012-07-30T22:07:53.780 回答
0

您的查询和以下策略将受益于 index ON log(device_id,when)。该索引可以替换 index ON log(device_id),因为该索引是多余的。


如果每个设备都有大量日志条目,则查询中的 JOIN 将生成一个大小合适的中间结果集,该结果集将被过滤到每个设备一行。我不相信 MySQL 优化器对该反连接操作有任何“快捷方式”(至少在 5.1 中没有)......但您的查询可能是最有效的。

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

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


更新:

您可能会考虑的一种策略是将另一个表添加到您的架构中,该表包含每个设备的最新日志条目。这可以通过在log表上定义的 TRIGGER 来维护。如果您只执行插入(对最近的日志条目没有 UPDATE 和 DELETE,这相当简单。每当对log表执行插入时,就会AFTER INSERT FOR EACH ROW触发触发器,它将when插入到日志表中的值进行比较device_id,表中的当前whenlog_latest,并插入/更新log_latest表中的行,以便始终存在最新的行。您还可以(冗余)将设备名称存储在表中。(或者,您可以添加latest_when和一个latest_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

SELECT 列表中的第一个表达式所做的是“标记”一行,只要该行上的设备 id 值与上一行上的设备 id 不同。HAVING 子句过滤掉所有未标记为 1 的行。(您可以省略 HAVING 子句以查看该表达式的工作原理。)

(我没有对此进行语法错误测试,如果您遇到错误,请告诉我,我会仔细查看。我的办公桌检查说没问题……但我可能错过了括号或逗号,)

(您可以通过将其包装在另一个查询中来“摆脱”该额外的列

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

(但同样,这可能会影响性能,如果您可以使用额外的列,您可能会获得更好的性能。)

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

这种方法适用于一大堆设备,并且日志中只有几行相关的行。否则,这将生成大量中间结果集(按日志表中的行数的顺序),必须将其转出到临时 MyISAM 表中。

更新:

如果您从本质上获取所有行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_id和列的降序,不是因为我们需要按 device_id 降序排列的行,而是通过允许 MySQL 执行“反向”来避免文件排序操作对具有前导列(device_id,when)的索引进行扫描”操作。whens

注意:此查询仍会将中间结果集作为临时 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是表中的 PRIMARY KEY(或 UNIQUE KEY)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 

所有这些都是替代策略(我相信这是您提出的问题),但我不确定其中任何一个是否会更好地满足您的特定需求。(但在工具带中有几个不同的工具总是好的,以适当使用。)

于 2012-07-30T21:39:18.623 回答