innodb 的 COUNT 替代方案以防止 table 扫描?

Alternative to COUNT for innodb to prevent table scan?

我已经设法组合了一个适合我需要的查询,尽管它比我希望的要复杂。但是,对于 tables 的大小,查询比应有的速度慢 (0.17s)。根据下面提供的 EXPLAIN,原因是 meta_relationships table 上有一个 table 扫描,因为 COUNTWHERE innodb 引擎上的子句。

查询:

SELECT
posts.post_id,posts.post_name,
GROUP_CONCAT(IF(meta_data.type = 'category', meta.meta_name,null)) AS category,
GROUP_CONCAT(IF(meta_data.type = 'tag', meta.meta_name,null)) AS tag
FROM posts
RIGHT JOIN meta_relationships ON (posts.post_id = meta_relationships.object_id)
LEFT JOIN meta_data ON meta_relationships.meta_data_id = meta_data.meta_data_id
LEFT JOIN meta ON meta_data.meta_id = meta.meta_id
WHERE meta.meta_name = computers AND meta_relationships.object_id 
NOT IN (SELECT meta_relationships.object_id FROM meta_relationships
        GROUP BY meta_relationships.object_id HAVING count(*) > 1)
GROUP BY meta_relationships.object_id

这个特定的查询,selects 帖子只有 computers 类别。 count > 1 的目的是排除包含 computers/hardwarecomputers/software 等的帖子。selected 的类别越多,计数就越高。

理想情况下,我想让它像这样运行:

WHERE meta.meta_name IN ('computers') AND meta_relationships.meta_order IN (0)

WHERE meta.meta_name IN ('computers','software') 
AND meta_relationships.meta_order IN (0,1)

等..

但不幸的是,这不起作用,因为它没有考虑到可能存在 meta_relationships.meta_order = 2。

我试过了...

WHERE meta.meta_name IN ('computers')
GROUP BY meta_relationships.meta_order
HAVING meta_relationships.meta_order IN (0) AND meta_relationships.meta_order NOT IN (1)

但它 return 行数不正确。

解释:

id  select_type   table               type    possible_keys          key               key_len ref                                   rows   Extra   
1   PRIMARY       meta                ref     PRIMARY,idx_meta_name  idx_meta_name     602     const                                 1      Using where; Using index; Using temporary; Using filesort
1   PRIMARY       meta_data           ref     PRIMARY,idx_meta_id    idx_meta_id       8       database.meta.meta_id                 1  
1   PRIMARY       meta_relationships  ref     idx_meta_data_id       idx_meta_data_id  8       database.meta_data.meta_data_id       11     Using where
1   PRIMARY       posts               eq_ref  PRIMARY                PRIMARY           4       database.meta_relationships.object_id 1  
2   MATERIALIZED  meta_relationships  index   NULL                   idx_object_id     4       NULL                                  14679  Using index

Tables/Indexes:

此 table 包含类别和标签名称。
索引:
主键 (meta_id), 键 idx_meta_name (meta_name)
meta_data
此 table 包含有关类别和标签的其他数据,例如类型(类别或标签)、描述、父项、计数。
索引:
主键 (meta_data_id), 键 idx_meta_id (meta_id)
meta_relationships
这是一个junction/lookuptable。它包含 posts_id 的外键,meta_data_id 的外键,还包含类别的顺序。
索引:
主键 (relationship_id), 键 idx_object_id (object_id), 键 idx_meta_data_id (meta_data_id)

如何优化这个查询?

编辑:

我一直没能找到解决这个问题的最佳方案。这实际上是 smcjones 改进索引的建议的组合,我建议对其进行 EXPLAIN 并查看 EXPLAIN Output Format,然后将索引更改为任何可以提供最佳性能的索引。
此外,hpf 建议添加另一列总计数帮助极大。最后,在更改索引后,我结束了这个查询。

SELECT posts.post_id,posts.post_name,
GROUP_CONCAT(IF(meta_data.type = 'category', meta.meta_name,null)) AS category,
GROUP_CONCAT(IF(meta_data.type = 'tag', meta.meta_name,null)) AS tag
FROM posts
JOIN meta_relationships ON meta_relationships.object_id = posts.post_id
JOIN meta_data ON meta_relationships.meta_data_id = meta_data.meta_data_id
JOIN meta ON meta_data.meta_id = meta.meta_id
WHERE posts.meta_count = 2
GROUP BY posts.post_id
HAVING category = 'category,subcategory'

摆脱 COUNT 后,最大的性能杀手是 GROUP BYORDER BY,但索引是您最好的朋友。我了解到做一个GROUP BY时,WHERE子句很重要,越具体越好。

看看这是否为您提供了正确的答案,可能更快:

SELECT  p.post_id, p.post_name,
        GROUP_CONCAT(IF(md.type = 'category', meta.meta_name, null)) AS category,
        GROUP_CONCAT(IF(md.type = 'tag', meta.meta_name, null)) AS tag
    FROM  
      ( SELECT  object_id
            FROM  meta_relation
            GROUP BY  object_id
            HAVING  count(*) = 1 
      ) AS x
    JOIN  meta_relation AS mr ON mr.object_id = x.object_id
    JOIN  posts AS p ON p.post_id = mr.object_id
    JOIN  meta_data AS md ON mr.meta_data_id = md.meta_data_id
    JOIN  meta ON md.meta_id = meta.meta_id
    WHERE  meta.meta_name = ?
    GROUP BY  mr.object_id 

由于 HAVING 似乎是问题所在,您能否改为在 posts table 中创建一个标志字段并改用它?如果我对查询的理解正确,那么您正在尝试查找只有一个 meta_relationship link 的 post。如果您在 posts table 中创建了一个字段,它是 post 的 meta_relationships 的计数,或者是否只有一个的布尔标志,并当然将其编入索引,这可能会快得多。如果 post 被编辑,这将涉及更新字段。

所以,考虑一下:

向 post 的 table 添加一个名为 "num_meta_rel" 的新字段。它可以是一个无符号的 tinyint,只要你永远不会有超过 255 个标签到任何一个 post。

像这样更新字段:

UPDATE posts
SET num_meta_rel=(SELECT COUNT(object_id) from meta_relationships WHERE object_id=posts.post_id);

此查询需要一些时间才能完成 运行,但一旦完成,您就可以预先计算所有计数。请注意,这可以通过连接更好地完成,但 SQLite (Ideone) 只允许子查询。

现在,您像这样重写查询:

SELECT
posts.post_id,posts.post_name,
GROUP_CONCAT(IF(meta_data.type = 'category', meta.meta_name,null)) AS category,
GROUP_CONCAT(IF(meta_data.type = 'tag', meta.meta_name,null)) AS tag
FROM posts
RIGHT JOIN meta_relationships ON (posts.post_id = meta_relationships.object_id)
LEFT JOIN meta_data ON meta_relationships.meta_data_id = meta_data.meta_data_id
LEFT JOIN meta ON meta_data.meta_id = meta.meta_id
WHERE meta.meta_name = computers AND posts.num_meta_rel=1
GROUP BY meta_relationships.object_id

如果我没有做错,运行可用代码在这里:http://ideone.com/ZZiKgx

请注意,此解决方案要求您更新 num_meta_rel(选择一个更好的名称,那个太糟糕了......)如果 post 有一个与之关联的新标签。但这应该比一遍又一遍地扫描整个 table 快得多。

通过结合优化查询 AND 优化您的 table,您将获得快速查询。但是,如果没有经过优化的 table.

,您将无法进行快速查询

我怎么强调都不为过:如果您的 table 结构正确且索引数量正确,则您不应该在查询中遇到任何完整的 table 读取像 GROUP BY...HAVING 除非你有意这样做。

根据您的示例,我创建了 this SQLFiddle

将其与 SQLFiddle #2 进行比较,其中我添加了索引并针对 meta.meta_naame 添加了一个 UNIQUE 索引。

根据我的测试,Fiddle#2 更快。

优化您的查询

这个查询让我发疯,即使在我提出索引是优化它的最佳方式之后也是如此。尽管我仍然认为 table 是提高性能的最大机会,但似乎必须有更好的方法来 运行 MySQL 中的此查询。在解决这个问题后我得到了启示,并使用了以下查询(见in SQLFiddle #3):

SELECT posts.post_id,posts.post_name,posts.post_title,posts.post_description,posts.date,meta.meta_name
   FROM posts
   LEFT JOIN meta_relationships ON meta_relationships.object_id = posts.post_id
   LEFT JOIN meta_data ON meta_relationships.meta_data_id = meta_data.meta_data_id
   LEFT JOIN meta ON meta_data.meta_id = meta.meta_id
   WHERE meta.meta_name = 'animals'
   GROUP BY meta_relationships.object_id
   HAVING sum(meta_relationships.object_id) = min(meta_relationships.object_id);

HAVING sum() = min() on a GROUP BY 应该检查每种类型是否有多个记录。显然,每次记录出现时,总和都会增加更多。 (编辑:在随后的测试中,这似乎与 count(meta_relationships.object_id) = 1 具有相同的影响。哦,重点是我相信您可以删除子查询并获得相同的结果)。

我想明确一点,如果对我提供给您的查询进行任何优化,您不会注意到太多,除非 WHERE meta.meta_name = 'animals' 部分正在查询索引(最好是唯一索引,因为我怀疑您'您将需要其中的多个,这将防止意外重复数据)。

所以,而不是像这样的 table:

CREATE TABLE meta_data (
  meta_data_id BIGINT,
  meta_id BIGINT,
  type VARCHAR(50),
  description VARCHAR(200),
  parent BIGINT,
  count BIGINT);

您应该确保像这样添加主键和索引:

CREATE TABLE meta_data (
  meta_data_id BIGINT,
  meta_id BIGINT,
  type VARCHAR(50),
  description VARCHAR(200),
  parent BIGINT,
  count BIGINT,
  PRIMARY KEY (meta_data_id,meta_id),
  INDEX ix_meta_id (meta_id)
);

不要过度,但每个 table 都应该有一个主键,并且任何时候你在聚合或查询特定值时,都应该有索引。

当不使用索引时,MySQL 将遍历 table 的每一行,直到找到您想要的内容。在像您这样的有限示例中,这不会花费太长时间(即使它仍然明显较慢),但是当您添加数千条或更多条记录时,这将变得异常痛苦。

将来,在查看您的查询时,请尝试确定完整 table 扫描发生的位置,并查看该列上是否有索引。一个好的起点是聚合或使用 WHERE 语法的任何地方。

关于 count 列的注释

我没有发现将 count 列放入 table 中有什么帮助。它会导致一些非常严重的完整性问题。如果一个 table 被适当优化,它应该很容易使用 count() 并获得当前计数。如果你想把它放在 table 中,你可以使用 VIEW,尽管这不是最有效的拉动方式。

count 列放入 table 的问题在于您需要使用 TRIGGER 或更糟的应用程序逻辑来更新该计数。随着程序的扩展,逻辑可能会丢失或被埋没。添加该列是对规范化的偏离,当发生这样的事情时,应该有一个 非常 很好的理由。

关于是否有曾经这样做的充分理由存在一些争论,但我认为我最好远离这场争论,因为有很多争论在两侧。相反,我会选择一个小得多的战斗,并说我看到这在这个用例中给你带来的麻烦多于好处,所以它可能值得 A/B 测试。

遗憾的是我无法测试性能,

但请使用您的真实数据尝试我的查询:

http://sqlfiddle.com/#!9/81b29/13

SELECT
posts.post_id,posts.post_name,
GROUP_CONCAT(IF(meta_data.type = 'category', meta.meta_name,null)) AS category,
GROUP_CONCAT(IF(meta_data.type = 'tag', meta.meta_name,null)) AS tag
FROM posts
INNER JOIN (
  SELECT meta_relationships.object_id
   FROM meta_relationships 
   GROUP BY meta_relationships.object_id 
   HAVING count(*) < 3
  ) mr ON mr.object_id = posts.post_id
LEFT JOIN meta_relationships ON mr.object_id = meta_relationships.object_id
LEFT JOIN meta_data ON meta_relationships.meta_data_id = meta_data.meta_data_id
INNER JOIN (
  SELECT * 
  FROM meta
  WHERE  meta.meta_name = 'health'
  ) meta ON meta_data.meta_id = meta.meta_id
GROUP BY posts.post_id

使用

sum(1)

而不是

count(*)