Help with querying a many to many table in mysql - mysql

I am trying to get a list of product id's that do not have certain colors (database is mysql)
Here are my tables:
product
+------------+-------------+
| product_id | description |
+------------+-------------+
| 1 | Widget 1 |
| 2 | Widget 2 |
| 3 | Widget 3 |
| 4 | Widget 4 |
| 5 | Widget 5 |
| 6 | Widget 6 |
+------------+-------------+
color
+----------+-------+
| color_id | name |
+----------+-------+
| 1 | red |
| 2 | black |
| 3 | white |
| 4 | green |
| 5 | blue |
| 6 | pink |
+----------+-------+
product_color
+------------+----------+
| product_id | color_id |
+------------+----------+
| 1 | 1 |
| 1 | 4 |
| 1 | 5 |
| 2 | 2 |
| 2 | 3 |
| 3 | 1 |
| 3 | 2 |
| 3 | 3 |
| 5 | 3 |
| 5 | 6 |
| 6 | 1 |
| 6 | 5 |
| 6 | 6 |
+------------+----------+
I want to select all the products that don't have colors 4 (green), 5 (blue), or 6 (pink).
So from the tables above, products 2, 3, and 4 would appear in the result set.
The best I've been able to do is:
SELECT product.*, GROUP_CONCAT(product_color.color_id) as color_ids
FROM product
LEFT JOIN product_color USING (product_id)
GROUP BY product.product_id
Here is my result set:
--------------------------------------
product_id description color_ids
--------------------------------------
1 Widget 1 1,4,5
2 Widget 2 2,3
3 Widget 3 1,2,3
4 Widget 4 NULL
5 Widget 5 3,6
6 Widget 6 1,3,6
And then I filter them programmatically but I'd prefer for the database to do all the work, if it can.
Optimally, I'd like my result set to look like this (all I need is the product id's).
------------
product_id
------------
2
3
4
Here is the schema and data just case your kind soul would like to help me out.
CREATE TABLE IF NOT EXISTS `color` (
`color_id` int(10) unsigned NOT NULL auto_increment,
`name` varchar(25) NOT NULL,
PRIMARY KEY (`color_id`),
UNIQUE KEY `name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=7 ;
INSERT INTO `color` (`color_id`, `name`) VALUES
(2, 'black'),
(5, 'blue'),
(4, 'green'),
(6, 'pink'),
(1, 'red'),
(3, 'white');
CREATE TABLE IF NOT EXISTS `product` (
`product_id` int(10) unsigned NOT NULL auto_increment,
`description` varchar(25) NOT NULL,
PRIMARY KEY (`product_id`),
UNIQUE KEY `description` (`description`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=7 ;
INSERT INTO `product` (`product_id`, `description`) VALUES
(1, 'Widget 1'),
(2, 'Widget 2'),
(3, 'Widget 3'),
(4, 'Widget 4'),
(5, 'Widget 5'),
(6, 'Widget 6');
CREATE TABLE IF NOT EXISTS `product_color` (
`product_id` int(10) unsigned NOT NULL,
`color_id` int(10) unsigned NOT NULL,
PRIMARY KEY (`product_id`,`color_id`),
KEY `color_id` (`color_id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
INSERT INTO `product_color` (`product_id`, `color_id`) VALUES
(1, 1),
(3, 1),
(6, 1),
(2, 2),
(3, 2),
(2, 3),
(3, 3),
(5, 3),
(1, 4),
(1, 5),
(6, 5),
(5, 6),
(6, 6);

I think a sub query in the where clause should be sufficient.
SELECT DISTINCT product_id
FROM product
WHERE product_id NOT IN (SELECT product_id FROM product_color WHERE color_id IN (4, 5, 6));
EDIT: I just saw the many to many bit! This works.

Related

Place the result of a join in a sub-object rather than "flat" in the result

I have a table people, a table departments and a table addresses.
I am wondering if there is a way in mySql to have some of the results as JSON rather than added rows or tables.
For this example:
people:
+--------------+-------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+--------------+-------------+------+-----+---------+-------+
| id | int | YES | | NULL | |
| name | varchar(50) | YES | | NULL | |
| departmentId | int | YES | | NULL | |
+--------------+-------------+------+-----+---------+-------+
addresses
+-------------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------------+--------------+------+-----+---------+-------+
| id | int | YES | | NULL | |
| personId | int | YES | | NULL | |
| fullAddress | varchar(125) | YES | | NULL | |
+-------------+--------------+------+-----+---------+-------+
departments;
+-------+-------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------+-------------+------+-----+---------+-------+
| id | int | YES | | NULL | |
| name | varchar(50) | YES | | NULL | |
+-------+-------------+------+-----+---------+-------+
Note: to create tables, run:
create table people ( id INT(11), name varchar(50), departmentId INT(11));
create table departments ( id INT(11), name varchar(50));
create table addresses ( id INT(11), personId INT(11), fullAddress varchar(125));
So a person can be linked to one specific department, but can have several addresses.
insert into departments values (0, 'sales');
insert into departments values (1, 'dev');
insert into people values (0, 'Chiara', 0);
insert into people values (1, 'Geoff', 0);
insert into people values (2, 'Tony', 1);
insert into people values (3, 'Raphael', 1);
insert into addresses values (0, 0, 'Street 0');
insert into addresses values (1, 0, 'Street 1');
insert into addresses values (2, 1, 'Street 2');
insert into addresses values (3, 1, 'Street 3');
insert into addresses values (4, 2, 'Street 4');
insert into addresses values (5, 2, 'Street 5');
insert into addresses values (6, 2, 'Street 6');
insert into addresses values (7, 2, 'Street 7');
insert into addresses values (8, 3, 'Street 8');
insert into addresses values (9, 3, 'Street 9');
QUESTION 1: JOIN 1:n as array rather than as multiple results
I can easily do a select and get all records in people:
select * from people left join addresses on people.id = addresses.personId where people.id = 2;
The result of this is:
+------+------+--------------+------+----------+-------------+
| id | name | departmentId | id | personId | fullAddress |
+------+------+--------------+------+----------+-------------+
| 2 | Tony | 1 | 7 | 2 | Street 7 |
| 2 | Tony | 1 | 6 | 2 | Street 6 |
| 2 | Tony | 1 | 5 | 2 | Street 5 |
| 2 | Tony | 1 | 4 | 2 | Street 4 |
+------+------+--------------+------+----------+-------------+
Is there an easy, straightforward way to have only ONE record as result (the contact ID 2) and have the addresses in an ARRAY (or, a JSON containing the addresses) instead?
QUESTION 2: JOIN 1:1 as a sub-object rather than adding columns
I can easily get the department information by using joins:
select * from people left join departments on departments.id = people.departmentId where people.id = 2 ;
Result:
+------+------+--------------+------+------+
| id | name | departmentId | id | name |
+------+------+--------------+------+------+
| 2 | Tony | 1 | 1 | dev |
+------+------+--------------+------+------+
However, what if I wanted a result like this instead:
+------+------+--------------+----------------------+
| id | name | departmentId | departmentIdRecord |
+------+------+--------------+----------------------+
| 2 | Tony | 1 | { id: 1, name: "dev"}|
+------+------+--------------+----------------------+
Basically having the departmentIdRecord column as a JSON containing the department record.
Is this even possible with MySql?

MySQL INSERT INTO ON DUPLICATE KEY UPDATE non sequential id

I have this table:
CREATE TABLE `test` (`id` INT(11) NOT NULL AUTO_INCREMENT , `name` VARCHAR(100) NOT NULL , PRIMARY KEY (`id`)) ENGINE = InnoDB;
| id | name |
| -- | ---- |
In which I insert the following two rows:
INSERT INTO `test`(`name`) VALUES ('Oscar'), ('Alba')
| id | name |
| -- | ----- |
| 1 | Oscar |
| 2 | Alba |
Now, I want to update the existing rows and create new ones:
INSERT INTO `test` (`id`, `name`) VALUES (1, 'Oscar'), (2, 'Nadia'), ('', 'Pedro') ON DUPLICATE KEY UPDATE `id` = VALUES (`id`), `name` = VALUES (`name`)
| id | name |
| -- | ----- |
| 1 | Oscar |
| 2 | Nadia |
| 3 | Pedro |
If I update existing rows again and create new ones, this is what happens:
INSERT INTO `test` (`id`, `name`) VALUES (1, 'Oscar'), (2, 'Nadia'), (3, 'Lucas'), ('', 'Maria'), ('', 'Sergio') ON DUPLICATE KEY UPDATE `id` = VALUES (`id`), `name` = VALUES (`name`)
| id | name |
| -- | ------ |
| 1 | Oscar |
| 2 | Nadia |
| 3 | Lucas |
| 6 | Maria |
| 7 | Sergio |
There is a jump in the auto increment id. I don't know why this happens. I would like to know if there is any way to avoid this.

MYSQL: Conditional counting of course enrolments with joined 1:N course categories?

I've spent way too long thinking about the title of this question, so I hope the one I chose is kinda understandable. But let me explain:
CREATE TABLE IF NOT EXISTS `course` (
`course_id` int(11) NOT NULL,
`course_name` varchar(255) NOT NULL
) ENGINE=MyISAM AUTO_INCREMENT=9 DEFAULT CHARSET=utf8;
INSERT INTO `course` (`course_id`, `course_name`) VALUES
(1, 'Course A'),
(2, 'Course B'),
(3, 'Course C'),
(4, 'Course D'),
(5, 'Course E');
CREATE TABLE IF NOT EXISTS `course_category` (
`course_id` int(11) NOT NULL,
`category_id` int(11) NOT NULL
) ENGINE=MyISAM AUTO_INCREMENT=11 DEFAULT CHARSET=utf8;
INSERT INTO `course_category` (`course_id`, `category_id`) VALUES
(1, 1),
(1, 2),
(1, 3),
(2, 2),
(3, 1),
(3, 3),
(5, 1),
(5, 3);
CREATE TABLE IF NOT EXISTS `enrolment` (
`enrolment_id` int(11) NOT NULL,
`course_id` int(11) NOT NULL,
`status` int(11) NOT NULL
) ENGINE=MyISAM AUTO_INCREMENT=11 DEFAULT CHARSET=utf8;
INSERT INTO `enrolment` (`enrolment_id`, `course_id`, `status`) VALUES
(1, 1, 0),
(2, 1, 1),
(3, 1, 0),
(4, 3, 0),
(5, 3, 0),
(6, 3, 0),
(7, 4, 1),
(8, 4, 1),
(9, 4, 1),
(10, 4, 0);
ALTER TABLE `course`
ADD PRIMARY KEY (`course_id`);
ALTER TABLE `enrolment`
ADD PRIMARY KEY (`enrolment_id`);
ALTER TABLE `course`
MODIFY `course_id` int(11) NOT NULL AUTO_INCREMENT,AUTO_INCREMENT=9;
ALTER TABLE `enrolment`
MODIFY `enrolment_id` int(11) NOT NULL AUTO_INCREMENT,AUTO_INCREMENT=11;
Fiddle: https://www.db-fiddle.com/f/dc3bgfMS5jTxE7wmp3SJ/0
# Table `course`
+-----------+-------------+
| course_id | course_name |
+-----------+-------------+
| 1 | Course A |
| 2 | Course B |
| 3 | Course C |
| 4 | Course D |
| 5 | Course E |
+-----------+-------------+
# Table `course_category`
+-----------+-------------+
| course_id | category_id |
+-----------+-------------+
| 1 | 1 |
| 1 | 2 |
| 1 | 3 |
| 2 | 2 |
| 3 | 1 |
| 3 | 3 |
| 5 | 1 |
| 5 | 3 |
+-----------+-------------+
# Table `enrolment`
+--------------+-----------+--------+
| enrolment_id | course_id | status |
+--------------+-----------+--------+
| 1 | 1 | 0 |
| 2 | 1 | 1 |
| 3 | 1 | 0 |
| 4 | 3 | 0 |
| 5 | 3 | 0 |
| 6 | 3 | 0 |
| 7 | 4 | 1 |
| 8 | 4 | 1 |
| 9 | 4 | 1 |
| 10 | 4 | 0 |
+--------------+-----------+--------+
(There's also a category table, but that's not important for the question.)
I am trying to get the following result:
All courses have to be in the result set
The category IDs of each course have to be a single string, separated by commas
The total number of enrolments whose status is 1 should be counted per course (Bear in mind that "1" is only an example here, it could also be another number or a string to be checked against)
+-------------+--------------+------------+
| course_name | category_ids | enrolments |
+-------------+--------------+------------+
| Course A | 3,1,2 | 1 |
| Course B | 2 | 0 |
| Course C | 1,3 | 0 |
| Course D | NULL | 3 |
| Course E | 1,3 | 0 |
+-------------+--------------+------------+
I have tried using SUM with a condition and COUNT with the DISTINCT keyword and a WHERE condition, but I can't get to have all the necessary points fulfilled.
Experiment with COUNT:
SELECT
course_name,
GROUP_CONCAT(DISTINCT category_id) AS category_ids,
COUNT(DISTINCT enrolment_id) as enrolments
FROM
course
LEFT JOIN course_category USING (course_id)
LEFT JOIN enrolment USING (course_id)
WHERE status = 1 OR status IS NULL
GROUP BY course_id
+-------------+--------------+------------+
| course_name | category_ids | enrolments |
+-------------+--------------+------------+
| Course A | 3,1,2 | 1 |
| Course D | NULL | 3 |
+-------------+--------------+------------+
In this case I only get the course if at least one enrolment with status = 1 or no enrolment for the course exists, otherwise the whole course is missing in the result.
Experiment with SUM:
SELECT
course_name,
GROUP_CONCAT(DISTINCT category_id) AS category_ids,
SUM(IF(status=1,1,0)) as enrolments
FROM
course
LEFT JOIN course_category USING (course_id)
LEFT JOIN enrolment USING (course_id)
GROUP BY course_id
+-------------+--------------+------------+
| course_name | category_ids | enrolments |
+-------------+--------------+------------+
| Course A | 3,1,2 | 3 |
| Course B | 2 | 0 |
| Course C | 1,3 | 0 |
| Course D | NULL | 3 |
| Course E | 1,3 | 0 |
+-------------+--------------+------------+
In this case all the courses are there, but the counting is wrong, since I am not able to count distinct enrolments.
Which apporach am I missing?
SELECT c.course_name,
GROUP_CONCAT(cc.category_id ORDER BY cc.category_id) category_ids,
COALESCE(e.status, 0) enrolments
FROM course c
LEFT JOIN course_category cc USING (course_id)
-- calculate statuses per course separately
LEFT JOIN (SELECT course_id, SUM(status) status -- maybe SUM(status=1)?
FROM enrolment
GROUP BY course_id ) e USING (course_id)
GROUP BY c.course_name, e.status
fiddle
I think this would be simpler expressed with correlated subqueries rather than outer aggregation:
select c.course_name,
(
select group_concat(cc.category_id order by cc.category_id)
from course_category cc
where cc.course_id = c.course_id
) category_ids,
(
select coalesce(sum(e.status), 0)
from enrolment e
where e.course_id = c.course_id
) enrolments
from course c
order by c.course_name

How to set UNIQUE constraint to multiple columns MySQL?

I have a table
table location_category {
id,
location_id,
category_id,
is_primary
}
What I want is to set a UNIQUE constraint for the combination location_id and is_primary.
I get that using this will make a multi column UNIQUE constraint
ALTER TABLE `votes` ADD UNIQUE `unique_index`(`location_id`, `is_primary`);
But my concern is that we can have multiple categories for a location but only set 1 category as primary.
For example:
| id | location_id | category_id | is_primary |
| 1 | 1 | 1 | 0 |
| 2 | 1 | 2 | 0 |
| 3 | 1 | 3 | 1 |
| 4 | 1 | 4 | 0 |
Will this violate the UNIQUE contraint? Since I have multiple instances of location_id = 1 and is_primary = 0?
Just trying to figure this out. Thank you for helping.
There is no need to change anything, UNIQUE allows multiple NULL values
CREATE TABLE `votes` (
`id` INTEGER,
`location_id` INTEGER,
`category_id` INTEGER,
`is_primary` INTEGER
);
ALTER TABLE `votes` ADD UNIQUE `unique_index`(`location_id`, `is_primary`);
INSERT INTO `votes`
(`id`, `location_id`, `category_id`, `is_primary`)
VALUES
('1', '1', '1', NULL),
('2', '1', '2', NULL),
('3', '1', '3', '1'),
('4', '1', '4', NULL);
SELECT * from `votes`
id | location_id | category_id | is_primary
-: | ----------: | ----------: | ---------:
1 | 1 | 1 | null
2 | 1 | 2 | null
3 | 1 | 3 | 1
4 | 1 | 4 | null
db<>fiddle here
So you can only have one location with is primary 1

MySQL - Count logged-in team members [closed]

Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed 8 years ago.
Improve this question
I have the following tables:
CREATE TABLE members (
member_id int(1) unsigned NOT NULL AUTO_INCREMENT,
logged_in tinyint(1) NOT NULL,
PRIMARY KEY (member_id),
UNIQUE KEY member_id (member_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO members (member_id, logged_in) VALUES
(1, 0),
(2, 1),
(3, 1),
(4, 1),
(5, 0);
+-----------------------+
| members |
+-----------+-----------+
| member_id | logged_in |
+-----------+-----------+
| 1 | 0 |
| 2 | 1 |
| 3 | 1 |
| 4 | 1 |
| 5 | 0 |
+-----------+-----------+
CREATE TABLE team_members (
team_id int(1) unsigned NOT NULL,
member_id int(1) unsigned NOT NULL,
PRIMARY KEY (team_id,member_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO team_members (team_id, member_id) VALUES
(1, 1),
(1, 2),
(2, 3),
(2, 4),
(3, 5);
+---------------------+
| team_members |
+---------+-----------+
| team_id | member_id |
+---------+-----------+
| 1 | 1 |
| 1 | 2 |
| 2 | 3 |
| 2 | 4 |
| 3 | 5 |
+---------+-----------+
And this is the output I need to achieve:
+---------+-----------------------+
| team_id | logged_in_users_count |
+---------+-----------------------+
| 1 | 1 |
| 2 | 2 |
| 3 | 0 |
+---------+-----------------------+
I am trying the following sql query but it is not returning the expected output:
SELECT
tm.team_id,
(SELECT COUNT(m.logged_in) FROM members m WHERE m.logged_in = 1) AS logged_in_users_count
FROM
members m
INNER JOIN team_members tm ON (m.member_id = tm.member_id)
GROUP BY
tm.team_id
SELECT
team_members.team_id,
COUNT(members.logged_in)
FROM
team_members LEFT JOIN members
ON (team_members.member_id = members.member_id AND members.logged_in = 1)
GROUP BY
team_members.team_id;