Alternative to GROUP_CONCAT? Multiple joins to same table, different columns - mysql

I'm not even sure of the correct terminology here. MySQL newbie, more or less.
Given a couple tables defined as follows:
CREATE TABLE users
( user_id int(11) NOT NULL auto_increment
, name VARCHAR(255)
, pri_location_id mediumint(8)
, sec_location_id mediumint(8)
, PRIMARY KEY (user_id)
);
CREATE TABLE locations
( location_id mediumint(8) NOT NULL AUTO_INCREMENT
, name varchar(255)
, PRIMARY KEY (location_id)
)
I'm trying to do a query to get the user name and both primary and secondary locations in one go.
I can get one like this:
SELECT u.name AS user_name, l.name as primary_location FROM users u, locations l WHERE u.primary_location_id=l.location_id
But I'm drawing a total blank on the correct syntax to use to get both in one query.

SELECT u.name AS user_name, l1.name as primary_location , l2.name as secondary_location
FROM users u
JOIN locations l1 ON(u.pri_location_id=l1.location_id)
JOIN locations l2 ON(u.sec_location_id = l2.location_id);

First of, I would strongly consider changing your DB schema if allowable to add a users_locations table that can be used to properly describe this many to many relationship.
This table could look like:
user_id location_id location_type
1 1 primary
1 2 secondary
2 1 secondary
2 2 primary
and so forth.
You would likely want a compound primary key across all three columns. And location_type might best be enum data type.
Your query would then be like
SELECT
u.name AS user_name
l.name AS location_name
ul.location_type AS location_type
FROM users AS u
INNER JOIN user_location AS ul /* possibly use left join here if user can exist without a location */
ON u.user_id = ul.user_id
INNER JOIN locations AS l
ON ul.location_id = l.location_id
ORDER BY ul.location_type ASC
This would return up to two records per user (separate record for primary and secondary, primary listed first)
If you need this collapsed to a single record you could do this:
SELECT
u.name AS user_name
COALESCE(CASE WHEN ul.location_type = 'primary' THEN l.name ELSE NULL END CASE) AS primary_location,
COALESCE(CASE WHEN ul.location_type = 'secondary' THEN l.name ELSE NULL END CASE) AS secondary_location
FROM users AS u
INNER JOIN user_location AS ul /* possibly use left join here if user can exist without a location */
ON u.user_id = ul.user_id
INNER JOIN locations AS l
ON ul.location_id = l.location_id
GROUP BY `user_name`
If however you are stuck with current schema, then solution by #Jlil should work for you.

Related

MySQL count across multiple tables with multiple conditions

I need to generate a report of how many registered users there are based on groups with more than 'x' members (members are users and can be registered or not registered).
A real simplified version would be:
table.users
userid int(11) NOT NULL,
username VARCHAR(40),
PRIMARY KEY (userid)
table.groups
gid int(11) NOT NULL,
guserid int(11) NOT NULL,
groupid VARCHAR(12) NOT NULL,
PRIMARY KEY (gid)
And some sample data:
INSERT INTO users (userid, username) VALUES
('1','bob'),('2','steve'),('3',''),('4','jill'),
('5',''),('6',''),('7','john'),('8','stan'),
('9',''),('10','rachel'),('11','lisa');
Out of those 11 users, 7 have usernames (registered)
INSERT INTO groups (gid, guserid, groupid) VALUES
('1','1','ABC123'),('2','2','ABC123'),('3','3','XYZ789'),('4','4','ABC123'),
('5','5','XYZ789'),('6','6','ABC123'),('7','7','DEF456'),('8','8','ABC123'),
('9','9','DEF456'),('10','10','XYZ789'),('11','11','XYZ789');
I need to get the groupid, the count of that groupid in the groups table, and then the count of the users registered for that group (username is not null).
'ABC123','5','4'
'XYZ789','4','2'
'DEF456','2','1'
The real data is a much larger subset and I need to get only results where we have a possible number of users in groups of more than 500 (which is around 1,000 groups which have anywhere from 500 to 25000 possible members). Everything I've tried involves nested selects and I can get close, but not the exact data returned that I need.
You just need to LEFT JOIN to the users table and COUNT:
SELECT groupid, COUNT(*), COUNT(DISTINCT u.username)
FROM groups AS g
LEFT JOIN users AS u ON u.userid = g.guserid AND u.username <> ''
GROUP BY groupid
Demo here
Something along the lines of:
SELECT groups.gid,
COUNT(groups.gid),
SUM(CASE WHEN users.username='' THEN 1 ELSE 0 END)
FROM groups
JOIN users ON users.userid=groups.guserid
GROUP BY groups.gid
Untested.

Referencing Two Tables in SQL DB

I'm new to SQL, how could I answer the following question? Do I use join?
Thank you so much.
What are the names and home cities for people searched for the word "drain"?
.schema is below:
CREATE TABLE `users` (
`id` INTEGER,
`name` VARCHAR,
`email` VARCHAR,
`city` VARCHAR,
`state` VARCHAR,
`last_visit` DATE,
`page_views` INTEGER,
PRIMARY KEY (`id`)
);
CREATE TABLE `search_terms` (
`id` INTEGER,
`word` VARCHAR,
PRIMARY KEY (`id`)
);
select u.name, u.city
from users u
inner join search_terms st on st.id = u.id
where st.word = 'drain'
I hope this helps.
You do need to join search_terms table with users table. I assume the search_terms.id is referencing to users.id.
Linking them both, will give us the results of the search terms for each user.
from there you can add more filters (WHERE conditions) to have more specific results.
So, to get the names and the home cities for each user you need to select name and city from users table, then join search_terms table and link id columns to know what words that each user has used in the search.
The query should be something like this :
SELECT name, city
FROM users
LEFT JOIN search_terms ON search_terms.id = users.id
WHERE
search_terms.word LIKE 'drain';
You could use a JOIN or a sub-query. Here's the basic strategy:
Get the id values that match what you're looking for
Look up any other info for those id values
.
SELECT name, city
FROM users usr
INNER JOIN search_terms stm ON usr.id = stm.id
WHERE stm.word = 'drain';
or ...
SELECT name, city
FROM users
WHERE id IN (SELECT id FROM search_terms WHERE word = 'drain');

Remove results from SELECT by conditions on multiple columns

So I have this feed of products
id man sup product
1 1 1 MacBook
2 1 2 iMac
3 2 1 Windows
4 2 2 Office
and then tables with manufacturers
id manufacturer
1 Apple
2 Microsoft
and suppliers
id supplier
1 TechData
2 Westcoast
Then, for some reasons, I don't want to show a manufacturer's products by a certain supplier, i.e.:
id man sup comment
1 2 1 TechData aren't allowed to sell Microsoft
2 1 2 hide all Apple products from Westcoast
Is there a way, in pure SQL, to show only the products which fall through my filter, in this case MacBook and Office? I believe this isn's just a WHERE NOT (x AND y) as the result will list the remaining combinations.
Thanks a lot!
This is just a variation on Return row only if value doesn't exist, except you're joining on two columns.
SELECT p.product, m.manufacturer, s.supplier
FROM products AS p
JOIN manufacturers AS m ON m.id = p.man
JOIN suppliers AS s ON s.id = p.sup
LEFT JOIN filter AS f ON p.man = f.man AND p.sup = f.sup
WHERE f.id IS NULL
DEMO
You can try this, mate:
First, create the temporary/real container for your custom manufacturer-supplier filter:
-- pivot temp/real table for suppliers - manufacturers
DROP TEMPORARY TABLE IF EXISTS `manufacturers_suppliers`;
CREATE TEMPORARY TABLE `manufacturers_suppliers` (
`id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
`manufacturer_id` INT(11) UNSIGNED,
`supplier_id` INT(11) UNSIGNED,
PRIMARY KEY (`id`),
UNIQUE KEY (`manufacturer_id` ASC, `supplier_id` ASC)
);
-- populate pivot table
INSERT INTO `manufacturers_suppliers` (`manufacturer_id`, `supplier_id`)
VALUES
-- TechData aren't allowed to sell Microsoft
(2, 1),
-- hide all Apple products from Westcoast
(1, 2);
After that, using the content of the container, you only need to create a standard query for your result set.
-- create result
SELECT
p.id, p.product, # show product detail
s.id, s.supplier, # show supplier detail
m.id, m.manufacturer # show manufacturer detail
FROM
products p
INNER JOIN suppliers s ON s.id = p.sup
INNER JOIN manufacturers m ON m.id = p.man
LEFT JOIN `manufacturers_suppliers` ms ON
ms.manufacturer_id = man.id
AND ms.supplier_id = sup.id
WHERE ms.id IS NULL;
So that every time you have an update to your filter, you only update records not the actual query script. Cheers

MySQL - Select data from relational tables A, B, C, D, E if record was not found on Table F

I have 6 tables in my database: users, courses, users_courses, lectures, users_lectures and attends. In the headline, I referred to them as A, B, C, D, E and F.
I'm creating an attending system for lecture as a part of my school project and I need to run a query that selects only those lectures where users haven't yet attended.
users table holds information about user. Its' table structure is following:
id INT(10) USINGNED PRIMARY KEY AUTO_INCREMENT
name VARCHAR(255)
email VARCHAR(255)
password VARCHAR(60)
created_at TIMESTAMP
updated_at TIMESTAMP
courses table is a parent table for lectures that holds information about courses where lectures belongs to. Its' table structure is following:
id INT(10) UNSIGNED AUTO_INCREMENT
name VARCHAR(255)
created_at TIMESTAMP
updated_at TIMESTAMP
users_courses is a relationship table between users and courses. It links users to courses. Its' table structure is following:
user_id INT(10) UNSIGNED FOREIGN KEY
course_id INT(10) UNSIGNED FOREIGN KEY
lectures table holds information about upcoming lectures users are supposed to attend. It's table structure is following:
id INT(10)
starting_at DATETIME
ending_at DATETIME
course_id INT(10) UNSIGNED FOREIGN KEY
created_at TIMESTAMP
updated_at TIMESTAMP
users_lectures table is a relationship table between users and lectures. It links lectures to users. Its' table structure is following:
user_id INT(10) UNSIGNED FOREIGN KEY
lecture_id INT(10) UNSIGNED FOREIGN KEY
attends table holds information about attending user's user's id, lecture's id and timestamps for created_at and updated_at. Its' table structure is following:
user_id INT(10) UNSIGNED FOREIGN KEY
lecture_id INT(10) UNSIGNED FOREIGN KEY
created_at TIMESTAMP
updated_at TIMESTAMP
Question:
Is it possible, with one query, to select only those upcoming lectures where users haven't yet attended?
Here's what I've tried, but it doesn't return any rows:
// PHP
$stmt = $pdo->prepare("
SELECT
courses.name AS course,
lectures.id AS id,
lectures.starting_at,
lectures.ending_at,
users.name AS user_name,
users_lectures.user_id
FROM lectures
LEFT JOIN users_courses ON users_courses.course_id = lectures.course_id
LEFT JOIN courses ON courses.id = users_courses.course_id
LEFT JOIN users_lectures ON users_lectures.lecture_id = lectures.id
LEFT JOIN users ON users.id = users_lectures.user_id
LEFT JOIN attends ON attends.lecture_id = users_lectures.lecture_id
WHERE users_lectures.user_id = :user_id
AND lectures.ending_at > DATE_SUB(CURRENT_TIMESTAMP, INTERVAL 1 MINUTE)
AND lectures.ending_at > DATE_ADD(CURRENT_TIMESTAMP, INTERVAL 1 MINUTE)
AND attends.lecture_id != users_lectures.lecture_id
AND attends.user_id != :user_id
ORDER BY lectures.starting_at DESC");
Am I missing something or thinking the whole structure in a wrong way?
Sorry for providing such a long text, but I wanted to be as specific as possible. Any help would be much appreciated :)
First of all you need to start with the subject of what you want to return. That would be users. So this will be your first table to select from (Note here: use AS to rename tables for brevity):
Select *
From users AS u
Next link together the data you want. Depending on how specific you want to be will depend on how many joins you will need to make (e.g. courseID vs CourseName).
Now assuming we want lots of human readable data such as names we will link the following tables.
LEFT OUTER JOIN users_lectures AS ul ON u.id = ul.userid
LEFT OUTER JOIN lectures AS l ON l.id = ul.lectureid
LEFT OUTER JOIN courses AS c ON c.id = l.courseid
LEFT OUTER JOIN attends AS a ON a.userid = u.id AND a.lectureid = l.id
And to top it off, to find people who didn't attend, they will not be in the attends table so we just check for people where the values are null for attends.
WHERE a.userid IS NULL
As a side note, you won't get any results with this in the WHERE clause
AND attends.lecture_id != users_lectures.lecture_id
Because you are joining with this statement
LEFT JOIN attends ON attends.lecture_id = users_lectures.lecture_id
Which contradict each other

SQL statement that shows at least 16 courses not taken

I have four schemas:
takes(ID,course_id,sec_id,semester,year)
student(ID,name,dept_name,tot_credit)
course(course_id,title,dept_name,credits)
department(dept_name,building,budget)
I want to create a query that finds the name and id of each Astronomy student whose name begins with the letter ’T’ and who has not taken at least 16 Astronomy courses.
What's the easiest way I could do this?
I already wrote this beginning bit
SELECT name, id
FROM student
WHERE dept_name='Astronomy' AND name LIKE '%T%'
I'm not quite sure how to finish this off.
Any help would be greatly appreciated :)
Here's the result
NAME ID CLASS_TAKEN
-------------------- ----- -----------
Tolle 38279 12
Teo 62268 13
Tolle 93223 13
Tsukamoto 17707 5
Titi 11576 9
Teo 91772 12
Toraichi 50387 11
Tewari 80754 14
Tiroz 64091 14
9 rows selected
I need Teo with the id 91772 and Tewari 80754 to be gone
Given my reading of the requirements and the comments it's pretty clear that the question is not very clear. :-) What you're looking for are students where (total # of courses given by the Astronomy department) - (# of Astronomy courses taken by student) >= 16. So, how do we find these values? First, let's start with the total number of courses given by the Astronomy department. This is pretty simple:
SELECT COUNT(*) AS ASTRONOMY_COURSE_COUNT
FROM COURSE
WHERE DEPT_NAME = 'ASTRONOMY'
Now, the second part is to determine how many courses given by the Astronomy department each student has taken. To do this we need to start with the student, join to the courses the student has taken (the TAKES table), then join to the COURSES table to find out which department each course is part of. Something like the following should do it:
SELECT s.ID, s.NAME, COUNT(*) AS STUDENT_ASTRO_COUNT
FROM STUDENT s
INNER JOIN TAKES t
ON t.ID = s.ID
INNER JOIN COURSE c
ON c.COURSE_ID = t.COURSE_ID
WHERE c.DEPT_NAME = 'ASTRONOMY' AND
s.NAME LIKE 'T%'
GROUP BY s.ID, s.NAME;
OK, now we need to put this together. You've tagged this question for both Oracle and MySQL so I'm going to guess you'll accept valid syntax for either database; thus I'll use Oracle Common Table Expression syntax to pull everything together:
WITH ASTRONOMY_COURSES AS (SELECT COUNT(*) AS ASTRONOMY_COURSE_COUNT
FROM COURSE
WHERE DEPT_NAME = 'ASTRONOMY'),
STUDENT_ASTRO_COURSES AS (SELECT s.ID,
s.NAME,
COUNT(*) AS STUDENT_ASTRO_COUNT
FROM STUDENT s
INNER JOIN TAKES t
ON t.ID = s.ID
INNER JOIN COURSE c
ON c.COURSE_ID = t.COURSE_ID
WHERE c.DEPT_NAME = 'ASTRONOMY' AND
s.NAME LIKE 'T%'
GROUP BY ID)
SELECT s.ID,
s.NAME,
s.STUDENT_ASTRO_COUNT,
a.ASTRONOMY_COURSE_COUNT - s.STUDENT_ASTRO_COUNT AS UNTAKEN_COUNT
FROM STUDENT_ASTRO_COURSES s
CROSS JOIN ASTRONOMY_COURSES a
WHERE a.ASTRONOMY_COURSE_COUNT - s.STUDENT_ASTRO_COUNT >= 16;
Note here that a CROSS JOIN is used to put together the subqueries. This means that all the rows of each subquery are joined to all the rows of the other subquery - but since in this case the ASTRONOMY_COURSES subquery will only return a single row what we're doing is appending the ASTRONOMY_COURSE_COUNT value onto each row returned by the STUDENT_ASTRO_COURSES subquery.
That should at least get you pretty close. Amend as needed.
Not tested on animals - you'll be first! :-)
Share and enjoy.
Do you need to use all tables?
Table department has no links with the student,
Table takes has no links with the student,
Table coursehas no links with the student.
If student lists total credits that are all Astronomy I think this can be used:
select name, id, MAX(tot_credit) as credits
from student
where dept_name='Astronomy' and name like 'T%'
group by name, id
having MAX(tot_credit)<=16
PS - you schema is not good; PK-FK references are missing
Your query will need to reference more tables than just the student table.
Your tables seem be missing some important information, which student has taken which course. There's a table named takes, but there doesn't appear to be any relationship between takes and student.
So first, figure out how to list the students along with the Astronomy courses they have taken. Each row will identify the student and a course.
SELECT s.id AS student_id
, s.name AS student_name
, t.???
FROM student s
JOIN ??? t
ON t.student_id = s.id
WHERE ...
You may also need to include another "join" to an additional table, in order to identify which course a student has taken is an Astronomy course.
To also include students that have not take any Astronomy courses, you can use an outer join, rather than an inner join. (That would mean including the LEFT keyword before JOIN, and relocating predicates from the WHERE clause to the ON clause. (A predicate in the WHERE clause that can only be satisfied by non-NULL values will negate the outer-ness of the join.)
Once you have a query that returns that set (students along with any astronomy courses they've taken), you can then add a GROUP BY clause to "collapse" a set of rows into a single row. (Looks like you want the rows "grouped" by student.)
And then an aggregate function like COUNT() or SUM() can be used to get a count of rows for each group. (If you don't want to count any re-takes of a course (a "duplicate" course for a student) you may be able to make use of the COUNT(DISTINCT t.foo) form.
And then a HAVING clause can be added to the query, to compare the value returned from the aggregate expression to a constant value, to return only rows that satisfy a specific condition.
FOLLOWUP
Assuming:
CREATE TABLE course
( id INT UNSIGNED NOT NULL COMMENT 'PK'
, title VARCHAR(30) NOT NULL COMMENT 'course title'
, dept_name VARCHAR(30) NOT NULL COMMENT 'FK ref dept.name'
, credits DECIMAL(5,2) COMMENT 'credit hours'
, PRIMARY KEY (id)
);
CREATE TABLE student
( id INT UNSIGNED NOT NULL COMMENT 'PK'
, name VARCHAR(30) NOT NULL COMMENT 'student name'
, dept_name VARCHAR(30) NOT NULL COMMENT 'FK ref dept.name'
, tot_credit INT COMMENT '?'
, PRIMARY KEY (id)
);
CREATE TABLE takes
( student_id INT UNSIGNED NOT NULL COMMENT 'FK ref student.id'
, course_id INT UNSIGNED NOT NULL COMMENT 'FK ref course.id'
, sec_id INT UNSIGNED NOT NULL COMMENT '?'
, semester INT UNSIGNED NOT NULL COMMENT '?'
, year INT UNSIGNED NOT NULL COMMENT '?'
, PRIMARY KEY (student_id, course_id, sec_id, semester, year)
, CONSTRAINT FK_takes_course FOREIGN KEY (course_id) REFERENCES course (id)
, CONSTRAINT FK_takes_student FOREIGN KEY (student_id) REFERENCES student (id)
);
Query to get a list of students...
SELECT s.id
, s.name
FROM student s
WHERE s.name LIKE 'T%'
AND s.dept_name = 'ASTRONOMY'
Get list of students along with the courses they've taken, returning the id of the ASTRONOMY courses they've taken...
SELECT s.id AS student_id
, s.name AS student_name
, c.id AS course_id
FROM student s
LEFT
JOIN takes t
ON t.student_id = t.id
LEFT
JOIN course c
ON c.id = t.course_id
AND c.dept_name = 'ASTRONOMY'
WHERE s.name LIKE 'T%'
AND s.dept_name = 'ASTRONOMY'
Collapse the rows to one per student using a GROUP BY, and use aggregate functions to get counts or totals.
SELECT s.id AS student_id
, s.name AS student_name
, SUM(c.credits) AS total_astronomy_credits_taken
, COUNT(c.id) AS count_astronomy_courses_taken
, COUNT(DISTINCT c.id) AS count_distinct_astronomy_courses_taken
FROM student s
LEFT
JOIN takes t
ON t.student_id = t.id
LEFT
JOIN course c
ON c.id = t.course_id
AND c.dept_name = 'ASTRONOMY'
WHERE s.name LIKE 'T%'
AND s.dept_name = 'ASTRONOMY'
GROUP
BY s.id
, s.name
To omit rows from this resultset, add a HAVING clause. For example, to exclude rows where total_astronomy_credits_taken is greater than or equal to 12...
HAVING total_astronomy_credits_taken >= 12
If you want the rows returned in a certain sequence, specify that in an ORDER BY clause
ORDER BY s.id
If you want to replace NULL values from the aggregates with zeroes, you can warp the aggregate expression in an IFNULL(foo,0) function, e.g.
, IFNULL(COUNT(c.id),0) AS count_astronomy_courses_taken
Try this :
select a.name, a.id, count(b.ID) as class_taken
from student a inner join takes b
on a.ID = b.ID
inner join course c
on b.course_id = c.course_id
where a.dept_name='Astronomy' and substring(a.name,1,1) = 'T'
group by a.name, a.id
having count(b.ID) < 17