Get rows with only one reference in many-to-many relationship - mysql

I have three MySQL tables with many-to-many relationship.
One table is for tasks, another is for users, and the third is for assigning tasks to users (each task can be assigned to several users).
users: tasks: assigned:
+----+------+ +----+-------+ +---------+---------+
| id | name | | id | title | | task_id | user_id |
+----+------+ +----+-------+ +---------+---------+
| 1 | John | | 1 | One | | 1 | 1 |
+----+------+ +----+-------+ +---------+---------+
| 2 | Mike | | 2 | Two | | 1 | 2 |
+----+------+ +----+-------+ +---------+---------+
| 2 | 1 |
+---------+---------+
How do I get tasks assigned to only one user?
How do I get tasks assigned to only one particular user?

You need to join all tables together. Use the following to show all tasks assigned to John:
SELECT name,title
FROM users
JOIN assigned ON (user_id=users.id)
JOIN tasks ON (tasks.id=task_id)
WHERE name="John";
Use GROUP BY and HAVING to see all tasks that were only assigned to one user.
SELECT title
FROM tasks
JOIN assigned ON (task_id=id)
GROUP BY id
HAVING count(*) = 1;
In latter you don't necessarily need to know to who tasks were assigned to, just that they were only assigned to one user. Therefore you don't need to join users table.
Update:
You can use the following to find tasks assigned to John alone:
SELECT name,title
FROM tasks
JOIN assigned ON (task_id=tasks.id)
JOIN users ON (user_id=users.id)
GROUP BY tasks.id
HAVING COUNT(*) = 1 and name="John";
This is possible due to two reasons:
MySQL allows non-aggregated references in HAVING clause.
COUNT(*)=1 forces name to be single value - i.e. you are not in a situation where name would have both 'John' and 'Mike'.

Related

SQL: many-to-many relationship and the 'ALL' clause

I have a table products and a table locations which are linked together in a many-to-many relationship with a table products_locations. Now a client can select a set of products, and I want to run a query that selects only the locations, where ALL of the selected products are available.
This seemed pretty straight forward at first, but I see myself being quite baffled by how to achieve this. I initially thought I could get all the correct location-ids with something like
SELECT location_id
FROM products_locations
WHERE product_id = ALL [the user selected product ids]
But on second thought that does not appear to make sense either (the structure of products_locations is quite simply [product_id, location_id].
Any suggestion on how to structure such a query would be appreciated. I feel like I am overlooking something basic..
EDIT: I am using mysql syntax/dialect
Quick sample: Given the following tables
| products | | locations | | products_locations |
| id | name | | id | name | | product_id | location_id |
|------------| |-----------| |--------------------------|
| 1 | prod1 | | 1 | locA | | 1 | 2 |
| 2 | prod2 | | 2 | locB | | 2 | 1 |
| 3 | prod3 | |-----------| | 2 | 2 |
|------------| | 3 | 1 |
|--------------------------|
If a user selects products 1 and 2, the query should return only location 2. If the user selects products 2 and 3, the query should return location 1. For 1, 2, and 3, no location would be valid, and for product 2, both locations would be valid.
I figured out a query that achieves what I need. Though it is not as clean as I had hoped, it seems to be a robust approach to what I'm trying to query:
SELECT t.location_id
FROM (SELECT location_id, COUNT(*) as n_hits
FROM products_locations
WHERE product_id IN [the user selected products]
GROUP BY location_id) t
WHERE n_hits = [the number of user selected products];
Explanation:
I create a temporary table t which contains every location_id that has at least one matching product in the user's selection, together with the number of times that location matches a product in the user's selection. This is achieved by grouping the query by location_id.
I select the location_id(s) from that temporary table t, where the number of hits is equal to the number of products the user had selected. If that number is lower, I know that at least one product did not match that location.

Multiple many-to-many relationships in SQL

How can I query multiple many-to-many relationships in the same result set?
I have two tables that I typically always LEFT JOIN for a standard result set:
tblPROJECTS-
id | jobnumber | jobname ...
--------------------------------------------------
1 | 1000 | Project X
2 | 2000 | Project Y
3 | 3000 | Project Z
tblTASKS-
id | tasknumber | jobnumber | taskname ...
--------------------------------------------------
1 | 10 | 1000 | Project X: Task 1
2 | 20 | 1000 | Project X: Task 2
3 | 30 | 2000 | Project Y: Task 1
Tasknumber is a GUID, independent of jobnumber, but will never be related to more than one job.
I LEFT JOIN tblTASKS on jobnumber, since not all projects will have tasks (yet)
But then I also have an owners table that defines 1-n users who own either the job as a whole or the individual tasks (or both). Each user can own multiple jobs and/or tasks. The original design of the DB spec'd that a single table be used.
tblOWNERS-
id | ownertype | ownerid | jobnumber | tasknumber ...
----------------------------------------------------------------
1 | 1 | 2 | 1000 |
2 | 1 | 4 | 1000 |
3 | 2 | 2 | | 10
An ownertype of 1 indicates the user owns the overall job.
An ownertype of 2 indicates the user owns the task within the job.
I have two queries that I'm trying to construct:
1) Return the job with all associates job owners, joined with all tasks for that job with all associated task owners.
jobnumber | jobowners | tasknumber | taskowners ...
1000 | 2,4,... | 10 | 2
2000 | | 20 | 4,6,8...
3000 | 4,5,6... | 30 |
2) Given an owner ID, return all the jobs and/or tasks they are associated with.
It's the multiple many-to-many from/to the same tables that has me stumped. Can I accomplish this? If so, am I looking for some sort of UNION or INTERSECT (what do I look up to learn)? Or, if not, what's the better schema for relationships like this that would allow for it?
TIA!
Generally, you need to place the foreign key in the many end of an ERD, so in this case you might have a field called 'ownerid' in the table 'tblPROJECTS', as well as having 'ownerid' in tblTASKS. Assuming then that all tasks have a job ID and an owner, and all projects also have an owner, you can use INNER JOINs:
SELECT P.jobnumber,T.tasknumber,O1.id AS taskowner,O2.id AS jobowner
FROM tblTASKS T
INNER JOIN tblPROJECTS P ON P.jobnumber=T.jobnumber
INNER JOIN tblOWNERS O1 ON O1.id=T.ownerid
INNER JOIN tblOWNERS O2 ON O2.id=P.ownerid
WHERE O1.id=1
This will not concatenate the jobowners and task owners as you have described, but will return a row for each, which you can then concatenate whilst processing the resultset.
Then just replace the WHERE clause as necessary to get the list of tasks for a given Job number...
WHERE P.jobnumber=1000

mySQL JOIN statement with COUNT not matching correctly

I am trying to write a mySQL statement that selects data from one table but counts up entries from another table with a matching ID in a specific field.
The two tables are jobs and job_cards. A job will always be a single entry which will have multiple job cards, so I need to write a singular statement that selects data from the job table but adds another field in the result which is a count of all related job cards.
Example:
jobs table:
| ID | customer | status | date_added |
|----------------------------------------|
| 1 | 3 | active | 2017-10-10 |
------------------------------------------
job_cards table is a bit more complex but includes a column called job_id which will be 1 in this case. But lets say there are 3 cards assigned to the job above. I wrote the following statement:
SELECT j.*, COUNT(jc.id) AS card_count FROM jobs j LEFT JOIN job_cards jc ON j.id = jc.job_id
But the count column only returns the TOTAL number of cards in the job_cards table, regardless of which job they are assigned to. Not only that, but it only ever returns a single result even though at the moment there are 4 entries in the jobs table.
Is there any way to do what I need to do with a single statement?
EDIT:
Sample data from the job_cards table:
| ID | job_id | customer | description | materials | notes |
|--------------------------------------------------------------|
| 1 | 1 | 3 | blah blah | none | test |
| 2 | 1 | 3 | something | pipes | n/a |
----------------------------------------------------------------
The result I would like to get is:
| ID | customer | date_added | card_count |
|-------------------------------------------|
| 1 | 3 | 2017-10-10 | 2 |
---------------------------------------------
Where the ID here is the ID of the job.
You can try this:
SELECT *, (select count(*)
from job_cards jc
where jc.job_id=j.id) as card_count
FROM jobs j

MySQL IN() Operator not working

How to use IN() Operator not working it's.
Those table are example and look the same as the real database I have.I don't have the permitting to add tables or change
Those are the tables:
students
+------+------+
| id | name |
+------+------+
| 1 | ali |
| 2 | man |
| 3 | sos |
+------+------+
Classes
+------+---------+
| c_id | students|
+------+---------+
| 1 | 1,2,3,4 |
| 2 | 88,33,55|
| 3 | 45,23,72|
+------+---------+
When I use this query it return me only the student with id =1
because "id IN (students)" return 1 when the first value are equal.
select name,c_id from students,classes where id IN (students);
when I get the list out on PHP than add it. it work fine.But, this solution need a loop and cost many queries.
select name,c_id from students,classes where id IN (1,2,3,4);
FIND_IN_SET()
the same happened, it's only return 1 but if the value on other position it return 0.
The IN operator works just fine, where it's applicable for what it does.
First, consider restructuring your data to be normalized, and avoid storing values as comma separated lists.
Second, if you absolutely have to deal with columns containing comma separated lists of values, MySQL provides the FIND_IN_SET() function.
FOLLOWUP
Ditch the old-school comma syntax for the join operation, and use the JOIN keyword instead. And relocate the join predicates from the WHERE clause to the ON clause. Fully qualify column references, eg.
SELECT s.name
, c.c_id
FROM students s
JOIN classes c
ON FIND_IN_SET(s.student_id,c.students)
ORDER BY s.name, c.c_id
To reiterate, storing a "comma separated list" in a column is an anti-pattern; it flies against relational theory and normalization, and disregards the best practices around relational databases. O
One might argue for improved performance, but this pattern doesn't improve performance; rather it adds unnecessary complexity in query and DML operations.
You need three tables.
One table students, one table classes, and then one table, say, students_to_classes containing something like
c_id | student_id
1 | 1
1 | 2
1 | 3
1 | 4
2 | 88
and so on.
Then you can query
select c_id from students_to_classes where student_id in (1,2,3,4)
Google "n:m relationship" for background on this.
EDIT
I know you're not specifically asking for another table structure, but this is a way of having a data type (a single number) that works with IN. Please believe me that this is the right way to do it, the reason you run into trouble with something as simple as IN is that you're using a non-standard approach, which, for such a standard problem, is typically not a good idea.
That's not how the function IN is supposed to work. You use IN when you have a list of possible matches like:
instead of:
WHERE id=1 or id=2 or id=3 or id=4
you use:
WHERE id IN (1,2,3,4)
Anyhow, your logic is not correct. The relation of Class and Student is Many-to-Many, thus a third table is needed. Let's call it studend_class, where you can store the students of each class.
student
+------+------+
| id | name |
+------+------+
| 1 | ali |
| 2 | man |
| 3 | sos |
+------+------+
class
+------+---------+
| id | name |
+------+---------+
| 1 | math |
| 2 | english |
| 3 | science |
+------+---------+
student_class
+------------+-------------+
| class_id | student_id |
+------------+-------------+
| 1 | 1 |
| 1 | 2 |
| 1 | 3 |
| 3 | 3 |
+--------------+-----------+
In the example above all students are in math class and ali is also in science class.
Finally, if you whant to know which students are in what class, let's say Math, you can use:
SELECT s.id, s.name, c.name
FROM student s
INNER JOIN student_class sc ON sc.student_id=s.id
INNER JOIN class c ON sc.class_id = c.id
WHERE c.name="math";

MYSQL query fetching DATA from two table using IN method one as composition of multiple data

I have two tables
one as td_job which has these structure
|---------|-----------|---------------|----------------|
| job_id | job_title | job_skill | job_desc |
|------------------------------------------------------|
| 1 | Job 1 | 1,2 | |
|------------------------------------------------------|
| 2 | Job 2 | 1,3 | |
|------------------------------------------------------|
The other Table is td_skill which is this one
|---------|-----------|--------------|
|skill_id |skill_title| skill_slug |
|---------------------|--------------|
| 1 | PHP | 1-PHP |
|---------------------|--------------|
| 2 | JQuery | 2-JQuery |
|---------------------|--------------|
now the job_skill in td_job is actualy the list of skill_id from td_skill
that means the job_id 1 has two skills associated with it, skill_id 1 and skill_id 2
Now I am writing a query which is this one
SELECT * FROM td_job,td_skill
WHERE td_skill.skill_id IN (SELECT td_job.job_skill FROM td_job)
AND td_skill.skill_slug LIKE '%$job_param%'
Now when the $job_param is PHP it returns one row, but if $job_param is JQuery it returns empty row.
I want to know where is the error.
The error is that you are storing a list of id's in a column rather than in an association/junction table. You should have another table, JobSkills with one row per job/skill combination.
The second and third problems are that you don't seem to understand how joins work nor how in with a subquery works. In any case, the query that you seem to want is more like:
SELECT *
FROM td_job j join
td_skill s
on find_in_set(s.skill_id, j.job_skill) > 0 and
s.skill_slug LIKE '%$job_param%';
Very bad database design. You should fix that if you can.