Повышение эффективности запроса GROUP_CONCAT

У меня есть следующий запрос. Идея состоит в том, что она позволяет мне узнать, что 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

Типы разрешений edit и view. Группе может быть присвоена одна или обе. Разрешения также являются рекурсивными в том смысле, что если групповых разрешений на 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

Вот пример EXPLAIN для запроса выше и операторов таблицы 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 происходит от родословной жемчужины, но если есть лучший способ сделать это, я был бы рад представить это как потяните за них запрос.

Ответ 1

NULL помещается первым (можно использовать COALESCE для замены NULL чем-то другим, вместо использования дополнительного столбца сортировки). Второе - это сокращение объединений, поскольку последние два были на 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
;

Ответ 2

Почти невозможно оптимизировать ваш запрос, если у нас нет вашей структуры таблицы и индексов. Использование оператора EXPLAIN является необходимой частью оптимизации запросов.

Без упомянутой информации все, что я могу прокомментировать ваш вопрос, состоит в том, что ваша часть ORDER BY может извлечь определенную выгоду из некоторой оптимизации. Использование любых функций или операторов в состоянии всегда приведет к катастрофе. Также использование поля с нулевым значением в ORDER BY также приведет к проблемам. Возможно, самым простым способом было бы добавление нового поля в вашу таблицу с 0s и 1s вместо текущего оператора CASE.

Не забывайте, что наличие индекса в любом поле в условии/порядке по/группе всегда необходимо, если количество записей значимо.

[ОБНОВЛЕНИЕ]

Ваш запрос довольно прост. Результат EXPLAIN показывает, что единственными частями, подходящими для индексации, являются:

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

Вторая строка EXPLAIN показывает, что в вашем запросе не используется индекс таблицы permissions. Это потому, что MySQL имеет несколько правил, когда он будет использовать индексы:

  • В каждом запросе (под-) может использоваться только один индекс каждой таблицы.
  • Любой индекс может использоваться только в том случае, если все его поля упоминаются в запросе (как в условиях/порядке по/группе).

Учитывая ваш запрос и тот факт, что все четыре поля таблицы permissions упомянуты, вам понадобится индекс для всех четырех или бесполезный.

Тем не менее ORDER BY может воспользоваться поправкой, о которой я упоминал ранее.