MYSQL returning incomplete results - mysql

I have a table called books with following data:
id | description | section
1 | textbook 1 | 1
2 | textbook 2 | 1
3 | textbook 3 | 1
I use the following query to select id 2 and the next and previous rows in section 1:
SELECT id FROM books where id IN
(2,
(SELECT id FROM books WHERE id < 2 LIMIT 1),
(SELECT id FROM books WHERE id > 2 LIMIT 1)
)
AND section = 1
When I add the section = 1, the query returns only id 2 while it should return all the 3 ids but when I remove this part it selects all the 3 ids. My scheme is actually more complex than that but I simplified it to explain the situation. So, what am I doing wrong above?

(SELECT id
FROM books
WHERE id < 2 and section = 1
ORDER BY id DESC
LIMIT 1)
UNION
(SELECT id
FROM books
WHERE id >= 2 and section = 1
ORDER BY id ASC
LIMIT 2)
The problem with your query is that you're picking random ids less than 2 and greater than 2, without taking the section into account. If those IDs aren't in section 1, they won't be included in the result.
The WHERE clause in your outer query is not distributed into the subqueries. The subqueries are executed independently, returning IDs, which are put into the IN clause. Then the outer query filters by section.
FIDDLE

SELECT id FROM books where id IN
(2,
(SELECT id FROM books WHERE id < 2 ORDER BY id DESC LIMIT 1),
(SELECT id FROM books WHERE id > 2 ORDER BY id ASC LIMIT 1)
)
AND section = 1
The ORDER BY should do the trick.

I'm pretty sure this solves your problem:
SELECT id
FROM books
WHERE id IN
(2
, ((SELECT id FROM books WHERE id < 2 AND section = 1 ORDER BY id DESC LIMIT 1))
, ((SELECT id FROM books WHERE id > 2 AND section = 1 ORDER BY id ASC LIMIT 1))
)
AND section = 1

Related

SQL Sub queries Seat Exchange

I am trying to understand how this subquery works. The questions are as follows
Mary is a teacher in a middle school and she has a table seat storing students' names and their corresponding seat ids.The column id is continuous increment.
Mary wants to change seats for the adjacent students.
SELECT
(CASE
WHEN MOD(id, 2) != 0 AND counts != id THEN id + 1
WHEN MOD(id, 2) != 0 AND counts = id THEN id
ELSE id - 1
END) AS id,
student
FROM
seat,
(SELECT
COUNT(*) AS counts
FROM
seat) AS seat_counts
ORDER BY id ASC;
I am trying to understand the how the above query works. So in the CASE it checks if the id is odd or even and checks against the count to see if it is the last element. But how does the ORDER BY ASC work? Because for the first time it selects student Dorris and id 2. but then how is id 2 assigned to Abbot. Thanks.
SQL Table
id | student
1 | Abbot
2 | Doris
3 | Emerson
4 | Green
5 | Jeames
The Result will look like
id | student
1 | Dorris
2 | Abbot
3 | Green
4 | Emerson
5 | Jeames
OK what this is doing is the following -- if an id number is odd and it is not the max number then add one to it, otherwise subtract one from it.
It should be clear that would swap all but the last pair.
I think it is badly written I would write it like this:
WITH student_count(max) as
(
SELECT COUNT(*) FROM seat
)
SELECT
CASE
WHEN student_count.max != id AND MOD(id, 2) != 0 THEN id + 1
WHEN student_count.max != id AND MOD(id, 2) = 0 THEN id - 1
ELSE id
END AS id,
student
FROM seat
CROSS JOIN student_count
ORDER BY id ASC;
I would recommend you to check the results by removing ORDER BY statement. When you remove ORDER BY statement, result will be:
2 Abbot
1 Doris
4 Emerson
3 Green
5 Jeames
Which is completely right for your case. Basically, your query just alters id's values based on the CASE statement. When you add ORDER BY id ASC statement it just orders the result above.
select name,
case when mod(seat_id,2) = 1 and seat_id <> (select max(seat_id) from students) then seat_id + 1
when mod(seat_id,2)= 0 then seat_id - 1
when mod(seat_id,2) = 1 and seat_id = (select max(seat_id) from students) then seat_id
end swap
from students
SELECT
(CASE
WHEN MOD(id, 2) != 0 AND counts != id THEN id + 1
WHEN MOD(id, 2) != 0 AND counts = id THEN id
ELSE id - 1
END) AS id,
student
FROM
seat,
(SELECT
COUNT(*) AS counts
FROM
seat) AS seat_counts
ORDER BY id ASC;

MySQL - How to get the next row

So I have a student_profiles table and ranks table, I want to get the next rank based on the student rank. For example, I have rank 5 then the next rank will be rank 6. So this is my rank structure.
RANKS TABLE:
SELECT * FROM RANKS WHERE style_id = 1"
id style_id level name type primary_colour secondary_colour
1 1 1 Newbie double #4e90b2 #3aad04
22 1 2 Normal solid #fba729 NULL
31 1 3 Expert solid #4e805b NULL
and this is STUDENT_PROFILES TABLE
id | student_id | rank_id
------------------------------------
1 | 1 | 36
2 | 4 | 22
3 | 7 | 10
so all I have a variable is student_id, rank_id & style_id
so for example, I have this value student_id = 4, rank_id = 22 & style_id = 1
It should return
id style_id level name type primary_colour secondary_colour
31 | 1 | 3 | Expert | Solid | #4e805b | NULL
If you just want to get the second row:
Do it like this:
select * from
(select * from table order by id asc limit 2) as a order by id desc limit 1
Any query structure it will work as you need second row if you follow that script.
Try with that:
SELECT * FROM `ranks` WHERE `level` > (SELECT `level` FROM `ranks` WHERE `id` = rank_id) LIMIT 1
But I think it isn't very effective solution.
One option for getting the next highest level in the RANKS table is to self-join this table on the level column, order ascending, and retain the very first record only.
SELECT r2.*
FROM RANKS r1
INNER JOIN
STUDENT_PROFILES s1
ON r1.id = s1.rank_id
INNER JOIN
RANKS r2
ON r2.level > r1.level
ORDER BY r2.level
LIMIT 1
Demo here:
SQLFiddle
Note: If RANKS has duplicate levels, and you want the next level with regard to cardinality (i.e. you don't want a duplicate equal level returned), then my query could be slightly modified to filter out such duplicates.

Select most recent record based on two conditions

I have user1 who exchanged messages with user2 and user4 (these parameters are known). I now want to select the latest sent or received message for each conversation (i.e. LIMIT 1 for each conversation).
SQLFiddle
Currently my query returns all messages for all conversations:
SELECT *
FROM message
WHERE (toUserID IN (2,4) AND userID = 1)
OR (userID IN (2,4) AND toUserID = 1)
ORDER BY message.time DESC
The returned rows should be messageID 3 and 6.
Assuming that higher id values indicate more recent messages, you can do this:
Find all messages that involve user 1
Group the results by the other user id
Get the maximum message id per group
SELECT *
FROM message
WHERE messageID IN (
SELECT MAX(messageID)
FROM message
WHERE userID = 1 -- optionally filter by the other user
OR toUserID = 1 -- optionally filter by the other user
GROUP BY CASE WHEN userID = 1 THEN toUserID ELSE userID END
)
ORDER BY messageID DESC
Updated SQLFiddle
You can do this easily by separating it into two queries with ORDER BY and LIMIT then joining them with UNION:
(SELECT *
FROM message
WHERE (toUserID IN (2,4) AND userID = 1)
ORDER BY message.time DESC
LIMIT 1)
UNION
(SELECT *
FROM message
WHERE (userID IN (2,4) AND toUserID = 1)
ORDER BY message.time DESC
LIMIT 1)
The parenthesis are important here, and this returns messages 2 and 6, which seems correct, not 3 and 6.
It also seems like you could use UNION ALL for performance instead of UNION because there won't be duplicates between the two queries, but it's better if you decide that.
Here's your data:
MESSAGEID USERID TOUSERID MESSAGE TIME
1 1 2 nachricht 1 123
2 1 2 nachricht 2 124
3 2 1 nachricht 3 125
4 3 2 nachricht wrong 1263
5 2 4 nachricht wrong 1261
6 4 1 nachricht sandra 126
The below works as required:
SELECT m1.*
FROM Message m1
LEFT JOIN Message m2
ON LEAST(m1.toUserID, m1.userID) = LEAST(m2.toUserID, m2.userID)
AND GREATEST(m1.toUserID, m1.userID) = GREATEST(m2.toUserID, m2.userID)
AND m2.time > m1.Time
WHERE m2.MessageID IS NULL
AND ( (m1.toUserID IN (2,4) AND m1.userID = 1)
OR (m1.userID IN (2,4) AND m1.toUserID = 1)
);
To simplify how this works, imagine you just wanted the latest message sent by userid 1, rather than having to match the to/from tuples as this adds clutter to the query that doesn't help. To get this I would use:
SELECT m1.*
FROM Message AS m1
LEFT JOIN Message AS m2
ON m2.UserID = m1.UserID
AND m2.time > m1.time
WHERE m1.UserID = 1
AND m2.MessageID IS NULL;
So, we are joining similar messages, stipulating that the second message (m2) has a greater time than the first, where m2 is null it means there is not a similar message with a later time, therefore m2 is the latest message.
Exactly the principal has been applied in the solution, but we have a more complicated join to link conversations.
I have used LEAST and GREATEST in the join, the theory being that since you have 2 members in your tuple (UserID, ToUserID), then in any combination the greatest and the least will be the same, e.g.:
From/To | Greatest | Least |
--------+-----------+-------+
1, 2 | 2 | 1 |
2, 1 | 2 | 1 |
1, 4 | 4 | 1 |
4, 1 | 4 | 1 |
4, 2 | 4 | 2 |
2, 4 | 4 | 2 |
As you can see, in similar From/To the greatest and the least will be the same, so you can use this to join the table to itself.
There are two parts of your query in the following order:
You want the latest outgoing or incoming message for a conversation between two users
You want these latest messages for two different pairs of users, i.e. conversations.
So, lets get the latest message for a conversation between UserID a and UserID b:
SELECT *
FROM message
WHERE (toUserID, userID) IN ((a, b), (b, a))
ORDER BY message.time DESC
LIMIT 1
Then you want these to be combined for the two conversations between UserIDs 1 and 2 and UserIDs 1 and 4. This is where the union comes into play (we do not need to check for duplicates, thus we use UNION ALL, thanks to Marcus Adams, who brought that up first).
So a complete and straightforward solution would be:
(SELECT *
FROM message
WHERE (toUserID, userID) IN ((2, 1), (1, 2))
ORDER BY message.time DESC
LIMIT 1)
UNION ALL
(SELECT *
FROM message
WHERE (toUserID, userID) IN ((4, 1), (1, 4))
ORDER BY message.time DESC
LIMIT 1)
And as expected, you get message 3 and 6 in your SQLFiddle.

MySQL SELECT, JOIN, GROUP BY query optimizing

I am trying to select DISTINCT products from within categories with category ids (1, 5, 12), ORDERED by cat_order + prod_order from MySQL database
The problem:
if a product is found in more than 1 category I need to show the first result,
ie: product number 1 is assigned to categories 1 and 5, I need to display product number 1 from category 1 along with its prod_order and skip the listing in category 5,
essentually I need to display all products from category 1, than move on to category 5 and display all products from there, where product id was not shown previously, and move on to another category in the list (12)
if I run something like:
SELECT
prod_to_cat.prod_id AS prod_to_cat_prod_id,
prod_to_cat.prod_order AS prod_to_cat_prod_order,
prod_to_cat.cat_id AS prod_to_cat_cat_id,
prod_to_cat.cat_order AS prod_to_cat_cat_order,
products.id,
products.name
FROM
prod_to_cat, products
WHERE
prod_to_cat.prod_id = products.id
AND prod_to_cat.cat_id IN (1, 5, 12)
GROUP BY
prod_to_cat.prod_id
ORDER BY
prod_to_cat_cat_order ASC,
prod_to_cat_prod_order DESC
I get inconsistent results (product 1 will not be selected from the first category in the list), that is why I opted to select without "GROUP BY prod_id" and wrap that with another select which than groups by prod_id.
like so:
SELECT
prod_to_cat_prod_id,
prod_to_cat_prod_order,
prod_to_cat_cat_id,
name
FROM
(
SELECT
prod_to_cat.prod_id AS prod_to_cat_prod_id,
prod_to_cat.prod_order AS prod_to_cat_prod_order,
prod_to_cat.cat_id AS prod_to_cat_cat_id,
prod_to_cat.cat_order AS prod_to_cat_cat_order,
products.id,
products.name
FROM
prod_to_cat, products
WHERE
prod_to_cat.prod_id = products.id
AND prod_to_cat.cat_id IN (1, 5, 12)
ORDER BY
prod_to_cat_cat_order ASC,
prod_to_cat_prod_order DESC
) AS prod
GROUP BY
prod_to_cat_prod_id
ORDER BY
prod_to_cat_cat_order ASC,
prod_to_cat_prod_order DESC
LIMIT 0, 10;
What I am trying to do:
I am trying to find a more efficiant way to do this.
Table structure:
prod_to_cat:
prod_id | cat_id | cat_order | prod_order |
1 1 1 2
2 1 1 0
3 1 1 0
1 5 2 4
4 5 2 0
products:
id | name | descr | price |
1 name_1
2 name_2
3 name_3
4 name_4
each product can be in any number of categories, for example product id 1 is in categories id 1 and 5 in the example above.
Thanks a lot for any replies.
Pasha
You want the groupwise minimum:
SELECT prod_to_cat.*,
products.name
FROM prod_to_cat NATURAL JOIN (
SELECT prod_id,
MIN(cat_id) AS cat_id
FROM prod_to_cat
WHERE cat_id IN (1, 5, 12)
GROUP BY prod_id
) t
JOIN products ON t.prod_id = products.id
ORDER BY prod_to_cat.cat_order ASC,
prod_to_cat.prod_order DESC
See it on sqlfiddle.

select the first and the last two elements

hi i have a table with this elements
id name
1 luke
2 johnny
3 perry
4 jenny
5 mark
I have to do a query that take the first element and the lasts 2
i this example
1 luke
4 jenny
5 mark
how can i do?
thanks
I don't think you can do that with a single query : I'd say you'll have to use two queries :
One, to get the first result :
select *
from your_table
order by id asc
limit 1
And one other to get the two last results -- sorting in the opposite direction and getting the first two one will do the trick :
select *
from your_table
order by id desc
limit 2
After that, instead of doing two requests from your programming language to the SQL server, you could send only one query, that would use an UNION to get the results of both :
(select * from your_table order by id asc limit 1)
UNION
(select * from your_table order by id desc limit 2)
But, thinking about it... not sure this is actually possible, having a UNION with order by and limits in each sub-query...
EDIT : I did the test, and it's seems it's possible :
Here are the two queries, executed independantly :
mysql> select id, title from post order by id asc limit 1;
+----+--------------+
| id | title |
+----+--------------+
| 1 | Premier post |
+----+--------------+
1 row in set (0,00 sec)
mysql> select id, title from post order by id desc limit 2;
+----+-------------------------+
| id | title |
+----+-------------------------+
| 7 | Septième post |
| 6 | Sixième post (draft=7) |
+----+-------------------------+
2 rows in set (0,00 sec)
And here's what it looks like with the UNION :
mysql> (select id, title from post order by id asc limit 1) UNION (select id, title from post order by id desc limit 2);
+----+-------------------------+
| id | title |
+----+-------------------------+
| 1 | Premier post |
| 7 | Septième post |
| 6 | Sixième post (draft=7) |
+----+-------------------------+
3 rows in set (0,03 sec)
Note, though, that the order of the 3 resulting rows is not quite well defined...
And, quoting the following manual page of MySQL 5.1 : 12.2.8.3. UNION Syntax :
To apply ORDER BY or LIMIT to an
individual SELECT, place the clause
inside the parentheses that enclose
the SELECT:
(SELECT a FROM t1 WHERE a=10 AND B=1 ORDER BY a LIMIT 10)
UNION
(SELECT a FROM t2 WHERE a=11 AND B=2 ORDER BY a LIMIT 10);
However, use of ORDER BY for
individual SELECT statements implies
nothing about the order in which the
rows appear in the final result
because UNION by default produces an
unordered set of rows.
Take the union of Combine somehow (a) the top one, sorting ascending, (b) the top two, sorting descending.
In two queries:
select * from table order by id asc limit 1
select * from table order by id desc limit 2
I'm not sure if you can do it in 2 queries in mysql. You could do it this way in ms-sql:
select * from table order by id asc limit 1
union all
select * from table order by id desc limit 2
Well, it's not pretty to do it in one query (especially since MySQL doesn't support LIMIT in IN subqueries), but it's possible (but subqueries are kind of cheating):
SELECT id, name
FROM table
WHERE id = (SELECT id FROM table ORDER BY id LIMIT 1)
OR id = (SELECT id FROM table ORDER BY id DESC LIMIT 1)
OR id = (SELECT id FROM table ORDER BY id DESC LIMIT 1,1)