How to reference generated/aliased table in same query? - mysql

I want to find a user's position in a leaderboard and return the 4 users above and 4 users below their position.
My table, 'predictions', looks something like this:
+----+---------+--------+-------+---------+
| id | userId | score | rank | gameId |
+----+---------+--------+-------+---------+
| 1 | 12 | 11 | 1 | 18 |
| 2 | 1 | 6 | 4 | 18 |
| 3 | 43 | 7 | 3 | 12 |
| 4 | 4 | 9 | 2 | 18 |
| 5 | 98 | 2 | 5 | 19 |
| 6 | 3 | 0 | 6 | 18 |
+----+---------+--------+-------+---------+
Obviously this isn't properly ordered, so I run this:
SELECT l.userId,
l.rank,
l.score,
l.createdAt,
#curRow := #curRow + 1 AS row_number
FROM (SELECT * FROM `predictions` WHERE gameId = 18) l
JOIN (SELECT #curRow := 0) r
ORDER BY rank ASC
which gets me a nice table with each entry numbered.
I then want to search this generated table, find the row_number where userId = X, and then return the values 'around' that result.
I think I have the logic of the query down, I just can't work out how to reference the table 'generated' by the above query.
It would be something like this:
SELECT *
FROM (
SELECT l.userId,
l.rank,
l.score,
l.createdAt,
#curRow := #curRow + 1 AS row_number
FROM (SELECT * FROM `predictions` WHERE gameId = 18) l
JOIN (SELECT #curRow := 0) r
ORDER BY rank ASC) generated_ordered_table
WHERE row_number < (SELECT row_number FROM generated_ordered_table WHERE userId = 1)
ORDER BY row_number DESC
LIMIT 0,5
This fails. What I'm trying to do is to generate my first table with the correct query, give it an alias of generated_ordered_table, and then reference this 'table' later on in this query.
How do I do this?

MySQL version 8+ could have allowed the usage of Window functions, and Common Table Expressions (CTEs); which would have simplified the query quite a bit.
Now, in the older versions (your case), the "Generated Rank Table" (Derived Table) cannot be referenced again in a subquery inside the WHERE clause. One way would be to do the same thing twice (select clause to get generated table) again inside the subquery, but that would be relatively inefficient.
So, another approach can be to use Temporary Tables. We create a temp table first storing the ranks. And, then reference that temp table to get results accordingly:
CREATE TEMPORARY TABLE IF NOT EXISTS gen_rank_tbl AS
(SELECT l.userId,
l.rank,
l.score,
l.createdAt,
#curRow := #curRow + 1 AS row_number
FROM (SELECT * FROM `predictions` WHERE gameId = 18) l
JOIN (SELECT #curRow := 0) r
ORDER BY rank ASC)
Now, you can reference this temp table to get the desired results:
SELECT *
FROM gen_rank_tbl
WHERE row_number < (SELECT row_number FROM gen_rank_tbl WHERE userId = 1)
ORDER BY row_number DESC
LIMIT 0,5

You could use a bunch of unions
select userid,rank,'eq'
from t where gameid = 18 and userid = 1
union
(
select userid,rank,'lt'
from t
where gameid = 18 and rank < (select rank from t t1 where t1.userid = 1 and t1.gameid = t.gameid)
order by rank desc limit 4
)
union
(
select userid,rank,'gt'
from t
where gameid = 18 and rank > (select rank from t t1 where t1.userid = 1 and t1.gameid = t.gameid)
order by rank desc limit 4
);
+--------+------+----+
| userid | rank | eq |
+--------+------+----+
| 1 | 4 | eq |
| 4 | 2 | lt |
| 12 | 1 | lt |
| 3 | 6 | gt |
+--------+------+----+
4 rows in set (0.04 sec)
But it's not pretty

You can use two derived tables:
SELECT p.*,
(#user_curRow = CASE WHEN user_id = #x THEN rn END) as user_rn
FROM (SELECT p.*, #curRow := #curRow + 1 AS rn
FROM (SELECT p.*
FROM predictions p
WHERE p.gameId = 18
ORDER BY rank ASC
) p CROSS JOIN
(SELECT #curRow := 0, #user_curRow := -1) params
) p
HAVING rn BETWEEN #user_curRow - 4 AND #user_currow + 4;

Related

How do you revert the cartesian product of two columns in SQL in order to get the columns before computing the cross join (cartesian product)?

Let's assume I have two columns: letters and numbers in a table called tbl;
letters numbers
a 1
b 2
c 3
d 4
Doing a cartesian product will lead to :
a 1
a 2
a 3
a 4
b 1
b 2
b 3
b 4
c 1
c 2
c 3
c 4
d 1
d 2
d 3
d 4
Write a query that reverts the cartesian product of these two columns back to the original table.
I tried multiple methods from using ROWNUM to selecting distinct values and joining them (which leads me back to the cartesian product)
SELECT DISTINCT *
FROM (SELECT DISTINCT NUMBERS
FROM TBL
ORDER BY NUMBERS) AS NB
JOIN (SELECT DISTINCT LETTERS
FROM TBL
ORDER BY LETTERS) AS LT1
which led me back to the cartesian product....
This is a version that works with 5.7.
SELECT `numbers`,`letters` FROM
(SELECT `numbers`,
#curRank := #curRank + 1 AS rank
FROM Table1 t, (SELECT #curRank := 0) r
GROUP By `numbers`
ORDER BY `numbers`) NB1
INNER JOIN
(SELECT `letters`,
#curRank1 := #curRank1 + 1 AS rank
FROM (
Select `letters` FROM Table1 t
GROUP By `letters`) t2, (SELECT #curRank1 := 0) r
ORDER BY `letters`) LT1 ON NB1.rank = LT1.rank;
https://dbfiddle.uk/?rdbms=mysql_5.7&fiddle=cc17c2cfeff049edc73e437e5e4fd892
As Raymond and Ankit pointed out you have to know which order have the letters and even the order of the numbers has to be defined prior or else you never get a correct answer.
Another way of writing this:
SELECT numbers
, letters
FROM
( SELECT DISTINCT numbers
, #curRank := #curRank + 1 rank
FROM Table1 t
, (SELECT #curRank := 0) r
ORDER
BY numbers
) NB1
JOIN
( SELECT letters
, #curRank1 := #curRank1 + 1 rank
FROM
( SELECT DISTINCT letters
FROM Table1 t
) t2
, (SELECT #curRank1 := 0) r
ORDER
BY letters
) LT1
ON NB1.rank = LT1.rank;
If you are sure that the order will never be destroyed and is deterministic, You can use dense_rank() analytic function to achieve it back -
SELECT LT1.LETTERS, NB.NUMBERS
FROM (SELECT DISTINCT NUMBERS
FROM TBL
ORDER BY NUMBERS) AS NB
JOIN (SELECT DISTINCT LETTERS, RN
FROM (SELECT LETTERS, DENSE_RANK() OVER (ORDER BY LETTERS) RN
FROM TBL
ORDER BY LETTERS) T) AS LT1
ON NB.NUMBERS = LT1.RN
Here is the fiddle
Perhaps this is oversimplifying the problem, but it should be seen that this, or some variation of it, would suffice...
SELECT * FROM my_table;
+---------+---------+
| letters | numbers |
+---------+---------+
| a | 1 |
| a | 2 |
| a | 3 |
| a | 4 |
| b | 1 |
| b | 2 |
| b | 3 |
| b | 4 |
| c | 1 |
| c | 2 |
| c | 3 |
| c | 4 |
| d | 1 |
| d | 2 |
| d | 3 |
| d | 4 |
+---------+---------+
16 rows in set (0.00 sec)
SELECT x.*
, #i:=#i+1 numbers
FROM
( SELECT DISTINCT letters
FROM my_table
) x
, (SELECT #i:=0) vars
ORDER
BY letters;
+---------+---------+
| letters | numbers |
+---------+---------+
| a | 1 |
| b | 2 |
| c | 3 |
| d | 4 |
+---------+---------+

Ranking Where Clause

I have a problem with my Query. I want to select data from my Ranking Query.
My query output is Perfect Like:
------------------------------
Rank | ID | Username | Value
-------------------------------
1 | 5 | Julian | 5000
2 | 2 | Masha | 2400
3 | 4 | Misha | 2300
4 | 1 | Jackson | 1900
5 | 9 | Beruang | 400
-------------------------------
But when I select ID = 4, the output like this:
------------------------------
Rank | ID | Username | Value
-------------------------------
***1*** | 4 | Misha | 2300
-------------------------------
The output of ranking is 1, not 3.
My Query is :
SELECT #curRank := #curRank + 1 AS rank,
a.id, a.username
FROM partimer a CROSS JOIN
(SELECT #curRank := 0) vars
# WHERE a.id = 4
ORDER By id;
If Rank is dinamically computed, you could do this:
SELECT *
FROM (
SELECT #curRank := #curRank + 1 AS rank
, a.id
, a.username
FROM partimer a
CROSS JOIN (SELECT #curRank := 0) vars
ORDER BY value
) p
WHERE p.id = 4;
This way, you store temporary table with rank, and then select from it.
you should like this
SELECT *
FROM (
...{your Query}...
) qry
WHERE qry.id = 4
Your rank is calculated dynamically in the query. The issue here is that these dynamic calculations are applied after the where clause. In other words, when MySQL executes your query, it first filters out all the rows that adhere to the where clause, and only then applies the rank calculation. In the given query, the row with id=4 is indeed the 1st row between all the rows that adhere to the where clause.
One way to get your desired behavior is to perform the original query first and only then filter the results by using this query as a subquery and applying the where clause to the outer query:
SELECT *
FROM (SELECT #curRank := #curRank + 1 AS rank, a.id, a.username
FROM partimer a
CROSS JOIN (SELECT #curRank := 0) vars
ORDER By id) t
WHERE id = 4

Points on highest record in Mysql

My table
+------+-------+--------+
| NAME | MARKS | POINTS |
+------+-------+--------+
| S1 | 53 | (null) |
| S2 | 55 | (null) |
| S3 | 56 | (null) |
| S4 | 55 | (null) |
| S5 | 52 | (null) |
| S6 | 51 | (null) |
| S7 | 53 | (null) |
+------+-------+--------+
Refer : http://www.sqlfiddle.com/#!2/5d046/1
I would like to add 3,2,1 points to the highest Marks. Here S3 goes to 3 points, S2,S4 goes to 2 points and S1,S7 goes to 1 points.
Final outputs looks,
+------+-------+--------+
| NAME | MARKS | POINTS |
+------+-------+--------+
| S1 | 53 | 1 |
| S2 | 55 | 2 |
| S3 | 56 | 3 |
| S4 | 55 | 2 |
| S5 | 52 | 0 |
| S6 | 51 | 0 |
| S7 | 53 | 1 |
+------+-------+--------+
Plz help
My suggestion is that you first calculate the ranking of each mark, and then use that in a case statement in an update.
The following query shows one way to calculate the ranking:
select t.*,
#rn := if(#marks = marks, #rn, #rn + 1) as ranking,
#marks := marks
from myTable t cross join
(select #rn := 0, #marks := -1) const
order by t.marks desc;
(As a note: I am a bit uncomfortable with this method, because MySQL does not guarantee the order of evaluation of the two expressions with constants. If #marks were set before #rn, then it wouldn't work. In practice, that does not seem to happen. And, this is more efficient that the equivalent with a correlated subquery.)
You can then put this into an update using join:
update myTable join
(select t.*,
#rn := if(#marks = marks, #rn, #rn + 1) as ranking,
#marks := marks
from myTable t cross join
(select #rn := 0, #marks := -1) const
order by t.marks desc
) mr
on myTable.Name = mr.Name
set myTable.Points := (case when mr.ranking = 1 then 3
when mr.ranking = 2 then 2
when mr.ranking = 3 then 1
else 0
end);
This has been tested on your SQL Fiddle.
You can do it via variables (see samples in other answers), or via case:
select
myTable.*,
case
when max1.marks is not null then 3
when max2.marks is not null then 2
when max3.marks is not null then 1
else 0
end as score
from
myTable
LEFT JOIN
(select marks from myTable order by marks desc limit 1) AS max1
ON myTable.marks=max1.marks
LEFT JOIN
(select marks from myTable order by marks desc limit 2,1) AS max2
ON myTable.marks=max2.marks
LEFT JOIN
(select marks from myTable order by marks desc limit 3,1) AS max3
ON myTable.marks=max3.marks;
the demo can be found here.
UPDATE myTable t1
INNER JOIN
(
SELECT #row:=#row-1 AS RowPoints, Marks
FROM (
SELECT Marks
FROM myTable
GROUP BY Marks
ORDER BY Marks DESC
LIMIT 3
) AS TopMarks
INNER JOIN (SELECT #row:=4) AS RowInit
) AS AddPoints ON t1.Marks = AddPoints.Marks
SET Points = COALESCE(Points, 0) + AddPoints.RowPoints;
This should work just fine. You should and and index on the Marks column.
The simplest way to do this:
SELECT t.Name Name, t.Marks Marks,
(CASE WHEN Marks = (Select max(marks) from mytable) THEN 3 ELSE 0 END+
CASE WHEN Marks = (Select min(marks) from (Select distinct marks
from mytable order by marks desc limit 2) a) THEN 2 ELSE 0 END+
CASE WHEN Marks = (Select min(marks) from (Select distinct marks
from mytable order by marks desc limit 3) b) THEN 1 ELSE 0 END)
AS `Points`
FROM mytable t;
SQL Fiddle

SQL Find Position in table

I have a table in mySql which has the users ID and scores.
What I would like to do is organise the table by scores (simple) but then find where a certain user ID sits in the table.
So far I would have:
SELECT * FROM table_score
ORDER BY Score DESC
How would I find where userID = '1234' is (i.e entry 10 of 12)
The following query will give you a new column UserRank, which specify the user rank:
SELECT
UserID,
Score,
(#rownum := #rownum + 1) UserRank
FROM table_score, (SELECT #rownum := 0) t
ORDER BY Score DESC;
SQL Fiddle Demo
This will give you something like:
| USERID | SCORE | USERRANK |
-----------------------------
| 4 | 100 | 1 |
| 10 | 70 | 2 |
| 2 | 55 | 3 |
| 1234 | 50 | 4 |
| 1 | 36 | 5 |
| 20 | 33 | 6 |
| 8 | 25 | 7 |
Then you can put this query inside a subquery and filter with a userId to get that user rank. Something like:
SELECT
t.UserRank
FROM
(
SELECT *, (#rownum := #rownum + 1) UserRank
FROM table_score, (SELECT #rownum := 0) t
ORDER BY Score DESC
) t
WHERE userID = '1234';
SQL Fiddle Demo
For a given user id, you can do this with a simple query:
select sum(case when ts.score >= thescore.score then 1 else 0 end) as NumAbove,
count(*) as Total
from table_score ts cross join
(select ts.score from table_score ts where userId = '1234') thescore
If you have indexes on score and userid, this will be quite efficient.

Select a row and rows around it

Ok, let's say I have a table with photos.
What I want to do is on a page display the photo based on the id in the URI. Bellow the photo I want to have 10 thumbnails of nearby photos and the current photo should be in the middle of the thumbnails.
Here's my query so far (this is just an example, I used 7 as id):
SELECT
A.*
FROM
(SELECT
*
FROM media
WHERE id < 7
ORDER BY id DESC
LIMIT 0, 4
UNION
SELECT
*
FROM media
WHERE id >= 7
ORDER BY id ASC
LIMIT 0, 6
) as A
ORDER BY A.id
But I get this error:
#1221 - Incorrect usage of UNION and ORDER BY
Only one ORDER BY clause can be defined for a UNION'd query. It doesn't matter if you use UNION or UNION ALL. MySQL does support the LIMIT clause on portions of a UNION'd query, but it's relatively useless without the ability to define the order.
MySQL also lacks ranking functions, which you need to deal with gaps in the data (missing due to entries being deleted). The only alternative is to use an incrementing variable in the SELECT statement:
SELECT t.id,
#rownum := #rownum+1 as rownum
FROM MEDIA t, (SELECT #rownum := 0) r
Now we can get a consecutively numbered list of the rows, so we can use:
WHERE rownum BETWEEN #midpoint - ROUND(#midpoint/2)
AND #midpoint - ROUND(#midpoint/2) +#upperlimit
Using 7 as the value for #midpoint, #midpoint - ROUND(#midpoint/2) returns a value of 4. To get 10 rows in total, set the #upperlimit value to 10. Here's the full query:
SELECT x.*
FROM (SELECT t.id,
#rownum := #rownum+1 as rownum
FROM MEDIA t,
(SELECT #rownum := 0) r) x
WHERE x.rownum BETWEEN #midpoint - ROUND(#midpoint/2) AND #midpoint - ROUND(#midpoint/2) + #upperlimit
But if you still want to use LIMIT, you can use:
SELECT x.*
FROM (SELECT t.id,
#rownum := #rownum+1 as rownum
FROM MEDIA t,
(SELECT #rownum := 0) r) x
WHERE x.rownum >= #midpoint - ROUND(#midpoint/2)
ORDER BY x.id ASC
LIMIT 10
I resolve this by using the below code:
SELECT A.* FROM (
(
SELECT * FROM gossips
WHERE id < 7
ORDER BY id DESC
LIMIT 2
)
UNION
(
SELECT * FROM gossips
WHERE id > 7
ORDER BY id ASC
LIMIT 2
)
) as A
ORDER BY A.id
I don't believe that you can have an "order by" in different sections of a UNION. Could you just do something like this:
SELECT * FROM media where id >= 7 - 4 and id <= 7 + 4 ORDER BY id
I'm agree with the answer suggested by malonso(+1), but if you try it with id= 1, you will get only 5 thumbnails. I don't know if you want this behaviour. If you want always 10 thumbs, you can try:
select top 10 * from media where id > 7 - 4
The problem is that select top is database dependent (in this case is a sql server clause). Other database has similar clauses:
Oracle:
SELECT * media
FROM media
WHERE ROWNUM < 10
AND id > 7 - 4
MySQL:
SELECT *
FROM media
WHERE id > 7 - 4
LIMIT 10
So maybe you can use the last one.
If we do it, we will have the same problem if you want the last 10 thumbs. By example, If we have 90 thumbs and we give an id=88 ... You can solve it adding an OR condition. In MySQL will be something like:
SELECT *
FROM media
WHERE id > 7 - 4
OR (Id+5) > (select COUNT(1) from media)
LIMIT 10
If you're happy to use temp tables, your original query could be broken down to use them.
SELECT
*
FROM media
WHERE id < 7
ORDER BY id DESC
LIMIT 0, 4
INTO TEMP t1;
INSERT INTO t1
SELECT
*
FROM media
WHERE id >= 7
ORDER BY id ASC
LIMIT 0, 6;
select * from t1 order by id;
drop table t1;
Try union all instead. Union requires the server to ensure that the results are unique and this conflicts with your ordering.
I had to solve a similar problem, but needed to account situations where we always got the same number of rows, even if the desired row was near the top or bottom of the result set (i.e. not exactly in the middle).
This solution is a tweak from OMG Ponies' response, but where the rownum maxes out at the desired row:
set #id = 7;
SELECT natSorted.id
FROM (
SELECT gravitySorted.* FROM (
SELECT Media.id, IF(id <= #id, #gravity := #gravity + 1, #gravity := #gravity - 1) AS gravity
FROM Media, (SELECT #gravity := 0) g
) AS gravitySorted ORDER BY gravity DESC LIMIT 10
) natSorted ORDER BY id;
Here's a break down of what's happening:
NOTE: In the example below I made a table with 20 rows and removed ids 6 and 9 to ensure a gap in ids do not affect the results
First we assign every row a gravity value that's centered around the particular row you're looking for (in this case where id is 7). The closer the row is to the desired row, the higher the value will be:
SET #id = 7;
SELECT Media.id, IF(id <= #id, #gravity := #gravity + 1, #gravity := #gravity - 1) AS gravity
FROM Media, (SELECT #gravity := 0) g
returns:
+----+---------+
| id | gravity |
+----+---------+
| 1 | 1 |
| 2 | 2 |
| 3 | 3 |
| 4 | 4 |
| 5 | 5 |
| 7 | 6 |
| 8 | 5 |
| 10 | 4 |
| 11 | 3 |
| 12 | 2 |
| 13 | 1 |
| 14 | 0 |
| 15 | -1 |
| 16 | -2 |
| 17 | -3 |
| 18 | -4 |
| 19 | -5 |
| 20 | -6 |
| 21 | -7 |
+----+---------+
Next we order all the results by the gravity value and limit on the desired number of rows:
SET #id = 7;
SELECT gravitySorted.* FROM (
SELECT Media.id, IF(id <= #id, #gravity := #gravity + 1, #gravity := #gravity - 1) AS gravity
FROM Media, (SELECT #gravity := 0) g
) AS gravitySorted ORDER BY gravity DESC LIMIT 10
returns:
+----+---------+
| id | gravity |
+----+---------+
| 7 | 6 |
| 5 | 5 |
| 8 | 5 |
| 4 | 4 |
| 10 | 4 |
| 3 | 3 |
| 11 | 3 |
| 2 | 2 |
| 12 | 2 |
| 1 | 1 |
+----+---------+
At this point we have all the desired ids, we just need to sort them back to their original order:
set #id = 7;
SELECT natSorted.id
FROM (
SELECT gravitySorted.* FROM (
SELECT Media.id, IF(id <= #id, #gravity := #gravity + 1, #gravity := #gravity - 1) AS gravity
FROM Media, (SELECT #gravity := 0) g
) AS gravitySorted ORDER BY gravity DESC LIMIT 10
) natSorted ORDER BY id;
returns:
+----+
| id |
+----+
| 1 |
| 2 |
| 3 |
| 4 |
| 5 |
| 7 |
| 8 |
| 10 |
| 11 |
| 12 |
+----+