这个查询难以置信地复杂吗?

时间:2012-07-30 21:22:07

标签: mysql sql join self-join

我有两个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”的方式来形成这个查询来获取我需要的数据,但我还没有找到它。

如果没有丑陋的子SELECT和自我JOIN,我想做什么呢?我可以用不同的策略完成工作吗?或者,查询的本质是不可简化的复杂事物吗?

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

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

3 个答案:

答案 0 :(得分: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的结果添加其他索引。

答案 1 :(得分: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;

答案 2 :(得分:0)

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


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

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

是的,还有其他策略,但我不知道其中任何一个是更好的&#34;比你的查询。


更新:

您可以考虑的一种策略是在架构中添加另一个表,其中包含每个设备的最新日志条目。这可以通过log表中定义的TRIGGER来维护。如果您只执行插入(没有UPDATE且没有最新日志条目的DELETE,这是相当简单的。每当对log表执行插入时,会触发AFTER INSERT FOR EACH ROW触发器,将when值插入到device_id的日志表中,插入when表中的当前log_latest值,并插入/更新log_latest表中的行,以便最新的行始终存在。您还可以(冗余地)将设备名称存储在表中。(或者,您可以在设备表中添加latest_whenlatest_message列,并维护他们在那里。)

但是这个策略超出了你原来的问题......但如果你需要经常为所有设备运行最新的日志消息,这是一个可行的策略。查询。缺点是你有一个额外的表,并且在执行log表的插入时会有性能损失。可以使用原始查询或下面的替代方法完全刷新此表。


一种方法是执行devicelog表的简单连接的查询,获取按设备排序的行和降序when。然后使用内存变量来处理行,过滤掉除了最新的&#34;之外的所有行。日志条目。请注意,此查询返回一个额外的列。 (可以通过将整个查询包装为内联视图来删除此额外列,但如果您可以使用额外的列返回,则可能会获得更好的性能:

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列表中的第一个表达式是&#34;标记&#34;每当该行上的设备ID值是PREVIOUS行上的设备ID的DIFFERS时,就是一行。 HAVING子句过滤掉所有未标记为1的行。(您可以省略HAVING子句以查看该表达式的工作方式。)

(我没有测试这个是否有语法错误,如果你收到错误,请告诉我,我会仔细看看。我的桌面检查说它很好......但是&#&# 39;我可能错过了一个paren或逗号,)

(你可以通过将其包装在另一个查询中来删除&#34;那个额外的列

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

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

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

对于一大堆设备,这种方法可以很好地工作,而日志中只有几个相关的行。否则,这将产生一个中间结果集(在日志表中的行数的顺序)的大量混乱,这将被分离出来到临时的MyISAM表。

更新:

如果基本上从device得到所有行(谓词不是很有选择性),那么通过获取log中每个device_id的最新日志条目,可以获得更好的性能。表,并将连接推迟到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

注意:我们在内联视图别名为device_id的ORDER BY子句中的whens列中指定降序,而不是因为我们需要以降序的device_id顺序排列行,但允许MySQL执行&#34;反向扫描&#34;以避免文件排序操作。对具有前导列的索引进行操作(device_id,when)。

注意:此查询仍会将中间结果集作为临时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;

注意:由于iddevice表中的PRIMARY KEY(或唯一键),并且因为您没有进行任何JOIN,生成额外的行,您可以省略GROUP BY子句。

注意:此查询将使用&#34;嵌套循环&#34;操作。也就是说,对于从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)是唯一的时候,这种方法可以保证返回正确的&#34;结果集。

将此查询作为内联视图,以获取最新的&#34;当值:

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 

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