INNER JOIN with position in second table - sql

I'm running a CodeIgniter backend for a game, where the user info and a highscore table are saved in the database. Every user can only enter the highscore with his personal best, so there is only a single entry per user
Now I need to export that data for the customer so he can pick a random winner.
Basically I want to give him all the user data plus the users points and his position in the highscore table.
I'm having problems figuring out how to get the position in the highscore table.
Let's say my user tables looks like this
user_table
| id|name|
|...| ...|
highscore_table
|id |user_id|points|
|...| ... | ... |
The SQL statement, not including the highscore position, looks like this
SELECT user_table.id, name, points
FROM user_table
INNER JOIN highscore_table ON user_table.id = highscore_table.user_id
ORDER BY name
The exported data should look like this
|id | name|points|hs_position|
|...| ... | ... | ... |
Usually when I request the highscore, I do something similar but sort the data by points. This is just not an option here.
Can somebody tell me how to achieve this, lead me in the right direction or just plain and simple tell me that this is not possible?
Any help is greatly appreciated.

I'm having problems figuring out how to get the position in the
highscore table.
If you're using SQL-Server you can use DENSE_RANK for the HS-Rank and ROW_NUMBER to get the row with the highest point per user:
WITH cte
AS (SELECT user_table.id,
name,
points,
hs_position=Dense_rank()
OVER(
ORDER BY points DESC),
Personalbest_Num=Row_number()
OVER(
partition BY id
ORDER BY points DESC)
FROM user_table
INNER JOIN highscore_table
ON user_table.id = highscore_table.user_id)
SELECT id,
name,
points,
hs_position
FROM cte
WHERE personalbest_num = 1
ORDER BY hs_position ASC,
name ASC

If you are using a RDBMS that does not support ranking functions (such as SQLite), then one way of achieving this is via a subquery - like so:
SELECT u.id, u.name, h.points,
(select count(*) + 1
from highscore_table h2
WHERE h2.points > h.points) hs_position
FROM user_table u
INNER JOIN highscore_table h ON u.id = h.user_id
ORDER BY u.name
SQLFiddle here.
(If you want a dense-ranked high score, change count(*) + 1 to count(distinct h2.points) + 1.)

This is a solution for MySQL:
SELECT user_table.id, name, points, hs_table.rank
FROM user_table
INNER JOIN
(
SELECT hs.*, IF(#prev != points, #pos:=#pos + 1, #pos) as rank
, #prev:=points
FROM highscore_table hs,
(SELECT #pos:=0, #prev:=NULL) vars
ORDER BY points DESC
) hs_table
ON user_table.id = hs_table.user_id
ORDER BY name
People with the same amount of points will have the same rank.

Related

Creating views with group by not grouping properly

I have data that looks like Music92838, Entertainment298928, SPORTS2837 etc. in my Event_type column, and I'm trying to create a view that groups the number of performances by event_type
I tried to do
CREATE VIEW Performances_Type_Cnt
AS SELECT regexp_replace(E.event_type, '[^a-zA-Z]', '', 'g') AS Event_Type,
COUNT(*)
FROM Event_Type E, Performance P
WHERE E.event_id = P.event_id
GROUP BY Event_Type;
Using regex to only select characters, and then group by Music, Sports etc in the Group By Event_type. But something isn't working as in my results I'm getting
event_type | count
---------------+-------
MUSIC | 1
SPORTS | 5
MUSIC | 8
MUSIC | 3
where Music appears more than once in event_type, which isn't the correct result.
Any and all help appreciated!
The "problem" is that Postgres allows column aliases in the GROUP BY. So, it is confused as to whether the EVENT_TYPE comes from the table or the alias. One simple solution is to use positional notation:
CREATE VIEW Performances_Type_Cnt AS
SELECT regexp_replace(E.event_type, '[^a-zA-Z]', '', 'g') AS Event_Type,
COUNT(*) as cnt
FROM Event_Type E JOIN
Performance P
ON E.event_id = P.event_id
GROUP BY 1;
I made some other changes:
Replaced the implicit join with an explicit JOIN. This is the standard way to write joins in SQL.
Added a column alias for COUNT(*).

select only last try

Consider the following table structure for an imaginary table named score:
player_name |player_lastname |try |score
primary key: (player_name,player_lastname,try)
(dont discuss the table schema, its just an example)
This table holds the scores of all players - every player should be able to play either one OR two times. Now, how could I fetch data about every player's last try only (i.e. first tries should be ignored for those who played more than once)?
An example of what I'm trying to achieve:
player_name,player_lastname,try,score
=====================================
bart, simpson,1,250
lisa,simpson,1,150
lisa,simpson,2,250
homer,simpson,1,300
homer,simpson,2,350
maggi,simpson,1,50
The result should be:
player_name,player_lastname,try,score
=====================================
bart, simpson,1,250
lisa,simpson,2,250
homer,simpson,2,350
maggi,simpson,1,50
One option is to JOIN the table to itself using a subquery with MAX:
select s.*
from score s
join (
select max(try) maxtry, player_name, player_lastname
from score
group by player_name, player_lastname
) s2 on s.player_name = s2.player_name
and s.player_lastname = s2.player_lastname
and s.try = s2.maxtry
SQL Fiddle Demo
Depending on your database, you may be able to take advantage of analytic functions such as ROW_NUMBER() though which would make this easier. Here is a another fiddle to demonstrate.
Since you are using postgresql, then you should be able to use the analytic ROW_NUMBER() function. This should work as well:
select *
from (
select try, player_name, player_lastname, score,
Row_Number() Over (Partition By player_name, player_lastname order by try desc) rn
from score
) s
where rn = 1
BTW -- I'd consider adding a player_id as a primary key.
This will probably have the best performance
select distinct on (player_name, player_lastname)
player_name, player_lastname, try, score
from score
order by 1, 2, 3 desc
A Rank function can solve this:
SELECT player_name,player_lastname,TRY,score
FROM (SELECT player_name,player_lastname,TRY,score,RANK() OVER (PARTITION BY player_name, Player_Lastname ORDER BY TRY DESC)AS try_rank
FROM score
)sub
WHERE try_rank = 1
I'm assuming 'try' is the number that can be 1/2.
Edit, forgot Partition BY
SELECT player_name,player_lastname,try,score
FROM scores sc
WHERE NOT EXISTS (
SELECT *
FROM scores nx
WHERE nx.player_name = sc.player_name
AND nx.player_lastname = sc.player_lastname
AND nx.try > sc.try
);
Try this out:
Sel player_name,
player_lastname,
try,
score
from score where try = 2 or
try = 1 and
(player_name,player_lastname) not in
(sel player_name,player_lastname from score where try=2);

sqlite equivalent of row_number() over ( partition by ...?

I'd like to know if it's possible to do the following using a single sqlite statement:
My table looks something like this:
|AnId|UserId|SomeDate|SomeData|
|123 |A |1/1/2010|aadsljvs|
| 87 |A |2/9/2010|asda fas|
|193 |A |2/4/2010|aadsljvs|
|927 |A |7/3/2010|aadsasdf|
|816 |B |1/1/2010|aa32973v|
|109 |B |7/5/2010|aaasfd10|
| 39 |B |1/3/2010|66699327|
...
Each row has a unique id, a user id, a datetime value, and some other data.
I'd like to delete records so I keep the latest 10 records per user, based on SomeDate.
In sql server I'd use something like this:
delete d
from data d
inner join (
select UserId
, AnId
, row_number() over ( partition by UserId order by SomeDate desc )
as RowNum
from data
) ranked on d.AnId = ranked.AnId
where ranked.RowNum > 10
Is there a way to do this in sqlite? The edge case where there are several records with the same SomeDate isn't a particular worry, e.g. if I keep all those records that'd be fine.
I know this question is old, but the following SQLite statement will do what Rory was originally asking for in one statement - Delete all records for a given UserId that are not the 10 most recent records for that UserId (based on SomeDate).
DELETE FROM data
WHERE AnId IN (SELECT AnId
FROM data AS d
WHERE d.UserId = data.UserId
ORDER BY SomeDate DESC
LIMIT -1 OFFSET 10)
I needed to fetch the second row for each "object" in a table with a 1 to many relationship to the "object" table.
Usually in SQL this will be done using ROW_NUMBER() OVER(PARTITION BY object_id ORDER BY primary_id DESC)
In Sqlite I had to come up with this voodoo sub-query to get the same result
SELECT object_id, MAX(_id)
FROM (SELECT object_id, _id
FROM aTable
EXCEPT
SELECT object_id, MAX(_id)
FROM aTable
GROUP BY object_id)
GROUP BY object_id;
Note: The _id is the primary key of aTable and the object table has a 1 to many relationship with the queried table
If you already haven't got the answer.
If it's one table, then you don't need any joins. You can just use:
Delete From data
where AnId not in (Select AnId
from data
Order by SomeDate DESC
Limit 10)
As of Sqlite 3.25 window functions are supported.
See https://www.sqlite.org/windowfunctions.html for details.
This might be prohibitively expensive (perhaps only do it when a user inserts a new record?) but how about this:
for user in users:
user-records = select * from records where user=user
if user-records.length > 10:
delete from records where user=user and date<user-records[10]
(in a mix of SQL and pseudocode)

calculate rank in highscore from 2 tables

i have a trivia game and i want to reward users for 2 events:
1) answering correctly
2) sending a question to the questions pool
i want to query for score and rank of a specific player and i use this query:
SELECT (correct*10+sent*30) AS score, #rank:=#rank+1 AS rank
FROM ( trivia_players
JOIN ( SELECT COUNT(*) AS sent, senderid
FROM trivia_questions
WHERE senderid='$userid'
) a
ON trivia_players.userid=a.senderid
)
ORDER BY score DESC
and it works if the player is in both tables i.e answered correctly AND sent a question.
but it doesn't work if a player hasn't sent a question
any idea how to fix this query? ($userid is the given parameter)
thanks!
Thanks Tom! only problem is the ranks are not correct:
userid score rank
58217 380 1
12354 80 3
32324 0 2
I would probably do it like this:
SELECT
user_id,
score,
rank
FROM
(
SELECT
TP.user_id,
(TP.correct * 10) + (COUNT(TQ.sender_id) * 30) AS score,
#rank:=#rank + 1 AS rank
FROM
Trivia_Players TP
LEFT OUTER JOIN Trivia_Questions TQ ON
TQ.sender_id = TP.user_id
GROUP BY
TP.user_id,
TP.correct
ORDER BY
score DESC
) AS SQ
WHERE
SQ.user_id = $user_id
I don't use MySQL much, so the syntax may not be perfect. I think that you can use a subquery like this in MySQL. Assuming that MySQL handles COUNT() by only counting rows with a non-null value for , this should work.
The keys are that you do a COUNT over a non-null column from Trivia Questions so that it counts them up by the user and you need to use a subquery so that you can get ranks for everyone BEFORE constraining to a particular user id.
Have you tried using a RIGHT JOIN or LEFT JOIN? Just off the top of my head!

MySQL get row position in ORDER BY

With the following MySQL table:
+-----------------------------+
+ id INT UNSIGNED +
+ name VARCHAR(100) +
+-----------------------------+
How can I select a single row AND its position amongst the other rows in the table, when sorted by name ASC. So if the table data looks like this, when sorted by name:
+-----------------------------+
+ id | name +
+-----------------------------+
+ 5 | Alpha +
+ 7 | Beta +
+ 3 | Delta +
+ ..... +
+ 1 | Zed +
+-----------------------------+
How could I select the Beta row getting the current position of that row? The result set I'm looking for would be something like this:
+-----------------------------+
+ id | position | name +
+-----------------------------+
+ 7 | 2 | Beta +
+-----------------------------+
I can do a simple SELECT * FROM tbl ORDER BY name ASC then enumerate the rows in PHP, but it seems wasteful to load a potentially large resultset just for a single row.
Use this:
SELECT x.id,
x.position,
x.name
FROM (SELECT t.id,
t.name,
#rownum := #rownum + 1 AS position
FROM TABLE t
JOIN (SELECT #rownum := 0) r
ORDER BY t.name) x
WHERE x.name = 'Beta'
...to get a unique position value. This:
SELECT t.id,
(SELECT COUNT(*)
FROM TABLE x
WHERE x.name <= t.name) AS position,
t.name
FROM TABLE t
WHERE t.name = 'Beta'
...will give ties the same value. IE: If there are two values at second place, they'll both have a position of 2 when the first query will give a position of 2 to one of them, and 3 to the other...
This is the only way that I can think of:
SELECT `id`,
(SELECT COUNT(*) FROM `table` WHERE `name` <= 'Beta') AS `position`,
`name`
FROM `table`
WHERE `name` = 'Beta'
If the query is simple and the size of returned result set is potentially large, then you may try to split it into two queries.
The first query with a narrow-down filtering criteria just to retrieve data of that row, and the second query uses COUNT with WHERE clause to calculate the position.
For example in your case
Query 1:
SELECT * FROM tbl WHERE name = 'Beta'
Query 2:
SELECT COUNT(1) FROM tbl WHERE name >= 'Beta'
We use this approach in a table with 2M record and this is way more scalable than OMG Ponies's approach.
The other answers seem too complicated for me.
Here comes an easy example, let's say you have a table with columns:
userid | points
and you want to sort the userids by points and get the row position (the "ranking" of the user), then you use:
SET #row_number = 0;
SELECT
(#row_number:=#row_number + 1) AS num, userid, points
FROM
ourtable
ORDER BY points DESC
num gives you the row postion (ranking).
If you have MySQL 8.0+ then you might want to use ROW_NUMBER()
The position of a row in the table represents how many rows are "better" than the targeted row.
So, you must count those rows.
SELECT COUNT(*)+1 FROM table WHERE name<'Beta'
In case of a tie, the highest position is returned.
If you add another row with same name of "Beta" after the existing "Beta" row, then the position returned would be still 2, as they would share same place in the classification.
Hope this helps people that will search for something similar in the future, as I believe that the question owner already solved his issue.
I've got a very very similar issue, that's why I won't ask the same question, but I will share here what did I do, I had to use also a group by, and order by AVG.
There are students, with signatures and socore, and I had to rank them (in other words, I first calc the AVG, then order them in DESC, and then finally I needed to add the position (rank for me), So I did something Very similar as the best answer here, with a little changes that adjust to my problem):
I put finally the position (rank for me) column in the external SELECT
SET #rank=0;
SELECT #rank := #rank + 1 AS ranking, t.avg, t.name
FROM(SELECT avg(students_signatures.score) as avg, students.name as name
FROM alumnos_materia
JOIN (SELECT #rownum := 0) r
left JOIN students ON students.id=students_signatures.id_student
GROUP BY students.name order by avg DESC) t
I was going through the accepted answer and it seemed bit complicated so here is the simplified version of it.
SELECT t,COUNT(*) AS position FROM t
WHERE name <= 'search string' ORDER BY name
I have similar types of problem where I require rank(Index) of table order by votes desc. The following works fine with for me.
Select *, ROW_NUMBER() OVER(ORDER BY votes DESC) as "rank"
From "category_model"
where ("model_type" = ? and "category_id" = ?)
may be what you need is with add syntax
LIMIT
so use
SELECT * FROM tbl ORDER BY name ASC LIMIT 1
if you just need one row..