SQL complicated select statement - mysql

I am trying to create a SELECT statement, but I am not really sure how to accomplish it.
I have 2 tables, user and group. Each user has a userid and each group has a ownerid that specifies who owns the group. Each group also has a name and then inside the user table, there is a column group designating which group that person belongs to. (excuse the annoying structure, I did not create it). I am trying to find all rows in group where the ownerid of that group does not have group (inside the user table) set to the name of that group. If this helps:
User
|-----------------------|
| id | username | group |
|----|----------|-------|
| 0 | Steve | night |
| 1 | Sally | night |
| 2 | Susan | sun |
| 3 | David | xray |
|-----------------------|
Group
|---------------------|
| ownerid | name |
|---------|-----------|
| 1 | night |
| 3 | bravo |
| 2 | sun |
|---------------------|
Where the SQL statement would return the group row for bravo because bravo's owner does not have his group set to bravo.

This is a join back to the original table and then a comparison of the values:
select g.*
from group g join
user u
on g.ownerid = id
where g.name <> u.group;
If the values can be NULL, then the logic would need to take that into account.

An anti-join is a familiar pattern:
SELECT g.*
FROM `Group` g
LEFT
JOIN `User` u
ON u.group = g.name
AND u.id = g.ownerid
WHERE u.id IS NULL
Let's unpack that a bit. We're going to start with returning all rows from Group. Then, we're going to "match" each row in Group with a row (or rows) from User. To be considered a "match", the User.id has to match the Group.ownerid, and the User.group value has to match the Group.name.
The "trick" is to eliminate all rows where we found a match (that's what the WHERE clause does), and that leaves us with only those rows from Group that didn't have a match.
Another way to obtain an equivalent result using a NOT EXISTS predicate
SELECT g.*
FROM `Group` g
WHERE NOT EXISTS
( SELECT 1
FROM `User` u
WHERE u.group = g.name
AND u.id = g.ownerid
)
This is uses a correlated subquery; it usually doesn't perform as fast as a join.
Note that these have the potential to return a slightly different result than the query from Gordon Linoff, if you had a row with in Group that had an ownerid value that wasn't in the user table.

SELECT G.*
FROM Group AS G
WHERE G.Name NOT IN (SELECT DISTINCT U.Group FROM User AS U)

Related

Query membership of group on date with open-ended memberships

Given the following table structure for tracking membership of given groups:
+----+----------+----------------+--------------+
| id | group_id | in_group_begin | in_group_end |
+----+----------+----------------+--------------+
| 1 | 10 | 2019-01-01 | 2019-02-01 |
| 1 | 11 | 2019-02-02 | 2019-03-01 |
| 1 | 12 | 2019-03-01 | NULL |
| 2 | 10 | 2019-01-01 | NULL |
+----+----------+----------------+--------------+
(Where in_group_end being NULL signifies this is their current group)
How would I form a query that would tell me, for example, what group_id each member was associated with on a given date?
... in_group_end IS NULL will give me their current group, not necessarily the group they were in
... in_group_end IS NULL OR in_group_end >= '{$date_str}' could give me multiple options
Ideally I would like something I can use in a joined query, e.g. with a table storing a persons name, address, etc. from which I expect only one row back.
Would some kind of IF stmt in the JOIN do it? or GROUP in a sub-query?
Consider the following logic, which would find all matches for 2019-01-15:
SELECT group_id
FROM yourTable
WHERE
'2019-01-15' >= in_group_begin AND
('2019-01-15' <= in_group_end OR in_group_end IS NULL);
The WHERE clause considers an input date a match if it lies in between the start and end dates or it is greater than the start date and there is no end date. Also, the WHERE clause as written can make use of an index.
Let's say you want to search for a date 2019-01-03.
SELECT
id,
group_id
FROM membership
WHERE '2019-01-03' BETWEEN in_group_begin AND IFNULL(in_group_end, CURRENT_DATE);
If you have another users table which stores details of users and id of that table is used in membership table using id field. You can do following query.
SELECT
u.id,
u.name,
u.address,
m.group_id
FROM users u
INNER JOIN membership m ON u.id = m.id
WHERE '2019-01-03' BETWEEN in_group_begin AND IFNULL(in_group_end, CURRENT_DATE);
Assuming there is a table users from which you want the user's details returned, join it to your table tablename like this:
select u.*, t.group_id
from users u inner join (
select
id, group_id, in_group_begin,
coalesce(in_group_end, current_date) in_group_end
from tablename
) t on t.id = u.id and #date between t.in_group_begin and t.in_group_end
Replace #date with the date you search for.

MySQL many-to-many JOIN returning duplicates

I have two three tables. users, jobs and users_jobs. One user can have many jobs and one job can have many users.
Here is my users table:
+----+------+--------+
| ID | Name | gender |
+----+------+--------+
| 1 | Bob | male |
| 2 | Sara | female |
+----+------+--------+
my jobs table:
+----+----------+
| id | job_id |
+----+----------+
| 1 | Engineer |
| 2 | Plumber |
| 3 | Doctor |
+----+----------+
users_jobs table:
+---------+--------+
| user_id | job_id |
+---------+--------+
| 1 | 1 |
| 1 | 2 |
| 1 | 3 |
| 2 | 1 |
+---------+--------+
As an example, i want to select all males and check if they have at least 1 job and then return their info. If they have no jobs, then don't select/return that user. This is my query:
SELECT * FROM users
INNER JOIN users_jobs
ON users_jobs.user_id = users.id
WHERE users.gender = 'male'
But it returns Bob's info 3 times since he has 3 jobs. I don't want duplicates, how can I get rid of duplicates without using DISTINCT or GROUP BY since performance is very important here.
Thank you!
MySQL allows you to do one a little odd thing, you can select more columns than what's in the GROUP BY clause and aggregate functions (this is not allowed in most other SQL engines). While this sometimes can produce unexpected results, it can work if you don't select data which can appear in multiple rows in the resulting query.
So, for your question - the query WILL return multiple rows for the same user, as some of them have many jobs (busy life, huh?). You generally can't get all their jobs in a single row, as each row is the user's data + their jobs - that's what we JOIN on. But that's not entirely true - you can use GROUP BY and GROUP_CONCAT() to concat all the other data into a single string. I wouldn't generally recommend it, but if its what you need...
SELECT u.Name, GROUP_CONCAT(j.job_id SEPARATOR ', ') as jobs
FROM users u
JOIN users_jobs uj
ON u.ID = uj.user_id
JOIN jobs j
ON j.id = uj.job_id
GROUP BY u.ID
This would return
Name | jobs
--------+-------------------------------
Bob | Engineer, Plumber, Doctor
Sara | Engineer
If you only want males, add in the where clause,
SELECT u.Name, GROUP_CONCAT(j.job_id SEPARATOR ', ') as jobs
FROM users u
JOIN users_jobs uj
ON u.ID = uj.user_id
JOIN jobs j
ON j.id = uj.job_id`
WHERE u.gender = 'male'
GROUP BY u.ID
See live fiddle at http://sqlfiddle.com/#!9/df0afe/2
For this it may will help you,
You can use "Limit" keyword to limit the amount of records fetched
"SELECT * FROM users
INNER JOIN users_jobs
ON users_jobs.user_id = users.id
WHERE users.gender = 'male'" limit 1;
May this will help you!
Thanks!
To follow on from the comments, for performance, it's necessary to use a distinct in your query, try:
SELECT DISTINCT Name FROM users
INNER JOIN users_jobs
ON users_jobs.user_id = users.id
WHERE users.gender = 'male'
If you're looking to get all the columns but keep the id's distinct you can use a GROUP BY, try:
SELECT * FROM users
INNER JOIN users_jobs
ON users_jobs.user_id = users.id
WHERE users.gender = 'male'
GROUP BY users.id
Although this will also effect performance, it depends on what you prioritize the most.

Return data from multiple tables by a single query

I have 2 tables, User and Grade.
Table User
Class_ID | Name
100 | Alex
101 | Anna
Table Grade
Class_ID | Teacher | Subject | Time
100 | Join | English | 9:00
101 | ... | Math | 10:00
Query all the table User, I run:
SELECT * FROM User WHERE class_ID=100;
Query all the table Grade, I run:
SELECT * FROM Grade WHERE class_ID=100;
How can I return
Name | Class_ID | Teacher | Subject | Time
Alex | 100 | Join | English | 9:00
with just a single query?
You can use a simple Inner Join between the two tables.
It is a good practice to use Aliasing for multi table
query.
User is a Reserved keyword in MySQL. So, it would be better if you can change your table name to something else. Otherwise, you may use backticks (`) around it.
Do read: Why is SELECT * considered harmful?
Try the following:
SELECT u.Name,
u.Class_ID,
g.Teacher,
g.Subject,
g.Time
FROM `User` AS u
JOIN Grade AS g
ON u.Class_ID = g.Class_ID
WHERE u.Class_ID = 100
You can use JOIN query. Get an idea from this.
SELECT column_name(s)
FROM table1
INNER JOIN table2 ON table1.column_name = table2.column_name;
SELECT u.Name, u.Class_ID, g.Teacher, g.Subject, g.Time from User u
INNER JOIN Grade g ON u.Class_ID = g.Class_ID ORDER BY u.Class_ID

Get data from one table and count matching records from another

I'm not sure if this is possible. I have one table members and a second table transactions.
I need to get the name of the member from the members table, but also count the number of transactions that member has made from another table. Is this even possible in a JOIN statement, or do I need to write two statements?
SELECT
m.first_name,
m.last_name,
COUNT(t.giver_id),
COUNT(t.getter_id)
FROM
members AS m
JOIN
transactions AS t
ON
m.id = t.giver_id
WHERE
m.id = $i
I should add that it's possible a member has not made any transactions and would therefore not appear in the transactions table.
When I run this code, it returns all NULL columns. When I add the EXPLAIN statement, MySql says "Impossible WHERE noticed after reading const table..."
Is this possible? If so, then what am I doing wrong? Thanks in advance.
EDIT:
Sample data structure and expected output:
members
id | first_name | last_name
_______________________________
1 | Bill | Smith
2 | Joe | Jones
transactions table
id | giver_id | getter_id | status
________________________________________
1 | 1 | 2 | complete
2 | 1 | 2 | complete
So running my query should return:
1 | Bill | Smith | 2 | 0
2 | Joe | Jones | 0 | 2
Simple LEFT JOIN should suffice:
SELECT
m.first_name,
m.last_name,
SUM(CASE WHEN m.id = t.giver_id THEN 1 END) AS giver_count,
SUM(CASE WHEN m.id = t.getter_id THEN 1 END) AS getter_count
FROM members AS m
LEFT JOIN transactions AS t ON m.id = t.giver_id OR m.id = t.getter_id
GROUP BY m.first_name, m.last_name
Do not forget adding GROUP BY when using aggregate functions. Just because MySQL allows the query to go through without it, it doesn't mean it is advised. MySQL will pick up random row values for unaggregated columns which can be problematic. Avoid this anti-pattern.

MySQL joining on specific result

I am trying to build a query to extract data from a form builder where I can join on a field value only where another field value is equal to a specific. My query returns no rows.
users
------------------
|id | name |
------------------
|40 | John |
|45 | Michael |
|47 | Bob |
------------------
data_table
----------------------------------------------------
|id | submission | field_type | field_value |
----------------------------------------------------
|1 | 12345 | user | 40 |
|2 | 12345 | score | 5 |
|3 | 12345 | completed | 1 |
|4 | 23456 | user | 45 |
|5 | 23456 | score | 3 |
|6 | 23456 | completed | 0 |
|7 | 45678 | user | 47 |
|8 | 45678 | score | 2 |
|9 | 45678 | completed | 1 |
----------------------------------------------------
Desired result
---------------
|Name | Score |
---------------
|John | 5 |
|Bob | 2 |
---------------
Select
u.name,
dt2.field_value as score
from
users u
left join
data_table dt on u.id=dt.field_value and dt.field_type='user'
left join
data_table dt2 on dt.submission=dt2.submission and dt2.field_type='score'
where
(dt.field_type='completed' and dt.field_value=1)
http://sqlfiddle.com/#!2/d6e72f/3
You could do something like that (if you really can't change your data structure, which looks... strange).
You'll have a subquery on data_table, with two self join on that table (as you need 3 different rows with conditions)
select u.name, s.score
from users u
join (
select dt.field_value as user_id, dt1.field_value as score
from data_table dt
join data_table dt1 on dt1.submission = dt.submission
join data_table dt2 on dt2.submission = dt1.submission
where dt.field_type='user' and
dt1.field_type = 'score' and
dt2.field_type='completed' and
dt2.field_value = 1
) s
on s.user_id = u.id
see SqlFiddle
Working fiddle http://sqlfiddle.com/#!2/d6e72f/19/0
I've seen this a lot when users get to dynamically add additional attributes to an existing structure. you must first unpivot the additional data and then you can treat it as a normal table. since mysql doesn't support an unpivot, i used the normal work around.
Select u.name, score
from users u
INNER JOIN (
Select submission,
max(case when field_Type='user' then field_value end) as user,
max(case when field_Type='score' then field_value end) as score,
max(case when field_Type='completed' then field_value end) as completed
FROM data_table
group by submission) dt
on dt.user = u.id
and dt.completed = 1
This assumes that for a given submission there can't be more than one field_Type value combination. if there is, this will only return the max value.
Basically what this does is unpivot the data into a table structure that we can then join back to.
The reason we do max or min is so that we get one row back instead of multiple rows for a given submission. Again, this simply unpivots the data and combines the rows back into one. But is based on an assumption that no field_type and field_value will be duplicated within a submission.
Let's take a look at what you are actually doing so we can see what is going wrong:
Select
u.name,
dt2.field_value as score
from
users u
Get a list of users
left join
data_table dt on u.id=dt.field_value and dt.field_type='user'
Join only rows from data_table of type 'user'
left join
data_table dt2 on dt.submission=dt2.submission and dt2.field_type='score'
Join only rows from data_table of type 'score'.
Now your result set looks something like:
User, DT (data table rows for type 'USER'), DT2 (data table rows for type 'SCORE')
where
(dt.field_type='completed' and dt.field_value=1)
Filter the results to include only users where dt.field_type (previously filtered to only include type 'user') have type 'complete'.
Basically your joins filter out all 'complete' rows in 'data_table', so your where statement finds no matches. That is just an explanation of what is happening. On to your problem.
Looking at your schema, you have a few options. As much as I am not a fan of the design, here is how I would write your query:
SELECT U.name, SCORE_DT.field_value AS score
FROM user U
JOIN data_table DT ON DT.field_value=U.id AND DT.field_type="USER"
JOIN data_table SCORE_DT ON SCORE_DT.submission=DT.submission AND SCORE_DT.field_type="SCORE"
JOIN data_table COMPLETED_DT.submission=DT.submission AND COMPLETED_DT.field_type="COMPLETED" AND COMPLETED_DT.field_value=1
Realistically, it would make your life much easier to change the table design, as this data structure requires you to build queries that perform pivot operations for every column you are interested in. For a small data set like this one it is doable, but as the number of columns in your form increases it will become incredibly tedious to work with.
Another variation that works is ...
select x.name
, d.field_value
from data_table d
join (select u.name
, d2.submission
from users u
join data_table d2
on d2.field_value = u.id
and d2.field_type = 'user'
join data_table d3
on d2.submission = d3.submission
and d3.field_type = 'completed'
and d3.field_value = '1'
) x
on x.submission = d.submission
and d.field_type = 'score'
see SqlFiddle
For your set of data, you might find this or xQbert's answer to perform differently.
I would give them both a try. Based on your data, try to get the inner most query to return the smallest data set possible. For example, if you know that only a small subset of the data_table records will have 'completed' = '1', then a 3rd nested select might not be unreasonable if it results in a smaller result for MySql to work with.