【问题标题】:Making a GROUP_CONCAT query more efficient使 GROUP_CONCAT 查询更高效
【发布时间】:2015-10-18 19:21:51
【问题描述】:

我有以下问题。这个想法是它让我知道groups 和随后的users 可以访问每个component_instance。我想知道是否有更好的方法来做到这一点,因为查询很慢,但是每次处理这个表时都有这些额外的列真的很方便:

SELECT component_instances.*, 
GROUP_CONCAT(DISTINCT IF(permissions.view, groups.id, NULL)) AS view_group_ids,
GROUP_CONCAT(DISTINCT IF(permissions.edit, groups.id, NULL)) AS edit_group_ids,
GROUP_CONCAT(DISTINCT IF(permissions.view, users.id, NULL)) AS view_user_ids,
GROUP_CONCAT(DISTINCT IF(permissions.edit, users.id, NULL)) AS edit_user_ids
FROM `component_instances`
LEFT OUTER JOIN permissions ON permissions.component_instance_id = component_instances.id
LEFT OUTER JOIN groups ON groups.id = permissions.group_id
LEFT OUTER JOIN groups_users ON groups_users.group_id = groups.id
LEFT OUTER JOIN users ON users.id = groups_users.user_id
GROUP BY component_instances.id
ORDER BY (case when component_instances.ancestry is null then 0 else 1 end), component_instances.ancestry, position

权限表是这样的(对不起 Rails!):

create_table "permissions", :force => true do |t|
  t.integer "component_instance_id"
  t.integer "group_id"
  t.boolean "view",                  :default => false
  t.boolean "edit",                  :default => false
end

权限类型有editview。可以分配一个组或同时分配一个组。权限也是递归的,如果component_instance 上没有组权限,我们必须检查其祖先以找到第一个设置权限的位置(如果有)。这使得一个查询变得非常重要,因为我可以将此查询与ancestry gem 提供的选择逻辑(物化路径树)结合起来。

更新

我发现这个查询基准测试更快:

SELECT component_instances.*,
GROUP_CONCAT(DISTINCT view_groups.id) AS view_group_ids,
GROUP_CONCAT(DISTINCT edit_groups.id) AS edit_group_ids,
GROUP_CONCAT(DISTINCT view_users.id) AS view_user_ids,
GROUP_CONCAT(DISTINCT edit_users.id) AS edit_user_ids
FROM `component_instances`
LEFT OUTER JOIN permissions ON permissions.component_instance_id = component_instances.id
LEFT OUTER JOIN groups view_groups ON view_groups.id = permissions.group_id AND permissions.view = 1
LEFT OUTER JOIN groups edit_groups ON edit_groups.id = permissions.group_id AND permissions.edit = 1
LEFT OUTER JOIN groups_users view_groups_users ON view_groups_users.group_id = view_groups.id
LEFT OUTER JOIN groups_users edit_groups_users ON edit_groups_users.group_id = edit_groups.id
LEFT OUTER JOIN users view_users ON view_users.id = view_groups_users.user_id
LEFT OUTER JOIN users edit_users ON edit_users.id = edit_groups_users.user_id
GROUP BY component_instances.id
ORDER BY (case when component_instances.ancestry is null then 0 else 1 end), component_instances.ancestry, position

这是对上述查询和表 CREATE 语句的解释:

+----+-------------+---------------------+--------+-----------------------------------------------+--------------------------------------------+---------+--------------------------------------------+------+------------------------------------------------------+
| id | select_type | table               | type   | possible_keys                                 | key                                        | key_len | ref                                        | rows | Extra                                                |
+----+-------------+---------------------+--------+-----------------------------------------------+--------------------------------------------+---------+--------------------------------------------+------+------------------------------------------------------+
| 1  | SIMPLE      | component_instances | ALL    | PRIMARY,index_component_instances_on_ancestry | NULL                                       | NULL    | NULL                                       | 119  | "Using temporary; Using filesort"                    |
| 1  | SIMPLE      | permissions         | ALL    | NULL                                          | NULL                                       | NULL    | NULL                                       | 6    | "Using where; Using join buffer (Block Nested Loop)" |
| 1  | SIMPLE      | view_groups         | eq_ref | PRIMARY                                       | PRIMARY                                    | 4       | 05707d890df9347c.permissions.group_id      | 1    | "Using where; Using index"                           |
| 1  | SIMPLE      | edit_groups         | eq_ref | PRIMARY                                       | PRIMARY                                    | 4       | 05707d890df9347c.permissions.group_id      | 1    | "Using where; Using index"                           |
| 1  | SIMPLE      | view_groups_users   | ref    | index_groups_users_on_group_id_and_user_id    | index_groups_users_on_group_id_and_user_id | 5       | 05707d890df9347c.view_groups.id            | 1    | "Using index"                                        |
| 1  | SIMPLE      | edit_groups_users   | ref    | index_groups_users_on_group_id_and_user_id    | index_groups_users_on_group_id_and_user_id | 5       | 05707d890df9347c.edit_groups.id            | 1    | "Using index"                                        |
| 1  | SIMPLE      | view_users          | eq_ref | PRIMARY                                       | PRIMARY                                    | 4       | 05707d890df9347c.view_groups_users.user_id | 1    | "Using index"                                        |
| 1  | SIMPLE      | edit_users          | eq_ref | PRIMARY                                       | PRIMARY                                    | 4       | 05707d890df9347c.edit_groups_users.user_id | 1    | "Using index"                                        |
+----+-------------+---------------------+--------+-----------------------------------------------+--------------------------------------------+---------+--------------------------------------------+------+------------------------------------------------------+

CREATE TABLE `component_instances` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `visible` int(11) DEFAULT '1',
  `instance_id` int(11) DEFAULT NULL,
  `deleted_on` date DEFAULT NULL,
  `instance_type` varchar(255) DEFAULT NULL,
  `component_id` int(11) DEFAULT NULL,
  `deleted_root_item` int(11) DEFAULT NULL,
  `locked_until` datetime DEFAULT NULL,
  `theme_id` int(11) DEFAULT NULL,
  `position` int(11) DEFAULT NULL,
  `ancestry` varchar(255) DEFAULT NULL,
  `ancestry_depth` int(11) DEFAULT '0',
  `cached_name` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `index_component_instances_on_ancestry` (`ancestry`)
) ENGINE=InnoDB AUTO_INCREMENT=121 DEFAULT CHARSET=utf8

CREATE TABLE `groups` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) NOT NULL DEFAULT '',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8

CREATE TABLE `groups_users` (
  `group_id` int(11) DEFAULT NULL,
  `user_id` int(11) DEFAULT NULL,
  KEY `index_groups_users_on_group_id_and_user_id` (`group_id`,`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

CREATE TABLE `permissions` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `component_instance_id` int(11) DEFAULT NULL,
  `group_id` int(11) DEFAULT NULL,
  `view` tinyint(1) DEFAULT '0',
  `edit` tinyint(1) DEFAULT '0',
  PRIMARY KEY (`id`),
  KEY `edit_permissions_index` (`edit`,`group_id`,`component_instance_id`),
  KEY `view_permissions_index` (`view`,`group_id`,`component_instance_id`)
) ENGINE=InnoDB AUTO_INCREMENT=28 DEFAULT CHARSET=utf8

CREATE TABLE `users` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `real_name` varchar(255) DEFAULT NULL,
  `username` varchar(255) NOT NULL DEFAULT '',
  `email` varchar(255) NOT NULL DEFAULT '',
  `crypted_password` varchar(255) DEFAULT NULL,
  `administrator` int(11) NOT NULL DEFAULT '0',
  `password_salt` varchar(255) DEFAULT NULL,
  `remember_token_expires` datetime DEFAULT NULL,
  `persistence_token` varchar(255) DEFAULT NULL,
  `disabled` tinyint(1) DEFAULT NULL,
  `time_zone` varchar(255) DEFAULT NULL,
  `login_count` int(11) DEFAULT NULL,
  `failed_login_count` int(11) DEFAULT NULL,
  `last_request_at` datetime DEFAULT NULL,
  `current_login_at` datetime DEFAULT NULL,
  `last_login_at` datetime DEFAULT NULL,
  `current_login_ip` varchar(255) DEFAULT NULL,
  `last_login_ip` varchar(255) DEFAULT NULL,
  `perishable_token` varchar(255) NOT NULL DEFAULT '',
  PRIMARY KEY (`id`),
  UNIQUE KEY `index_users_on_username` (`username`),
  KEY `index_users_on_perishable_token` (`perishable_token`)
) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8

ORDER BY 来自 ancestry gem,但如果有更好的方法可以做到这一点,我很乐意将其作为拉取请求提交给他们。

【问题讨论】:

  • 习惯上将所有文本保留在问题中,如果我是你,我会使用UPDATE 行分隔我的每个更新,并将词干全部保留在问题部分中。它使阅读更加清晰。
  • 谢谢 Mehran,我已经更新了。我最初去回答我自己的问题,然后想到做一个赏金。
  • 另外我认为如果使用第二个版本,您可以省略最后两个连接并在 group_concat 中使用 view_groups_users.user_id 和 edit_groups_users.user_id
  • 大声笑,你说得对!我绝对可以摆脱用户表本身的加入。我会根据你的建议调查祖先排序的事情:)

标签: mysql ruby-on-rails group-by group-concat


【解决方案1】:

NULL 放在首位(也可以使用 COALESCENULL 替换为其他内容,而不是使用额外的排序列)。第二件事是减少连接,因为最后两个在我们连接的 id 上。

SELECT
   component_instances.*,
   GROUP_CONCAT(DISTINCT view_groups.id) AS view_group_ids,
   GROUP_CONCAT(DISTINCT edit_groups.id) AS edit_group_ids,
   GROUP_CONCAT(DISTINCT view_groups_users.user_id) AS view_user_ids,
   GROUP_CONCAT(DISTINCT edit_groups_users.user_id) AS edit_user_ids
FROM
   `component_instances`
   LEFT OUTER JOIN permissions
      ON permissions.component_instance_id = component_instances.id
   LEFT OUTER JOIN groups view_groups
      ON view_groups.id = permissions.group_id AND permissions.view = 1
   LEFT OUTER JOIN groups edit_groups
      ON edit_groups.id = permissions.group_id AND permissions.edit = 1
   LEFT OUTER JOIN groups_users view_groups_users
      ON view_groups_users.group_id = view_groups.id
   LEFT OUTER JOIN groups_users edit_groups_users
      ON edit_groups_users.group_id = edit_groups.id
GROUP BY
   component_instances.id
ORDER BY
   component_instances.ancestry, -- MySQL was sorting the NULL values already correctly
   position
;

【讨论】:

  • 谢谢 Maraca,抱歉我先接受了其他用户的回答,因为我以为是你!我已经扭转了这一点。你是对的,NULL 放在第一位。我怀疑它支持另一种数据库类型的代码可能是因为祖先库不仅适用于 MySQL。不过我可以覆盖那部分,所以我会这样做。
  • 不幸的是,您上面的查询导致 view_user_ids 和 edit_user_ids 与我的联接查询的结果不同。它们几乎同时执行,所以除非你想知道为什么会这样,否则我很乐意接受没有子选择的更简单的答案。
  • 我想他们一定是。查看结果,就好像子选择只获取第一个组 id 而忽略其余部分。额外的连接肯定会按预期工作。
  • 不客气。我才意识到我必须单独分配赏金。 :)
  • @BrendonMuir 很多人忘记了它,这就是为什么如果您接受答案并且不分配赏金,接受的答案将在宽限期后收到它。否则,评分最高的答案(我认为最低得分为 2 或 3)但如果我没记错的话,那么只有 2/3 的赏金。谢谢,第一次赏金:)
【解决方案2】:

如果我们没有您的表结构和索引,几乎不可能优化您的查询。使用EXPLAIN 语句是查询优化的必要部分。

如果没有提到的信息,我只能对您的问题发表评论是,您的ORDER BY 部分肯定可以从一些优化中受益。在条件中使用任何函数或语句总是会导致灾难。同样在ORDER BY 中使用可为空的字段也会导致问题。也许最简单的方法是在您的表中添加一个包含 0 和 1 的新字段,而不是当前的 CASE 语句。

不要忘记,如果记录数量相当多,则始终需要在条件/排序依据/分组依据中的任何字段上建立索引。

[更新]

您的查询相当简单。 EXPLAIN 的结果表明,唯一适合作为候选索引的部分是:

CREATE INDEX inx4 ON permissions (`component_instance_id`, `group_id`, `edit`, `view`);

EXPLAIN 的第二行显示您的查询中没有使用表permissions 的索引。这是因为 MySQL 在使用索引时有几个规则:

  • 在每个(子)查询中只能使用每个表的一个索引。
  • 只有在查询中提到了所有字段(如条件/排序依据/分组依据)时,才能使用任何索引。

考虑到您的查询,以及表permissions 的所有四个字段都被提及的事实,您需要对所有四个字段都建立一个索引,否则它是无用的。

然而ORDER BY 可以从我之前提到的修正中受益。

【讨论】:

  • 谢谢 Mehran,我已经添加了您要求的额外细节。我绝对对 ORDER BY 语句感兴趣,请参阅更新后的问题。我在上面的答案中解释了查询,而不是问题中的查询。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-06-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多