Counting number of medals in weekly tournaments - sql

I have a table holding weekly scores of players:
# select * from pref_money limit 5;
id | money | yw
----------------+-------+---------
OK32378280203 | -27 | 2010-44
OK274037315447 | -56 | 2010-44
OK19644992852 | 8 | 2010-44
OK21807961329 | 114 | 2010-44
FB1845091917 | 774 | 2010-44
(5 rows)
This SQL statement gets me the weekly winners and how many times each player has won:
# select x.id, count(x.id) from (
select id,
row_number() over(partition by yw order by money desc) as ranking
from pref_money
) x
where x.ranking = 1 group by x.id;
id | count
------------------------+-------
OK9521784953 | 1
OK356310219480 | 1
MR797911753357391363 | 1
OK135366127143 | 1
OK314685454941 | 1
OK308121034308 | 1
OK4087658302 | 5
OK452217781481 | 6
....
I would like to save the latter number in the medals column of the players table:
# \d pref_users;
Table "public.pref_users"
Column | Type | Modifiers
------------+-----------------------------+--------------------
id | character varying(32) | not null
first_name | character varying(64) |
last_name | character varying(64) |
city | character varying(64) |
medals | integer | not null default 0
How to do this please? I can only think of using a temporary table, but there must be an easier way... Thank you
UPDATE:
The query suggested by Clodoaldo works, but now my cronjob occasionally fails with:
/* reset and then update medals count */
update pref_users set medals = 0;
psql:/home/afarber/bin/clean-database.sql:63: ERROR: deadlock detected
DETAIL: Process 31072 waits for ShareLock on transaction 124735679; blocked by process 30368.
Process 30368 waits for ShareLock on transaction 124735675; blocked by process 31072.
HINT: See server log for query details.
update pref_users u
set medals = s.medals
from (
select id, count(id) medals
from (
select id,
row_number() over(partition by yw order by money desc) as ranking
from pref_money where yw <> to_char(CURRENT_TIMESTAMP, 'IYYY-IW')
) x
where ranking = 1
group by id
) s
where u.id = s.id;

update pref_users u
set medals = s.medals
from (
select id, count(id) medals
from (
select id,
row_number() over(partition by yw order by money desc) as ranking
from pref_money
) x
where ranking = 1
group by id
) s
where u.id = s.id

You could create a view which uses your "medal-select" and joins it with the actual data:
CREATE VIEW pref_money_medals AS
SELECT *
FROM pref_money
JOIN (SELECT count(x.id)
FROM (SELECT id, row_number()
OVER(PARTITION BY yw ORDER BY money DESC) AS ranking
FROM pref_money
) x
WHERE x.ranking = 1 group by x.id) medals
ON pref_money.id = medals.id;

Related

Select first rows where condition [duplicate]

Here's what I'm trying to do. Let's say I have this table t:
key_id | id | record_date | other_cols
1 | 18 | 2011-04-03 | x
2 | 18 | 2012-05-19 | y
3 | 18 | 2012-08-09 | z
4 | 19 | 2009-06-01 | a
5 | 19 | 2011-04-03 | b
6 | 19 | 2011-10-25 | c
7 | 19 | 2012-08-09 | d
For each id, I want to select the row containing the minimum record_date. So I'd get:
key_id | id | record_date | other_cols
1 | 18 | 2011-04-03 | x
4 | 19 | 2009-06-01 | a
The only solutions I've seen to this problem assume that all record_date entries are distinct, but that is not this case in my data. Using a subquery and an inner join with two conditions would give me duplicate rows for some ids, which I don't want:
key_id | id | record_date | other_cols
1 | 18 | 2011-04-03 | x
5 | 19 | 2011-04-03 | b
4 | 19 | 2009-06-01 | a
How about something like:
SELECT mt.*
FROM MyTable mt INNER JOIN
(
SELECT id, MIN(record_date) AS MinDate
FROM MyTable
GROUP BY id
) t ON mt.id = t.id AND mt.record_date = t.MinDate
This gets the minimum date per ID, and then gets the values based on those values. The only time you would have duplicates is if there are duplicate minimum record_dates for the same ID.
I could get to your expected result just by doing this in mysql:
SELECT id, min(record_date), other_cols
FROM mytable
GROUP BY id
Does this work for you?
To get the cheapest product in each category, you use the MIN() function in a correlated subquery as follows:
SELECT categoryid,
productid,
productName,
unitprice
FROM products a WHERE unitprice = (
SELECT MIN(unitprice)
FROM products b
WHERE b.categoryid = a.categoryid)
The outer query scans all rows in the products table and returns the products that have unit prices match with the lowest price in each category returned by the correlated subquery.
I would like to add to some of the other answers here, if you don't need the first item but say the second number for example you can use rownumber in a subquery and base your result set off of that.
SELECT * FROM
(
SELECT
ROW_NUM() OVER (PARTITION BY Id ORDER BY record_date, other_cols) as rownum,
*
FROM products P
) INNER
WHERE rownum = 2
This also allows you to order off multiple columns in the subquery which may help if two record_dates have identical values. You can also partition off of multiple columns if needed by delimiting them with a comma
This does it simply:
select t2.id,t2.record_date,t2.other_cols
from (select ROW_NUMBER() over(partition by id order by record_date)as rownum,id,record_date,other_cols from MyTable)t2
where t2.rownum = 1
If record_date has no duplicates within a group:
think of it as of filtering. Simpliy get (WHERE) one (MIN(record_date)) row from the current group:
SELECT * FROM t t1 WHERE record_date = (
select MIN(record_date)
from t t2 where t2.group_id = t1.group_id)
If there could be 2+ min record_date within a group:
filter out non-min rows (see above)
then (AND) pick only one from the 2+ min record_date rows, within the given group_id. E.g. pick the one with the min unique key:
AND key_id = (select MIN(key_id)
from t t3 where t3.record_date = t1.record_date
and t3.group_id = t1.group_id)
so
key_id | group_id | record_date | other_cols
1 | 18 | 2011-04-03 | x
4 | 19 | 2009-06-01 | a
8 | 19 | 2009-06-01 | e
will select key_ids: #1 and #4
SELECT p.* FROM tbl p
INNER JOIN(
SELECT t.id, MIN(record_date) AS MinDate
FROM tbl t
GROUP BY t.id
) t ON p.id = t.id AND p.record_date = t.MinDate
GROUP BY p.id
This code eliminates duplicate record_date in case there are same ids with same record_date.
If you want duplicates, remove the last line GROUP BY p.id.
This a old question, but this can useful for someone
In my case i can't using a sub query because i have a big query and i need using min() on my result, if i use sub query the db need reexecute my big query. i'm using Mysql
select t.*
from (select m.*, #g := 0
from MyTable m --here i have a big query
order by id, record_date) t
where (1 = case when #g = 0 or #g <> id then 1 else 0 end )
and (#g := id) IS NOT NULL
Basically I ordered the result and then put a variable in order to get only the first record in each group.
The below query takes the first date for each work order (in a table of showing all status changes):
SELECT
WORKORDERNUM,
MIN(DATE)
FROM
WORKORDERS
WHERE
DATE >= to_date('2015-01-01','YYYY-MM-DD')
GROUP BY
WORKORDERNUM
select
department,
min_salary,
(select s1.last_name from staff s1 where s1.salary=s3.min_salary ) lastname
from
(select department, min (salary) min_salary from staff s2 group by s2.department) s3

Count max number of consecutive occurrences of a value in SQL Server

I have a table with players, results and ID:
Player | Result | ID
---------------
An | W | 1
An | W | 1
An | L | 0
An | W | 1
An | W | 1
An | W | 1
Ph | L | 0
Ph | W | 1
Ph | W | 1
Ph | L | 0
Ph | W | 1
A 'W' will always have an ID of 1,
I need to create a query that will count the maximum number of consecutive 'W's for each player:
Player | MaxWinStreak
---------------------
An | 3
Ph | 2
I tried to use Rows Unbounded Preceeding but i can only get it to count the maximum number of Ws in total, and not consecutively
Select
t2.player
,max(t2.cumulative_wins) As 'Max'
From
( Select
t.Player
,Sum(ID) Over (Partition By t.Result,t.player
Order By t.GameWeek Rows Unbounded Preceding) As cumulative_wins
From
t
) t2
Group By
t2.player
Is there a different approach i can take ?
You need a column to specify the ordering. SQL tables represent unordered sets. In the below query, the ? represents this column.
You can use the difference of row numbers to get each winning streak:
select player, count(*) as numwins
from (select t.*,
row_number() over (partition by player order by ?) as seqnum,
row_number() over (partition by player, result order by ?) as seqnum_r
from t
) t
where result = 'W'
group by player, (seqnum - seqnum_r);
You can then get the maximum:
select player, max(numwins)
from (select player, count(*) as numwins
from (select t.*,
row_number() over (partition by player order by ?) as seqnum,
row_number() over (partition by player, result order by ?) as seqnum_r
from t
) t
where result = 'W'
group by player, (seqnum - seqnum_r)
) pw
group by player;

Group by minimum value in one field while selecting distinct rows

Here's what I'm trying to do. Let's say I have this table t:
key_id | id | record_date | other_cols
1 | 18 | 2011-04-03 | x
2 | 18 | 2012-05-19 | y
3 | 18 | 2012-08-09 | z
4 | 19 | 2009-06-01 | a
5 | 19 | 2011-04-03 | b
6 | 19 | 2011-10-25 | c
7 | 19 | 2012-08-09 | d
For each id, I want to select the row containing the minimum record_date. So I'd get:
key_id | id | record_date | other_cols
1 | 18 | 2011-04-03 | x
4 | 19 | 2009-06-01 | a
The only solutions I've seen to this problem assume that all record_date entries are distinct, but that is not this case in my data. Using a subquery and an inner join with two conditions would give me duplicate rows for some ids, which I don't want:
key_id | id | record_date | other_cols
1 | 18 | 2011-04-03 | x
5 | 19 | 2011-04-03 | b
4 | 19 | 2009-06-01 | a
How about something like:
SELECT mt.*
FROM MyTable mt INNER JOIN
(
SELECT id, MIN(record_date) AS MinDate
FROM MyTable
GROUP BY id
) t ON mt.id = t.id AND mt.record_date = t.MinDate
This gets the minimum date per ID, and then gets the values based on those values. The only time you would have duplicates is if there are duplicate minimum record_dates for the same ID.
I could get to your expected result just by doing this in mysql:
SELECT id, min(record_date), other_cols
FROM mytable
GROUP BY id
Does this work for you?
To get the cheapest product in each category, you use the MIN() function in a correlated subquery as follows:
SELECT categoryid,
productid,
productName,
unitprice
FROM products a WHERE unitprice = (
SELECT MIN(unitprice)
FROM products b
WHERE b.categoryid = a.categoryid)
The outer query scans all rows in the products table and returns the products that have unit prices match with the lowest price in each category returned by the correlated subquery.
I would like to add to some of the other answers here, if you don't need the first item but say the second number for example you can use rownumber in a subquery and base your result set off of that.
SELECT * FROM
(
SELECT
ROW_NUM() OVER (PARTITION BY Id ORDER BY record_date, other_cols) as rownum,
*
FROM products P
) INNER
WHERE rownum = 2
This also allows you to order off multiple columns in the subquery which may help if two record_dates have identical values. You can also partition off of multiple columns if needed by delimiting them with a comma
This does it simply:
select t2.id,t2.record_date,t2.other_cols
from (select ROW_NUMBER() over(partition by id order by record_date)as rownum,id,record_date,other_cols from MyTable)t2
where t2.rownum = 1
If record_date has no duplicates within a group:
think of it as of filtering. Simpliy get (WHERE) one (MIN(record_date)) row from the current group:
SELECT * FROM t t1 WHERE record_date = (
select MIN(record_date)
from t t2 where t2.group_id = t1.group_id)
If there could be 2+ min record_date within a group:
filter out non-min rows (see above)
then (AND) pick only one from the 2+ min record_date rows, within the given group_id. E.g. pick the one with the min unique key:
AND key_id = (select MIN(key_id)
from t t3 where t3.record_date = t1.record_date
and t3.group_id = t1.group_id)
so
key_id | group_id | record_date | other_cols
1 | 18 | 2011-04-03 | x
4 | 19 | 2009-06-01 | a
8 | 19 | 2009-06-01 | e
will select key_ids: #1 and #4
SELECT p.* FROM tbl p
INNER JOIN(
SELECT t.id, MIN(record_date) AS MinDate
FROM tbl t
GROUP BY t.id
) t ON p.id = t.id AND p.record_date = t.MinDate
GROUP BY p.id
This code eliminates duplicate record_date in case there are same ids with same record_date.
If you want duplicates, remove the last line GROUP BY p.id.
This a old question, but this can useful for someone
In my case i can't using a sub query because i have a big query and i need using min() on my result, if i use sub query the db need reexecute my big query. i'm using Mysql
select t.*
from (select m.*, #g := 0
from MyTable m --here i have a big query
order by id, record_date) t
where (1 = case when #g = 0 or #g <> id then 1 else 0 end )
and (#g := id) IS NOT NULL
Basically I ordered the result and then put a variable in order to get only the first record in each group.
The below query takes the first date for each work order (in a table of showing all status changes):
SELECT
WORKORDERNUM,
MIN(DATE)
FROM
WORKORDERS
WHERE
DATE >= to_date('2015-01-01','YYYY-MM-DD')
GROUP BY
WORKORDERNUM
select
department,
min_salary,
(select s1.last_name from staff s1 where s1.salary=s3.min_salary ) lastname
from
(select department, min (salary) min_salary from staff s2 group by s2.department) s3

How to increment value of a Column based on previous Row's value in SQL

I'm using SQL Server 2008.
I have two Tables: User_master and Item_master.
There is a User with user_id = 10.
|---------|
| user_id |
|---------|
| 10 |
|---------|
There are 5 Items with item_id = 20 to 24.
|---------|---------|------------|
| item_id | user_id | item_order |
|---------|---------|------------|
| 20 | 10 | 0 |
|---------|---------|------------|
| 21 | 10 | 0 |
|---------|---------|------------|
| 22 | 10 | 0 |
|---------|---------|------------|
| 23 | 10 | 0 |
|---------|---------|------------|
| 24 | 10 | 0 |
|---------|---------|------------|
There is one more column in Item_master that is item_order(int).
I want to place item_order = 0 to 4 in all these rows with only single query.
Is it possible?
EDIT :
item_id is not supposed to be in order.
For example, instead of 20,21,22,23,24; it could be 20,25,31,47,58.
You can use the row_number() window function to assign an increasing number to each row with the same user_id. A subquery is required because you cannot use window functions directly in the set clause.
update im
set im.item_order = im.rn
from (
select row_number() over (partition by user_id
order by item_id) - 1 as rn
, *
from item_master
) as im;
Live example at SQL Fiddle.
Extrapolating a little bit and since {item_id, user_id} is unique in the table, here is a generic solution:
UPDATE m
SET item_order = x.new_item_order
FROM item_master m
INNER JOIN (
SELECT [item_id], [user_id],
(ROW_NUMBER() OVER (PARTITION BY [user_id]
ORDER BY [item_id]))-1 AS [new_item_order]
FROM item_master
) x ON m.item_id = x.item_id AND m.user_id = x.user_id
SQL Fiddle example
This will set the item_order column in order of item_id for each user, starting at 0.
I've assumed you'd want to group by user to generate the number and that item_order column is already there, just needs updating?
update IM
set item_order = t.RowNumber
FROM Item_master IM
INNER JOIN
(select item_id , user_id , ROW_NUMBER() over(PARTITION BY user_id order by item_id ) -1 as 'RowNumber' from Item_master) T
ON T.item_id = IM.item_id

Find previous/next rows with order by when querying for a specific id

I have a table such as (simplified to the extreme to make it clearer)
create table mytable (
id integer not null,
owner text not null,
order_field_1 integer not null,
order_field_2 integer not null
)
I'm trying to get the next and previous elements' ids every time I get a row from the database, to allow navigation. The rows are not ordered by id, but by ORDER BY order_field_1 DESC, order_field_2 DESC.
When getting the last entries for an owner, I have no problem to find what I want using a window and lead/lag
SELECT
id,
owner,
lag(id) over w AS previous_id,
lead(id) over w AS next_id
FROM
mytable
WHERE
owner = 'someuser'
WINDOW w AS (
ORDER BY order_field_1 DESC,
order_field_2 DESC
)
ORDER BY
order_field_1 DESC,
order_field_2 DESC
LIMIT
5
This is written from memory but that's the gist of it, and it works perfectly.
My problem is when I want to get a specific row, using owner AND id, yet I still want to find the previous and next ids, I can not use a window function anymore since only one row is returned by the where, and my current solution of doing a subquery to get both navigation id is not very good performance wise
For exemple (I only put previous id since it's the same for next)
SELECT
m1.id,
m1.owner,
(
SELECT
m2.id
FROM
mytable m2
WHERE
m2.owner = m1.owner
AND m2.id != m1.id
AND (
m2.order_field_1 < m1.order_field_1
OR (
m2.order_field_1 = m1.order_field_1
AND m2.order_field_2 <= m1.order_field_2
)
ORDER BY
m2.order_field_1 DESC,
m2.order_field_2 DESC
LIMIT
1
) AS previous_id
FROM
mytable m1
WHERE
owner = 'someuser'
AND id = 12345
So I'm selecting my row, then selection the first row from the same user, with a different id, that is either with a lower order_field_1 or the same but a lower order_field_2.
This is not really efficient and I am getting poor performances, and I'm wondering if anyone has any idea on how I could improve it ?
Exemple dataset:
id | owner | order_field_1 | order_field_2
1 | someuser | 4 | 2
2 | someuser | 2 | 8
3 | someuser | 4 | 3
4 | someuser | 3 | 2
5 | someuser | 4 | 6
6 | someuser | 4 | 5
Ordered:
id | owner | order_field_1 | order_field_2
5 | someuser | 4 | 6
6 | someuser | 4 | 5
3 | someuser | 4 | 3
1 | someuser | 4 | 2
4 | someuser | 3 | 2
2 | someuser | 2 | 8
If I select owner = 'someuser' and id = 3, previous_id should be 1, next_id should be 6.
If I select owner = 'someuser' and id = 1, previous_id should be 4, next_id should be 3.
Thanks in advance for any help
With window functions and CTE
It is much cheaper to have the WHERE owner = 'someuser' in the CTE already:
WITH t AS (
SELECT id
,owner
,lag(id) over w AS previous_id
,lead(id) over w AS next_id
FROM mytable
WHERE owner = 'someuser'
WINDOW w AS (ORDER BY order_field_1 DESC, order_field_2 DESC)
)
SELECT *
FROM t
WHERE id = 3
Also, as you only select a single row, there is no need for an ORDER BY in the final SELECT.
__
Old school with subqueries
It's rather ugly, but it might be faster if there are a lot of rows per owner. You'll have to test ...
SELECT id
, owner
,(SELECT id
FROM tbl p
WHERE p.owner = t.owner -- same owner
AND p.id <> t.id -- different id
AND p.order_field_1 <= t.order_field_1
AND p.order_field_2 <= t.order_field_2
ORDER BY order_field_1 DESC
, order_field_2 DESC
LIMIT 1) AS previous_id
,(SELECT id
FROM tbl n
WHERE n.owner = t.owner
AND n.id <> t.id
AND n.order_field_1 >= t.order_field_1
AND n.order_field_2 >= t.order_field_2
ORDER BY order_field_1
, order_field_2
LIMIT 1) AS next_id
FROM tbl t
WHERE owner = 'someuser'
AND id = 3
This one works for older versions of PostgreSQL, too.
The key to performance are proper indexes, of course.
How about finding the lag and lead values before applying the WHERE clause?
WITH T as (
SELECT
id,
owner,
lag(id) over w AS previous_id,
lead(id) over w AS next_id
FROM
mytable
WINDOW w AS (
ORDER BY order_field_1 DESC,
order_field_2 DESC
)
)
SELECT * FROM T
WHERE
owner = 'someuser' AND id = 3
ORDER BY
order_field_1 DESC,
order_field_2 DESC