I have a challenging question for MySQL experts.
I have a users permissions system with 4 tables:
users (id | email | created_at)
permissions (id | responsibility_id | key | weight)
permission_user (id | permission_id | user_id)
responsibilities (id | key | weight)
Users can have any number of permissions assigned and any permission can be granted to any number of users (many to many). Responsibilities are like groups for permissions, each permission belongs to exactly one responsibility. For example, one permission is called update
with responsibility of customers
. Another one would be delete
with orders
responsibility.
I need to get a full map of permissions per user, but only for those who have at least one permission granted. Results should be ordered by:
created_at
column, oldest firstweight
weight
Example result set:
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
Let's say I have 10 users in database, but only two of them have any permissions granted. There are 4 permissions in total:
create
of customers
responsibilityupdate
of customers
responsibilitycreate
of orders
responsibilityupdate
of orders
responsibility.That's why we have 8 records in results (2 users with any permission × 4 permissions). User with id = 5 is displayed first, because he's got more permissions. If there were any draws, the ones with older created_at
date would go first. Permissions are always sorted by the weight of their responsibility and then by their own weight.
My question is, how to write optimal query for this case? I have already made one myself and it works good:
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
The problem is that I'm using the same subquery twice.
Can I do better? I count on you, MySQL experts!
--- EDIT ---
Thanks to Gordon Linoff's comment I made it use HAVING
clause:
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
I was surprised to discover that HAVING
can go alone without GROUP BY
.
Can it now be improved for better performance?
The following recommendations will help you in your SQL tuning process.
You'll find 3 sections below:
ALTER TABLE `permission_user` ADD INDEX `permission_user_idx_user_id` (`user_id`);
ALTER TABLE `permissions` ADD INDEX `permissions_idx_id` (`id`);
ALTER TABLE `responsibilities` ADD INDEX `responsibilities_idx_id` (`id`);
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