2015-01-20 3 views
0

У меня есть сложный вопрос для экспертов MySQL.Как оптимизировать этот запрос MySQL? (CROSS JOIN, subquery)

У меня есть система прав пользователей с 4-мя столами:

  1. users (id | email | created_at)
  2. permissions (id | responsibility_id | key | weight)
  3. permission_user (id | permission_id | user_id)
  4. responsibilities (id | key | weight)

Пользователи могут иметь любое количество разрешений, назначенных и любого разрешения может быть предоставлено любому количеству использования rs (многие ко многим). Ответственность подобна группам для разрешений, каждое разрешение принадлежит только одной ответственности. Например, одно разрешение называется update с ответственностью customers. Еще один будет delete с ответственностью orders.

Мне нужно получить полную карту разрешений для каждого пользователя, но только для тех, у кого есть хотя бы одно разрешение. Результаты должны быть отсортированы по: номеру

  1. пользователя разрешений от большего к меньшей
  2. created_at колонка пользователя, сначала старый
  3. ответственности в weight
  4. разрешение в weight

Примера результирующий набор:

user_id | responsibility | permission | granted 
----------------------------------------------- 
     5 | customers  | create  |  1 
     5 | customers  | update  |  1 
     5 | orders   | create  |  1 
     5 | orders   | update  |  1 
     2 | customers  | create  |  0 
     2 | customers  | delete  |  0 
     2 | orders   | create  |  1 
     2 | orders   | update  |  0 

Предположим, что у меня 10 пользователей в базе данных, но только у двух из них есть разрешения. Есть 4 разрешения в общей сложности:

  1. create из customers ответственности
  2. update из customers ответственности
  3. create из orders ответственности
  4. update из orders ответственности.

Вот почему у нас есть 8 записей в результатах (2 пользователя с разрешением × 4 разрешений). Сначала отображается пользователь с id = 5, потому что у него больше прав. Если бы были какие-то ничьи, то первые со старыми created_at будут идти первым. Разрешения всегда сортируются по весу их ответственности, а затем по собственному весу.

Мой вопрос в том, как написать оптимальный запрос для этого случая? Я уже сделал один я, и он работает хорошо:

SELECT `users`.`id` AS `user_id`, 
     `responsibilities`.`key` AS `responsibility`, 
     `permissions`.`key` AS `permission`, 
     !ISNULL(`permission_user`.`id`) AS `granted` 
FROM `users` 
CROSS JOIN `permissions` 
JOIN `responsibilities` 
    ON `responsibilities`.`id` = `permissions`.`responsibility_id` 
LEFT JOIN `permission_user` 
     ON `permission_user`.`user_id` = `users`.`id` 
     AND `permission_user`.`permission_id` = `permissions`.`id` 
WHERE (
    SELECT COUNT(*) 
    FROM `permission_user` 
    WHERE `user_id` = `users`.`id` 
) > 0 
ORDER BY (
      SELECT COUNT(*) 
      FROM `permission_user` 
      WHERE `user_id` = `users`.`id` 
     ) DESC, 
     `users`.`created_at` ASC, 
     `responsibilities`.`weight` ASC, 
     `permissions`.`weight` ASC 

Проблема заключается в том, что я использую тот же подзапрос дважды.

Могу ли я сделать лучше? Я рассчитываю на вас, эксперты MySQL!

--- EDIT ---

Благодаря комментарий Гордон Линофф, я сделал это использовать HAVING пункт:

SELECT `users`.`email`, 
     `responsibilities`.`key`, 
     `permissions`.`key`, 
     !ISNULL(`permission_user`.`id`) as `granted`, 
     (
      SELECT COUNT(*) 
      FROM `permission_user` 
      WHERE `user_id` = `users`.`id` 
     ) AS `total_permissions` 
FROM `users` 
CROSS JOIN `permissions` 
JOIN `responsibilities` 
    ON `responsibilities`.`id` = `permissions`.`responsibility_id` 
LEFT JOIN `permission_user` 
     ON `permission_user`.`user_id` = `users`.`id` 
     AND `permission_user`.`permission_id` = `permissions`.`id` 
HAVING `total_permissions` > 0 
ORDER BY `total_permissions` DESC, 
     `users`.`created_at` ASC, 
     `responsibilities`.`weight` ASC, 
     `permissions`.`weight` ASC 

Я был удивлен, обнаружив, что HAVING может идти в одиночку без GROUP BY.

Можно ли улучшить его улучшение для лучшей производительности?

+1

Добавить подзапрос в предложение 'select'. Затем вы можете фильтровать, используя псевдоним в предложении 'having', а также использовать псевдоним в' order by'. –

+0

Параметр where-where гарантирует, что есть строки разрешающего_ищика, т. Е. LEFT JOIN может быть регулярным JOIN. – jarlh

+0

@jarlh Пожалуйста, внимательно прочитайте мой вопрос. Я сказал, что мне нужно отображать ВСЕ разрешения, а не только те, которые предоставляются. Нужно много нулей. –

ответ

1

Вероятно, наиболее эффективный способ сделать это:

SELECT u.email, r.`key`, r.`key`, 
     !ISNULL(pu.id) as `granted` 
FROM (SELECT u.*, 
      (SELECT COUNT(*) FROM `permission_user` pu WHERE pu.user_id = u.id 
     ) AS `total_permissions` 
     FROM `users` u 
    ) u CROSS JOIN 
    permissions p JOIN 
    responsibilities r 
    ON r.id = p.responsibility_id LEFT JOIN 
    permission_user pu 
    ON pu.user_id = u.id AND 
     pu.permission_id = p.id 
WHERE u.total_permissions > 0 
ORDER BY `total_permissions` DESC, 
     `users`.`created_at` ASC, 
     `responsibilities`.`weight` ASC, 
     `permissions`.`weight` ASC; 

Это будет работать подзапрос один раз для каждого пользователя, а не один раз для каждого пользователя/сочетание разрешения (и как модифицированного запроса и исходного запроса делают). Это имеет две издержки. Во-первых, это материализация подзапроса, поэтому данные в таблице пользователей должны быть прочитаны и записаны снова. Наверное, это не большое дело, учитывая все остальное в запросе. Вторая - потеря индексов на таблице users. Еще раз, с cross join, индексы (вероятно) не используются, поэтому это тоже незначительно.