您的查询和以下策略将受益于 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,表中的当前when
值log_latest
,并插入/更新log_latest
表中的行,以便始终存在最新的行。您还可以(冗余)将设备名称存储在表中。(或者,您可以添加latest_when
和一个latest_message
列到设备表,并在那里维护它们。)
但是这个策略超出了你原来的问题......但如果你需要经常运行“所有设备的最新日志消息”查询,这是一个可行的策略。缺点是您有一个额外的表,并且在对log
表执行插入时会影响性能。可以使用与原始查询类似的查询或以下替代方法来完全刷新此表。
一种方法是一个查询,它对device
和log
表进行简单连接,获取按设备和降序排列的行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)的索引进行扫描”操作。when
s
注意:此查询仍会将中间结果集作为临时 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
所有这些都是替代策略(我相信这是您提出的问题),但我不确定其中任何一个是否会更好地满足您的特定需求。(但在工具带中有几个不同的工具总是好的,以适当使用。)