Search code examples
mysqljoinleft-joinsql-viewright-join

How do I join a many-to-many where the left table can be null OR the right table can be null?


Here is my schema and test data to create a minimum reproducible example:

CREATE TABLE `scpsl_user_id_bans` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4;

CREATE TABLE `scpsl_ip_bans` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4;

CREATE TABLE `scpsl_ban_link` (
  `user_id_ban_id` bigint(20) unsigned DEFAULT NULL,
  `ip_ban_id` bigint(20) unsigned DEFAULT NULL,
  `start_date_skew` bigint(20) DEFAULT NULL,
  `end_date_skew` bigint(20) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

INSERT INTO `scpsl_user_id_bans` VALUES (1, "First Ban");
INSERT INTO `scpsl_user_id_bans` VALUES (2, "Second Ban");
INSERT INTO `scpsl_user_id_bans` VALUES (3, "Third Ban");
INSERT INTO `scpsl_user_id_bans` VALUES (4, "Fourth Ban");

INSERT INTO `scpsl_ip_bans` VALUES  (3, "Third Ban");
INSERT INTO `scpsl_ip_bans` VALUES  (4, "Fourth Ban");
INSERT INTO `scpsl_ip_bans` VALUES  (5, "Fifth Ban");
INSERT INTO `scpsl_ip_bans` VALUES  (6, "Sixth Ban");

INSERT INTO `temp`.`scpsl_ban_link` VALUES ('3', '3', '0', '0');
INSERT INTO `temp`.`scpsl_ban_link` VALUES ('4', '4', '0', '0');

This is the query I want my VIEW to work with:

SELECT DISTINCT
    `idbans`.`id` AS `user_id_ban_id`,
    `ipbans`.`id` AS `ip_ban_id`,
    `idbans`.`name` AS `user_id_ban_name`,
    `ipbans`.`name` AS `ip_ban_name`,
    `idbans`.`user_id` AS `user_id`,
    `ipbans`.`user_id` AS `ip_address`
FROM `scpsl_ban_link` `ban_link`
LEFT OUTER JOIN `scpsl_user_id_bans` `idbans` ON (`idbans`.`id` = `ban_link`.`user_id_ban_id`)
RIGHT OUTER JOIN `scpsl_ip_bans` `ipbans` ON (`ban_link`.`ip_ban_id` = `ipbans`.`id`)
WHERE user_id_ban_id IS NULL or ip_ban_id IS NULL
ORDER BY user_id_ban_id DESC
;

And I expect to view this data:

+------+------------+----------------+-----------+-------+------------+
| id   | name       | user_id_ban_id | ip_ban_id | id    | name       |
+------+------------+----------------+-----------+-------+------------+
|    1 | First Ban  |              1 |         1 |  NULL | NULL       |
|    2 | Second Ban |              2 |         2 |  NULL | NULL       |
|    3 | Third Ban  |              3 |         3 |     3 | Third Ban  |
|    4 | Fourth Ban |              4 |         4 |     4 | Fourth Ban |
| NULL | NULL       |           NULL |         5 |     5 | Fifth Ban  |
| NULL | NULL       |           NULL |         6 |     6 | Sixth Ban  |
+------+------------+----------------+-----------+-------+------------+

However, MySQL creates this SQL when I try to create a view:

CREATE 
    ALGORITHM = UNDEFINED 
    DEFINER = `scpsl`@`localhost` 
    SQL SECURITY DEFINER
VIEW `scpsl_print_bans` AS
    SELECT DISTINCT
        `idbans`.`id` AS `user_id_ban_id`,
        `ipbans`.`id` AS `ip_ban_id`,
        `idbans`.`name` AS `user_id_ban_name`,
        `ipbans`.`name` AS `ip_ban_name`,
        `idbans`.`user_id` AS `user_id`,
        `ipbans`.`user_id` AS `ip_address`,
        FROM_DOTNETTICKS(`idbans`.`start_date`) AS `start_date`,
        FROM_DOTNETTICKS(`idbans`.`end_date`) AS `end_date`,
        `idbans`.`admin` AS `admin`,
        `idbans`.`reason` AS `reason`,
        `idbans`.`active` AS `user_id_ban_active`,
        `ipbans`.`active` AS `ip_ban_active`,
        CONCAT('UPDATE scpsl_user_id_bans SET active = 0 WHERE id = ',
                `idbans`.`id`) AS `unban_id`,
        CONCAT('UPDATE scpsl_ip_bans SET active = 0 WHERE id = ',
                `ipbans`.`id`) AS `unban_ip`
    FROM
        (`scpsl_ip_bans` `ipbans`
        LEFT JOIN (`scpsl_ban_link` `ban_link`
        LEFT JOIN `scpsl_user_id_bans` `idbans` ON ((`idbans`.`id` = `ban_link`.`user_id_ban_id`))) ON ((`ban_link`.`ip_ban_id` = `ipbans`.`id`)))
    ORDER BY `user_id_ban_id` DESC

Why is the view being mangled? How do I get my data to display like this, where a ban is always present in both (scpsl_user_id_bans AND scpsl_ban_link) OR (scpsl_ban_link OR scpsl_ip_bans)?


Solution

  • The query that I wanted contained THREE queries, UNIONed together:

    SELECT
        `scpsl_user_id_bans`.`id` as `scpsl_user_id_bans_id`,
        `scpsl_user_id_bans`.`name` as `scpsl_user_id_bans_name`,
        `user_id_ban_id`,
        `ip_ban_id`,
        null as `scpsl_ip_bans_id`,
        null as `scpsl_ip_bans_name`
    FROM
        `scpsl_user_id_bans`
        LEFT OUTER JOIN `scpsl_ban_link` ON (`scpsl_ban_link`.`user_id_ban_id` = `scpsl_user_id_bans`.`id`)
        WHERE `ip_ban_id` IS NULL
    UNION
    SELECT
        `scpsl_user_id_bans`.`id` as `scpsl_user_id_bans_id`,
        `scpsl_user_id_bans`.`name` as `scpsl_user_id_bans_name`,
        `user_id_ban_id`,
        `ip_ban_id`,
        `scpsl_ip_bans`.`id` as `scpsl_ip_bans_id`,
        `scpsl_ip_bans`.`name` as `scpsl_ip_bans_name`
    FROM
        `scpsl_user_id_bans`
        LEFT JOIN `scpsl_ban_link` ON (`scpsl_ban_link`.`user_id_ban_id` = `scpsl_user_id_bans`.`id`)
        LEFT JOIN `scpsl_ip_bans` ON (`scpsl_ban_link`.`ip_ban_id` = `scpsl_ip_bans`.`id`)
    UNION
    SELECT
        null as `scpsl_user_id_bans_id`, 
        null as `scpsl_user_id_bans_name`,
        `user_id_ban_id`,
        `ip_ban_id`,
        `scpsl_ip_bans`.`id` as `scpsl_ip_bans_id`,
        `scpsl_ip_bans`.`name` as `scpsl_ip_bans_name`
    FROM
        `scpsl_ip_bans`
        LEFT OUTER JOIN `scpsl_ban_link` ON (`scpsl_ban_link`.`ip_ban_id` = `scpsl_ip_bans`.`id`)
        WHERE `user_id_ban_id` IS NULL
    ;