MySQL on redundant rows - mysql

A user has a student (one to one) and a student can have many languages and hobbies (both times many to many).
If I run this query,
SELECT email, hobbies.name, languages.name
FROM users
INNER JOIN students
ON users.id = students.user_id
LEFT OUTER JOIN languages_student
ON students.id = languages_student.student_id
INNER JOIN languages
ON languages_student.language_id = languages.id
LEFT OUTER JOIN hobbies_student
on students.id = hobbies_student.student_id
INNER JOIN hobbies
ON hobbies_student.hobbie_id = hobbies.id
WHERE users.id = 6
I get this result set:
If I add another language to a student, I get six rows in the result set. Is there a way of combining the second and third columns in order to get something more compact and not redundant? Can each hobby appear just once and get a NULL in languages when they run out?

There are a couple of approaches to being able to aggregate this information. One is to do this in your application logic based on the type of result set you currently show. This might be done be reading the rows from the result set into an appropriate data structure you can then use in your application to display this information.
The second approach is to use GROUP_CONCAT() to concatenate values within same group (in this case email name) into a single row. That might lead to a results set like this:
shields.katlynn#swaniaski.biz Endurance Sports,Golf Balochi,Assamesse
This might mean that in your application, you would need to split apart the data in each row to get to individual values.
An example of how you might write the query to get the above result would be:
SELECT
email,
GROUP_CONCAT(DISTINCT hobbies.name) AS hobbies,
GROUP_CONCAT(DISTINCT languages.name) AS languages
FROM users
INNER JOIN students
ON users.id = students.user_id
LEFT OUTER JOIN languages_student
ON students.id = languages_student.student_id
INNER JOIN languages
ON languages_student.language_id = languages.id
LEFT OUTER JOIN hobbies_student
on students.id = hobbies_student.student_id
INNER JOIN hobbies
ON hobbies_student.hobbie_id = hobbies.id
WHERE users.id = 6
GROUP BY email
In either case, you will need some sort of post-retrieval data processing in your application.

Related

Is it possible to join many to many and one to many in one go?

My tables have the following schema:
wp_careers
wp_locations
wp_careers_locations
wp_educations
A career applicant can apply to many locations, and have many educational records.
The desired result is to get ALL records from wp_careers and group the applied locations as a locations fields, and put all educational records (wp_educations) as an array attached to the applicant.
Right now I know how to join many to many relations and group the locations:
SELECT c.*, GROUP_CONCAT(l.name) as locations
FROM wp_careers c
JOIN wp_careers_locations cl ON c.id = cl.career_id
JOIN wp_locations l ON cl.location_id = l.id
GROUP BY c.id
But I don't know how to extend this query to include the educational records.
One way would be to just join again:
SELECT c.*, GROUP_CONCAT(DISTINCT l.name) as locations,
GROUP_CONCAT(DISTINCT e.institute) AS edu_institutes
FROM wp_careers c
LEFT JOIN wp_careers_locations cl ON c.id = cl.career_id
LEFT JOIN wp_locations l ON cl.location_id = l.id
LEFT JOIN wp_educations e ON c.id = e.career_id
GROUP BY c.id
But this is likely to create a Cartesian product, as it will inadvertently join every location to every education. So if you have three locations and two educations for a given career, it will generate 3x2 = 6 rows when you didn't expect it to. I tried to compensate for this with DISTINCT so the list of names in each GROUP_CONCAT() will eliminate duplicates.
But honestly, I would prefer to run two queries. One for locations, and a second query for educations. That would avoid the Cartesian product. MySQL is not so weak that it can't handle an extra query, and it might actually be less expensive than doing the DISTINCT operations.
Re your comment:
You want to restrict careers in the education query only to those that have at least one location?
You can do this with a semi-join:
SELECT c.*, GROUP_CONCAT(e.institute) AS edu_institutes
FROM wp_careers c
JOIN wp_educations e ON c.id = e.career_id
WHERE c.id IN (SELECT career_id FROM wp_career_locations)
GROUP BY c.id
Even though there may be multiple rows in wp_career_locations matching each respective c.id, it doesn't cause a Cartesian product.

MySQL getting data from 3 tables which are connected by mapping tables

I have the following database example:
The example is pretty much self-explanatory: There are lessons held by teachers at defined time periods (time_start, time_end) each time period -> lesson connection has its own max_students number.
I know want to list all lessons with all information of the 3 tables (and the max_students). I would do it like that (I heard, that joining table like that is the fastest way):
SELECT * FROM lesson, teacher, time, teacher_has_lesson, time_has_lesson
WHERE lesson.lesson_id = teacher_has_lesson.lesson_lesson_id
AND teacher.teacher_id = teacher_has_lesson.teacher_teacher_id
AND lesson.lesson_id = time_has_lesson.lesson_lesson_id
AND time.time_id = time_has_lesson.time_time_id
1.) Is this a good solution if you just want to join 3 tables or are there better ones?
2.) This SQL call will get me only lessons, that have a teacher and a time. I also want to display lessons, that are in the database, but dont have a teacher or time yet. How can I do that?
There's an alternative way of writing this using join syntax. What you have is equivalent to an inner join, where you only see rows where there are matches:
select
*
from
lesson l
inner join
teacher_has_lesson tl
on l.lession_id = tl.lesson_lesson_id
inner join
teacher t
on tl.teacher_teacher_id = t.teacher_d
inner join
time_has_lesson tml
on l.lesson_id = tml.lesson_lesson_id
inner join
time tm
on tml.time_time_id = tm.time_ud
There's another type of join called outer join, where all the rows from one table are shown, and null values supplied if there are no matching values in the other table. It comes in two or three variants. left outer join shows all rows from the first table. right outer join shows all rows from the second table. full outer join shows all rows from both tables. So, for your second query you could use:
select
*
from
lesson l
left outer join
teacher_has_lesson tl
on l.lession_id = tl.lesson_lesson_id
left outer join
teacher t
on tl.teacher_teacher_id = t.teacher_d
left outer join
time_has_lesson tml
on l.lesson_id = tml.lesson_lesson_id
left outer join
time tm
on tml.time_time_id = tm.time_ud

More Efficient Version of this Correlated Subquery

I've got 3 different tables and I need to pull data once from 2 of them, and twice from the third. The tables are jobs, customers, and customers_attributes. I'm trying to pull data for a specific job, and part of that data is information about the customer who owns the job. Customer data is stored in customers_attributes where the type of data is defined as an integer that corresponds with a type(using strings here for simplicity's sake) and then a content field contains the data itself.
In this case, I need to pull 2 rows from the customers_attributes table that correspond to the customer that corresponds to the job. One row for 'PhoneNumber', and another row for 'CustomerInfo'. I used an INNER JOIN for one of them, but because I can't put WHERE values for both, I used a subquery for the other one. I think this is really nasty and I'm sure there's got to be a cleaner way of doing it:
SELECT jobs.*, customers.Name AS CustomerName,
customers_attributes.Content AS PhoneNumber,
( SELECT `Content`
FROM customers_attributes
WHERE Type = 'CustomerInfo' AND ForeignCustomer = jobs.Customer
LIMIT 1) AS CustomerInfo
FROM jobs
INNER JOIN customers ON jobs.Customer = customers.ID
INNER JOIN customers_attributes ON jobs.Customer = customers_attributes.ForeignCustomer
WHERE jobs.ID = $jobID AND customers_attributes.Type = 'PhoneNumber'
LIMIT 1
I should mention that a customer could have multiple rows for the same attribute if they have more than 1 job, and this query ideally should either return the latest information, or the information that was submitted at the same time as the job(based on corresponding ID orders).
Just join the same table again under a different alias.
SELECT j.*, c.Name AS CustomerName,
ca.Content AS PhoneNumber,
ca2.Content as CustomerInfo
FROM jobs j
INNER JOIN customers c ON j.Customer = c.ID
INNER JOIN customers_attributes ca ON (j.Customer = ca.ForeignCustomer)
INNER JOIN customers_attributes ca2 ON (j.Customer = ca2.ForeignCustomer)
WHERE j.ID = '$jobID'
AND ca.Type = 'PhoneNumber'
AND ca2.Type = 'CustomerInfo'
LIMIT 1
Warning
It looks like you're using PHP. If you must insist on using the outdated mysql_ library and not the much improved mysqli_ lib.
Please remember to use mysql_real_escape_string() and to quote your $vars.
If not you'll be hid by SQL-injection problems.

MYSQL populate foreign id with value

I have a printed table here, and I issue a query to attempt to join the tables where the Tech_id, clients_id, job_id, part_id should populate with corresponding key in their tables / column too.
Here is my query:
SELECT * FROM work_orders, technicians as tech, parts_list as parts, job_types as job, clients as client
LEFT JOIN technicians ON tech_id = technicians.tech_name
LEFT JOIN parts_list ON part_id = parts_list.Part_Name
LEFT JOIN job_types ON job_id = job_types.Job_Name
LEFT JOIN clients ON clients_id = clients.client_name
I've messed around with multiple different variations, this one seem to be syntax correct, but now I'm getting: Column 'clients_id' in on clause is ambiguous
I'm sure that it will happen for not only clients but maybe others. I want to be able to print the table as in the picture above, but with the clients listed. Is it possible to be done via one query as well? thanks.
You have two problems.
First (this might not be your problem, but that's a "good practice"), you shouldn't use SELECT *, as you could indeed have a field with same name in different tables.
This is one (of the many) good reason to avoid * in a Select clause.
Then, your main problem is that you select tables in your from clause, and then again by joining.
Problematic line :
FROM work_orders, technicians as tech, parts_list as parts, job_types as job, clients as client
So (I don't know your table structure, so they may be errors, but you've got the idea)
SELECT
w.client_id,
t.tech_name
--etc
FROM work_orders w
LEFT JOIN technicians t ON c.tech_id = t.tech_name
LEFT JOIN parts_list p ON c.part_id = p.Part_Name
LEFT JOIN job_types j ON w.job_id = j.Job_Name
LEFT JOIN clients c ON w.clients_id = c.client_name
This means that clients_id exists in multiple tables. You need to specify which one you want. So if you for example want the clients_id of the clients table, do SELECT clients.clients_id
If all the fiels listed in your question are in the clients table you could do:
SELECT clients.* FROM work_orders, technicians as tech, parts_list as parts, job_types as job, clients as client
LEFT JOIN technicians ON tech_id = technicians.tech_name
LEFT JOIN parts_list ON part_id = parts_list.Part_Name
LEFT JOIN job_types ON job_id = job_types.Job_Name
LEFT JOIN clients ON clients_id = clients.client_name

Mysql Multiple table select with condition

I have a noob question but rather a troublesome one for me. I am using SELECT on three tables the middle one of which is realtional (Holds relations - ID of user against ID of Place), the first is a table of users, the last of places. I have written this perfectly woking query
$query = "SELECT users.Username,usrxplc.User,places.Name
FROM users,usrxplc,places
WHERE usrxplc.Place=places.ID AND usrxplc.User=users.ID"
That spits out all places associated with all users. Fine, but I would like to limit it only to a certain user. Seems simple, but I am stuck.
You use a WHERE clause to filter the results, so just add a clause for users.ID:
select users.Username,
usrxplc.User,
places.name
from users,
usrxplc,
places
where usrxplc.Place = places.ID
and usrxplc.User = users.ID
and users.ID = 123
Just felt the need to post the alternative - instead of selecting and all tables you can use INNER JOIN to join one table onto another
SELECT
users.Username,
places.Name
FROM users
INNER JOIN usrxplc ON usrxplc.User=users.ID
INNER JOIN places ON places.ID = usrxplc.Place
WHERE users.ID = 111
It's functionally the same as the other answer, however when you get onto more complex queries and tables you will find that using JOINs allows for greater optimisation as you are able to further limit the rows each individual JOIN gets, for example the following is also valid, where the User row is limited before joining onto other tables
SELECT
users.Username,
places.Name
FROM places
INNER JOIN usrxplc ON usrxplc.Place = places.ID
INNER JOIN users ON users.ID = usrxplc.User AND users.ID = 111
In more complicated queries, or if these tables were to be far larger, this would in turn offer a more optimal query generally speaking