I am often confronted with such db queries:
Get all entries (e.g. comments) of userX and also all entries of the friends of userX
Which is the best way to do this in SQL (MySQL), assuming userX is not friend of himself.
1. Make two queries and merge them later with PHP
a = SELECT *
FROM comments
WHERE user = X
b = SELECT c.*
FROM comments c
INNER JOIN relation r ON r.user2 = c.user
WHERE r.user1 = X
merge(a, b)
That is what I have usually done. It is rather performant, but I cannot use things as ORDER BY or LIMIT
2. Subqueries with IN and UNION
SELECT c.*
FROM comments
WHERE user IN (
SELECT "X"
UNION
SELECT user2 FROM relation WHERE user1 = X
)
This seems to be very slow, and therefore a bad idea, isn't it?
3. Other solutions? Conditional Joins or something...
One way to do this is to start the query from the user table. Join it with the relation table. Then you can join comments on this with a conditional 'ON' statement. This way MySQL can use the indexes.
select c.* from users a
left outer join relation friend on a.id = friend.user1_id
join comments c on (c.user_id = a.id or c.user_id = friend.user2_id)
where a.id = 1
group by c.id;
Here's a working example: http://sqlfiddle.com/#!2/da298/1
Why not:
SELECT c.*
FROM comments
WHERE user = X OR user IN (
SELECT user2 FROM relation WHERE user1 = X
)
If that's slow, you should look at the execution plan. You might be missing an index.
There is nothing wrong with your #2. You most likely don't have the needed indexes.
As an academic case study, here are various forms that should return the same result set. You should try each one with the EXPLAIN and see which performs best for you (making sure the correct indexes are there) (I'm partial to the third because I have found it perform just as well if not better AND it allows for multiple fields to be related. You can search SO or Google for articles about MySQL performance of IN vs EXISTS)
UNION the results - this is an improved version of your #1; the data is still UNIONed in MySQL, not PHP
SELECT *
FROM comments
WHERE user = X
UNION
SELECT c.*
FROM comments c
INNER JOIN relation r
ON r.user2 = c.user
WHERE r.user1 = X
Your option #2 - IN
SELECT c.*
FROM comments
WHERE user IN (
SELECT "X"
UNION
SELECT user2 FROM relation WHERE user1 = X)
EXISTS
Generally, I prefer this to the IN since it allows for multiple fields to be related
SELECT c.*
FROM comments AS c
WHERE c.user = X
OR EXISTS(SELECT *
FROM relation AS r
WHERE r.user1 = X
AND r.user2 = c.user)
Related
Just trying to get into the use of Joins and figure out the best usage and formulation of query. Trying to get all pets for OWNER - Do I call the join with the comparisons?
SELECT *
FROM VISITOR
INNER JOIN OWNER ON VISITOR.OWNER_ID = 1
AND OWNER.ID = 1
Or, do I call the full join and add a WHERE statement?
SELECT *
FROM VISITOR
INNER JOIN OWNER ON VISITOR.OWNER_ID = OWNER.ID
WHERE VISITOR.OWNER_ID =1
Any explanation as to what would be the correct method would also be appreciated.
Thanks all : )
When querying, it is typical to have your joins based on the table-to-table relationship fields. IN ADDITION, you can add additional criteria at the respective level to further filter your final data set. That said...
SELECT
*
FROM
VISITOR V
INNER JOIN OWNER Own
ON V.OWNER_ID = Own.OWNER_ID
WHERE
V.OWNER_ID = 1
Ensure you have an index on the visitor table on the "owner_id" key to help in optimization... Or, you could reverse it such as
SELECT
*
FROM
OWNER Own
JOIN VISITOR V
ON Own.OWNER_ID = V.OWNER_ID
WHERE
Own.OWNER_ID = 1
Using MySQL, I'd like to list all users that don't have the document "liaison". It could means Users that does not have any document at all, or users that have documents, but not "liaison" in these ones.
How can I do using MySQL Query ? I can't make it work!
Here's the (simple) model
Users (id, name)
Documents (id, user_id, name, path)
The NOT EXISTS is a workable solution. As an alternative, sometimes, with large sets, an "anti JOIN" operation can give better performance:
SELECT u.*
FROM Users u
LEFT
JOIN (SELECT d.user_id
FROM Documents d
WHERE d.name = 'liaison'
) l
ON l.user_id = u.id
WHERE l.user_id IS NULL
The inline view aliased as l returns us a list of user_id that have document named 'liaison'; that result set gets outer joined to the Users table, and then we exclude any rows where we found a match (the test of l.user_id IS NULL).
This returns a resultset equivalent to your query with the NOT EXISTS predicate.
Another alternative is to use a query with a NOT IN predicate. Note that we need to guarantee that the subquery does not return a NULL, so the general approach is to include an IS NOT NULL predicate on the column being returned by the subquery.
SELECT u.*
FROM Users u
WHERE u.id NOT IN
( SELECT d.user_id
FROM Documents d
WHERE d.user_id IS NOT NULL
AND d.name = 'liaison'
)
I'd write the NOT EXISTS query like this:
SELECT u.*
FROM Users u
WHERE NOT EXISTS
( SELECT 1
FROM Documents d
WHERE d.name = 'liaison'
AND d.user_id = u.id
)
My personal preference is to use a literal 1 in the SELECT list of that correlated subquery; it reminds me that the query is just looking for the existence of 1 row.)
Again, I usually find that the "anti-join" pattern gives the best performance with large sets. (You'd need to look at the EXPLAIN output for each statement, and measure the performance of each to determine which will work best in your situation.)
The correct query you are looking for is:
SELECT
*
FROM
Users
WHERE
id NOT IN (
SELECT
user_id
FROM
Documents
WHERE
name = "liaison"
)
This will achieve the exact result you are looking for. If a specific user has no documents, it will be listed. If it has many documents, and one of those is 'liaison', it won't be listed.
If you want to search for 'liaison' in your document's name, replace name = "liaison" for name LIKE "%liaison%".
It basically says: Select all users such as there are no documents with name "liaison" pointing to it.
So, I finally came up with this solution that seems to work good :
SELECT * FROM users u WHERE id NOT IN (SELECT DISTINCT user_id FROM user_documents WHERE name = 'LIAISON') ORDER BY c.lastname, c.firstname
SELECT users.*
FROM users left join Documents
on users.id = Documents.user_id
and documents.name='LIAISON'
WHERE documents.user_id is null
select * from Users where not exists (select id from Documents where Users.id = Documents.id and Documents.name = 'liaison')
Try :
SELECT DISTINCT u.*
FROM users u LEFT JOIN documents d ON d.user_id = u.id
WHERE d.id IS NULL OR d.name NOT LIKE '%liaison%'
Remove percent signs if "liaison" is the exact name of the document.
I usually go with the join approach but in this case I am a bit confused. I am not even sure that it is possible at all. I wonder if the following query can be converted to a left join query instead of the multiple select in used:
select
users.id, users.first_name, users.last_name, users.description, users.email
from users
where id in (
select assigned.id_user from assigned where id_project in (
select assigned.id_project from assigned where id_user = 1
)
)
or id in (
select projects.id_user from projects where projects.id in (
select assigned.id_project from assigned where id_user = 1
)
)
This query returns the correct result set. However, I guess the repetition of the query that selects assigned.id_project is a waste.
You could start with the project assignments of user 1 a1. Then find all assignments of other people to those projects a2, and the user in the project table p. The users you are looking for are then in either a2 or p. I added distinct to remove users who can be reached in both ways.
select distinct u.*
from assigned a1
left join
assigned a2
on a1.id_project = a2.id_project
left join
project p
on a1.id_project = p.id
join user u
on u.id = a2.id_user
or u.id = p.id_user
where a1.id_user = 1
Since both subqueries have a condition where assigned.id_user = 1, I start with that query. Let's call that assignment(s) the 'leading assignment'.
Then join the rest, using left joins for the 'optional' tables.
Use an inner join on user that matches either users of assignments linked to the leading assignment or users of projects linked to the leading project.
I use distinct, because I assumen you'd want each user once, event if they have an assignment and a project (or multiple projects).
select distinct
u.id, u.first_name, u.last_name, u.description, u.email
from
assigned a
left join assigned ap on ap.id_project = a.id_project
left join projects p on p.id = a.id_project
inner join users u on u.id = ap.id_user or u.id = p.id_user
where
a.id_user = 1
Here's an alternative way to get rid of the repetition:
SELECT
users.id,
users.first_name,
users.last_name,
users.description,
users.email
FROM users
WHERE id IN (
SELECT up.id_user
FROM (
SELECT id_user, id_project FROM assigned
UNION ALL
SELECT id_user, id FROM projects
) up
INNER JOIN assigned a
ON a.id_project = up.id_project
WHERE a.id_user = 1
)
;
That is, the assigned table's pairs of id_user, id_project are UNIONed with those of projects. The resulting set is then joined with the user_id = 1 projects to obtain the list of all users who share the projects with the ID 1 user. And now it only remains to retrieve the details for those users, which in this case is done in the same way as in your query, i.e. using an IN clause.
I'm sorry to say that I don't have MySQL to thoroughly test the performance of this query and so cannot be quite sure if it is in any way better or worse than your original query or than the one suggested both by #GolezTrol and by #Andomar. Generally I tend to agree with #GolezTrol's comment that a query with simple (semi- or whatever-) joins and repetitive parts might turn out more efficient than an equivalent sophisticated query that doesn't have repetitions. In the end, however, it is testing that must reveal the final answer for you.
Need to generate courses list and count
all
unanswered
answered but unchecked
Questions.
My database structure is looking like that
https://docs.google.com/open?id=0B9ExyO6ktYcOenZ1WlBwdlY2R3c
Explanation for some of tables:
answer_chk_results - checked answers table. So if some answer doesn't exist on this table, it means it's unchecked
lesson_questions - lesson <-> question associations (by id) table
courses-lessons - courses <-> lessons associations (by id) table
Executing
SELECT
c.ID,
c. NAME,
COUNT(lq.id) AS Questions,
COUNT(
CASE
WHEN a.id IS NULL THEN
lq.id
END
) AS UnAnswered,
COUNT(
CASE
WHEN cr.id IS NULL THEN
lq.id
END
) AS UnChecked
FROM
courses c
LEFT JOIN `courses-lessons` cl ON cl.cid = c.id
LEFT JOIN `lesson_questions` lq ON lq.lid = cl.lid
LEFT JOIN answers a ON a.qid = lq.qid
LEFT JOIN answer_chk_results cr ON cr.aid = a.id
GROUP BY
c.ID
Tested it first on SQL fiddle with sample data. (Real data is huge, so I can't place it on sqlfiddle) It returned some values. Thought works well. But while I test it with real data, see that returns wrong values. Forex, when I manually count, result for all questions count must be 25, but it returns 27. Maybe I'm doing something wrong.
Note MySQL server running on my local machine, so I can give you teamviewer id and password if you want to connect to my desktop remotely and test query with real data.
I suspect the problem is that different joins are resulting in a multiplication of rows. The best way to fix this is by using subqueries along each dimension. The following is a more practical way. Replace the COUNTs in the select with COUNT DISTINCT:
SELECT c.ID, c. NAME,
COUNT(distinct lq.id) AS Questions,
COUNT(distinct CASE WHEN a.id IS NULL THEN lq.id END) AS UnAnswered,
COUNT(distinct CASE WHEN cr.id IS NULL THEN lq.id END) AS UnChecked
Compared to COUNT, COUNT DISTINCT is a resource hog (it has to remove duplicates). However, it will probably work fine for your purposes.
Use this query
SELECT
c.ID,
c.NAME,
COUNT(lq.id) AS Questions,
COUNT(IFNULL(a.id),lq.id)AS UnAnswered,
COUNT(IFNULL(cr.id),lq.id)AS UnChecked,
FROM courses c
LEFT JOIN `courses-lessons` cl ON cl.cid = c.id
LEFT JOIN `lesson_questions` AS lq ON lq.lid = cl.lid
LEFT JOIN answers a ON a.qid = lq.qid
LEFT JOIN answer_chk_results cr ON cr.aid = a.id
GROUP BY c.ID
This is the query that I am using to match up a members name to an id.
SELECT eve_member_list.`characterID` ,eve_member_list.`name`
FROM `eve_mining_op_members`
INNER JOIN eve_member_list ON eve_mining_op_members.characterID = eve_member_list.characterID
WHERE op_id = '20110821105414-741653460';
My issue is that I have two different member lists, one lists are members that belong to our group and the second list is a list of members that do not belong to our group.
How do i write this query so that if a member is not found in the eve_member_list table it will look in the eve_nonmember_member_list table to match the eve_mining_op_members.characterID to the charName
I apologize in advance if the question is hard to read as I am not quite sure how to properly ask what it is that I am looking for.
Change your INNER JOIN to a LEFT JOIN and join with both the tables. Use IFNULL to select the name if it appears in the first table, but if it is NULL (because no match was found) then it will use the value found from the second table.
SELECT
characterID,
IFNULL(eve_member_list.name, eve_nonmember_member_list.charName) AS name
FROM eve_mining_op_members
LEFT JOIN eve_member_list USING (characterID)
LEFT JOIN eve_nonmember_member_list USING (characterID)
WHERE op_id = '20110821105414-741653460';
If you have control of the database design you should also consider if it is possible to redesign your database so that both members and non-members are stored in the same table. You could for example use a boolean to specify whether or not they are members. Or you could create a person table and have information that is only relevant to members stored in a separate memberinfo table with an nullable foreign key from the person table to the memberinfo table. This will make queries relating to both members and non-members easier to write and perform better.
You could try a left join on both tables, and then selecting the non-null results from the resulting query -
select * from
(select * from
eve_mining_op_members as x
left join eve_member_list as y1 on x.characterID = y1.characterID
left join eve_member_list2 as y2 on x.characterID = y2.characterID) as t
where t.name is not null
Or, you could try the same thing with a union and using inner join (assuming joined tables are the same):
select * from
(select * from eve_mining_op_members as x
inner join eve_member_list as y1 on x.characterID = y1.characterID
UNION
select * from eve_mining_op_members as x
inner join eve_member_list2 as y2 on x.characterID = y2.characterID) as t
You can throw in your op_id condition where you see fit (sorry, I didn't really understand where it came from). Good luck!
You have several options but by
using a UNION between the eve_member_list and eve_nonmember_member_list table
and JOIN the results of this UNION with your original eve_mining_op_members table
you will get your required results.
SQL Statement
SELECT lst.`characterID`
, lst.`name`
FROM `eve_mining_op_members` AS m
INNER JOIN (
SELECT characterID
, name
FROM eve_member_list
UNION ALL
SELECT characterID
, name
FROM eve_nonmember_member_list
) AS lst ON lst.characterID = m.characterID
WHERE op_id = '20110821105414-741653460';