ELO Formula in SQL Query - tsql

This isn't my original query, it's one posted by a gentleman at http://elliot.land/. This is the script for the ELO Rating system. Problem with me is, I've tried and tried, but I don't know how to convert this POSTGRESQL (RECURSIVE) to TSQL (CTE). Anyone can help me with the conversion?
This is the data:
CREATE TABLE plays (
game_number INT NOT NULL, -- must be consecutive and start at 1!
p1name VARCHAR(255) NOT NULL, -- first player
p1score FLOAT NOT NULL, -- 0 or 1
p2name VARCHAR(255) NOT NULL, -- second player
p2score FLOAT NOT NULL -- 0 or 1
);
INSERT INTO plays VALUES
(1, 'Elliot', 0, 'Brendan', 1),
(2, 'Bob', 1, 'Brendan', 0),
(3, 'Bob', 1, 'Elliot', 0),
(4, 'Jane', 1, 'Bob', 0),
(5, 'Bob', 0, 'Brendan', 1),
(6, 'Jane', 1, 'Elliot', 0);
Here is the Formula where I need help with the conversion:
WITH RECURSIVE p(current_game_number) AS (
WITH players AS (
SELECT DISTINCT p1name AS player_name
FROM plays
UNION
SELECT DISTINCT p2name
FROM plays
)
SELECT
0 AS game_number,
player_name,
1000.0 :: FLOAT AS previous_elo,
1000.0 :: FLOAT AS new_elo
FROM players
UNION ALL
(
WITH previous_elos AS (
SELECT *
FROM p
)
SELECT
plays.game_number,
player_name,
previous_elos.new_elo AS previous_elo,
round(CASE WHEN player_name NOT IN (p1name, p2name)
THEN previous_elos.new_elo
WHEN player_name = p1name
THEN previous_elos.new_elo + 32.0 * (p1score - (r1 / (r1 + r2)))
ELSE previous_elos.new_elo + 32.0 * (p2score - (r2 / (r1 + r2))) END)
FROM plays
JOIN previous_elos
ON current_game_number = plays.game_number - 1
JOIN LATERAL (
SELECT
pow(10.0, (SELECT new_elo
FROM previous_elos
WHERE current_game_number = plays.game_number - 1 AND player_name = p1name) / 400.0) AS r1,
pow(10.0, (SELECT new_elo
FROM previous_elos
WHERE current_game_number = plays.game_number - 1 AND player_name = p2name) / 400.0) AS r2
) r
ON TRUE
)
)
SELECT
player_name,
(
SELECT new_elo
FROM p
WHERE t.player_name = p.player_name
ORDER BY current_game_number DESC
LIMIT 1
) AS elo,
count(CASE WHEN previous_elo < new_elo
THEN 1
ELSE NULL END) AS wins,
count(CASE WHEN previous_elo > new_elo
THEN 1
ELSE NULL END) AS losses
FROM
(
SELECT *
FROM p
WHERE previous_elo <> new_elo
ORDER BY current_game_number, player_name
) t
GROUP BY player_name
ORDER BY elo DESC;

Related

(Postgres) Query in a tree table in ascending and descending mode

I'm having some issues with two queries to search in a "tree" table.
So, my table is represented by the following code, and it has one only direction. However, I need to get data in both directions, ascending and descending mode.
create table graph_examle (input int null, output int );
insert into graph_examle (input, output) values
(null, 1),
(1, 2),
(2, 3 ),
(3, 4 ),
(null, 7 ),
(7,8),
(8, 4 ),
(null, 10 ),
(10, 11 ),
(11, 4),
(3, 15),
(25, 15),
(26, 15),
(15, 4 );
The ascending query has some issues. If I search by id 1, I'm expecting to see the relations:
1, 1->2, 1->2->3, 1->2->3->4, but the results are:
WITH recursive cte (initial_id, level, path, loop, input, output) AS
(
SELECT input, 1, ':' ||input || ':' , 0, input, output
FROM graph_examle WHERE input = 1
UNION ALL
SELECT
c.initial_id,
c.level + 1,
c.path ||ur.input|| ':' ,
CASE WHEN c.path LIKE '%:' ||ur.input || ':%' THEN 1 ELSE 0 END,
ur.*
FROM graph_examle ur
INNER JOIN cte c ON c.output = ur.input AND c.loop = 0
)
SELECT *
FROM cte
ORDER BY initial_id, level;
The descending query does not work as expected. If I search by id 4, I'm expecting to see the relations:
4, 4->3, 4->3->2, 4->3->2->1
4->8, 4->8->7
4->11,4->11>10
4->15, (...)
But I'm only getting:
WITH RECURSIVE cte (input, output, level, real_parent_id, path) AS
(
SELECT
ur.input, ur.input, 1, output, ( ur.input|| ' -> ' || ur.output)
FROM graph_examle ur
WHERE ur.output = 4
UNION ALL
SELECT
ur_cte.input, ur.input, level + 1, ur.output, (ur_cte.path || '->' || ur.output)
FROM cte ur_cte
INNER JOIN graph_examle ur on ur.input = ur_cte.real_parent_id
)
SELECT *
FROM cte
ORDER BY path
Note that in my queries I'm trying to solve circular dependencies
The ascending query sounds good ... maybe you can concatenate the path and output columns.
For the descending query, you can try this :
WITH RECURSIVE cte (input, output, level, path, loop) AS
(
SELECT
ur.input, ur.output, 1, ( ur.output|| ' -> ' || ur.input), 0
FROM graph_examle ur
WHERE ur.output = 4
UNION ALL
SELECT
ur.input, ur_cte.output, level + 1, (ur_cte.path || '->' || ur.input),
CASE WHEN ur_cte.path LIKE '%->' || ur.input THEN 1 ELSE 0 END
FROM cte ur_cte
INNER JOIN graph_examle ur on ur.output = ur_cte.input
WHERE ur_cte.loop = 0
AND ur.input IS NOT NULL
)
SELECT *
FROM cte
ORDER BY path
see dbfiddle

Is there a smarter method to create series with different intervalls for count within a query?

I want to create different intervalls:
0 to 10 steps 1
10 to 100 steps 10
100 to 1.000 steps 100
1.000 to 10.000 steps 1.000
to query a table for count the items.
with "series" as (
(SELECT generate_series(0, 10, 1) AS r_from)
union
(select generate_series(10, 90, 10) as r_from)
union
(select generate_series(100, 900, 100) as r_from)
union
(select generate_series(1000, 9000, 1000) as r_from)
order by r_from
)
, "range" as ( select r_from
, case
when r_from < 10 then r_from + 1
when r_from < 100 then r_from + 10
when r_from < 1000 then r_from + 100
else r_from + 1000
end as r_to
from series)
select r_from, r_to,(SELECT count(*) FROM "my_table" WHERE "my_value" BETWEEN r_from AND r_to) as "Anz."
FROM "range";
I think generate_series is the right way, there is another way, we can use simple math to calculate the numbers.
SELECT 0 as r_from,1 as r_to
UNION ALL
SELECT power(10, steps ) * v ,
power(10, steps ) * v + power(10, steps )
FROM generate_series(1, 9, 1) v
CROSS JOIN generate_series(0, 3, 1) steps
so that might as below
with "range" as
(
SELECT 0 as r_from,1 as r_to
UNION ALL
SELECT power(10, steps) * v ,
power(10, steps) * v + power(10, steps)
FROM generate_series(1, 9, 1) v
CROSS JOIN generate_series(0, 3, 1) steps
)
select r_from, r_to,(SELECT count(*) FROM "my_table" WHERE "my_value" BETWEEN r_from AND r_to) as "Anz."
FROM "range";
sqlifddle
Rather than generate_series you could create defined integer range types (int4range), then test whether your value is included within the range (see Range/Multirange Functions and Operators. So
with ranges (range_set) as
( values ( int4range(0,10,'[)') )
, ( int4range(10,100,'[)') )
, ( int4range(100,1000,'[)') )
, ( int4range(1000,10000,'[)') )
) --select * from ranges;
select lower(range_set) range_start
, upper(range_set) - 1 range_end
, count(my_value) cnt
from ranges r
left join my_table mt
on (mt.my_value <# r.range_set)
group by r.range_set
order by lower(r.range_set);
Note the 3rd parameter in creating the ranges.
Creating a CTE as above is good if your ranges are static, however if dynamic ranges are required you can put the ranges into a table. Changes ranges then becomes a matter to managing the table. Not simple but does not require code updates. The query then reduces to just the Main part of the above:
select lower(range_set) range_start
, upper(range_set) - 1 range_end
, count(my_value) cnt
from range_tab r
left join my_table mt
on (mt.my_value <# r.range_set)
group by r.range_set
order by lower(r.range_set);
See demo for both here.

How can I increment the numerical value in my WHERE clause using a loop?

I am currently using the UNION ALL workaround below to calculate old_eps_tfq regression slopes of each ticker based off its corresponding rownum value (see WHERE rownum < x). I am interested to know what the old_eps_tfq is when rownum < 4 then increment 4 by 1 to find out what old_eps_tfq is when rownum < 5, and so on (there are ~20 rownum)
Could I use PL/pgSQL for this?
SELECT * FROM(
WITH regression_slope AS(
SELECT
ROW_NUMBER() OVER ( PARTITION BY ticker ORDER BY earnings_growths_ped) AS rownum,
*
FROM "ANALYTICS"."vEARNINGS_GROWTHS"
--WHERE ticker = 'ACN'
ORDER BY ticker )
SELECT
ticker,
current_period_end_date,
max(earnings_growths_ped) AS max_earnings_growths_ped,
--max(rownum) AS max_rownum,
round(regr_slope(old_eps_tfq, rownum)::numeric, 2) AS slope,
round(regr_intercept(old_eps_tfq, rownum)::numeric, 2) AS y_intercept,
round(regr_r2(old_eps_tfq, rownum)::numeric, 3) AS r_squared
FROM regression_slope
WHERE rownum < 4
GROUP BY ticker, current_period_end_date
ORDER BY ticker asc ) q
UNION ALL
SELECT * FROM(
WITH regression_slope AS(
SELECT
ROW_NUMBER() OVER ( PARTITION BY ticker ORDER BY earnings_growths_ped) AS rownum,
*
FROM "ANALYTICS"."vEARNINGS_GROWTHS"
--WHERE ticker = 'ACN'
ORDER BY ticker )
SELECT
ticker,
current_period_end_date,
max(earnings_growths_ped) AS max_earnings_growths_ped,
--max(rownum) AS max_rownum,
round(regr_slope(old_eps_tfq, rownum)::numeric, 2) AS slope,
round(regr_intercept(old_eps_tfq, rownum)::numeric, 2) AS y_intercept,
round(regr_r2(old_eps_tfq, rownum)::numeric, 3) AS r_squared
FROM regression_slope
WHERE rownum < 5
GROUP BY ticker, current_period_end_date
ORDER BY ticker asc ) q
Here is my table
The top query SELECT * FROM (...) q sounds like useless.
Then you can try this :
WITH regression_slope AS(
SELECT
ROW_NUMBER() OVER ( PARTITION BY ticker ORDER BY earnings_growths_ped) AS rownum,
*
FROM "ANALYTICS"."vEARNINGS_GROWTHS"
--WHERE ticker = 'ACN'
ORDER BY ticker )
SELECT
max,
ticker,
current_period_end_date,
max(earnings_growths_ped) AS max_earnings_growths_ped,
--max(rownum) AS max_rownum,
round(regr_slope(old_eps_tfq, rownum)::numeric, 2) AS slope,
round(regr_intercept(old_eps_tfq, rownum)::numeric, 2) AS y_intercept,
round(regr_r2(old_eps_tfq, rownum)::numeric, 3) AS r_squared
FROM regression_slope
INNER JOIN generate_series(4, 24) AS max -- the range 4 to 24 can be adjusted to the need
ON rownum < max
GROUP BY max, ticker, current_period_end_date
ORDER BY max asc, ticker asc

Rank result set according to condition

I have a table which has 3 columns: Product, Date, Status
I want to rank in this manner:
for each product order by Date, and Rank if Status = FALSE then 0, if it's TRUE then start ranking by 1, continue ranking by the same value if previous Status is TRUE.
In this ordered set if FALSE comes assign to it 0, and for the next coming TRUE status for same product assign x+1 (x here is previous rank value for status TRUE).
I hope picture makes it more clear
This code uses SS2008R2 features which do not include LEAD/LAG. A better solution is certainly possible with more modern versions of SQL Server.
-- Sample data.
declare #Samples as Table ( Product VarChar(10), ProductDate Date,
ProductStatus Bit, DesiredRank Int );
insert into #Samples values
( 'a', '20160525', 0, 0 ), ( 'a', '20160526', 1, 1 ), ( 'a', '20160529', 1, 1 ),
( 'a', '20160601', 1, 1 ), ( 'a', '20160603', 0, 0 ), ( 'a', '20160604', 0, 0 ),
( 'a', '20160611', 1, 2 ), ( 'a', '20160612', 0, 0 ), ( 'a', '20160613', 1, 3 ),
( 'b', '20160521', 1, 1 ), ( 'b', '20160522', 0, 0 ), ( 'b', '20160525', 1, 2 );
select * from #Samples;
-- Query to rank data as requested.
with WithRN as (
select Product, ProductDate, ProductStatus, DesiredRank,
Row_Number() over ( partition by Product order by ProductDate ) as RN
from #Samples
),
RCTE as (
select *, Cast( ProductStatus as Int ) as C
from WithRN
where RN = 1
union all
select WRN.*, C + Cast( 1 - R.ProductStatus as Int ) * Cast( WRN.ProductStatus as Int )
from RCTE as R inner join
WithRN as WRN on WRN.Product = R.Product and WRN.RN = R.RN + 1 )
select Product, ProductDate, ProductStatus, DesiredRank,
C * ProductStatus as CalculatedRank
from RCTE
order by Product, ProductDate;
Note that the sample data was extracted from an image using a Mark I Eyeball. Had the OP taken heed of advice here it would have been somewhat easier.
Tip: Using column names that don't happen to match data types and keywords makes life somewhat simpler.
Try this query,
SELECT a.Product ,
a.Date ,
a.Status ,
CASE WHEN a.Status = 'FALSE' THEN 0
ELSE 1
END [Rank]
FROM ( SELECT Product ,
Date ,
Status ,
ROW_NUMBER() OVER ( PARTITION BY Product ORDER BY DATE, Status ) RNK
FROM TableProduct
) a
ORDER BY Product, a.RNK

Reduce same sub-query repeating many times in select statement, if it includes outside condition

I have a table containing transaction information on items. The possible transactions are purchase (type_id: 1) and sale(type_id: 2). A really stripped down version of the table "Transactions" is like the following:
Transaction_ID Item_ID Transaction_Type_ID Quantity Price Date
1, 1, 1, 50, 10, 6/1,
2, 2, 1, 40, 20, 13/1,
3, 1, 2, 10, 13, 14/1,
4, 2, 2, 20, 25, 3/2,
5, 1, 2, 20, 12, 20/2
I have the following query:
SELECT B.Item_ID
B.Quantity - (SELECT SUM(Quantity) FROM Transactions A Where A.Item_ID = B.Item_ID AND A.Transaction_Type_ID = 2) AS 'Quantity Left'
B.Price * (B.Quantity - (SELECT SUM(Quantity) FROM Transactions A Where A.Item_ID = B.Item_ID AND A.Transaction_Type_ID = 2)) AS 'Purchase Amount Left'
FROM Transaction B
WHERE B.Transaction_Type_ID = 1
AND B.Quantity - (SELECT SUM(Quantity) FROM Transactions A Where A.Item_ID = B.Item_ID AND A.Transaction_Type_ID = 2) > 0
Like you may already noticed, I am trying to get all the purchased items that are still in stock. You may notice also that there is an annoying sub-query repeating twice in the SELECT clause and once in the WHERE clause.
How can I reduce it? Is it possible to use WITH in the beginning of the statement in this case?
JOIN onto an aggregated derived table?
SELECT
B.Item_ID
B.Quantity - ISNULL(A.SUMQuantity, 0) AS 'Quantity Left',
B.Price * (B.Quantity - ISNULL(A.SUMQuantity, 0)) AS 'Purchase Amount Left'
FROM
Transaction B
LEFT JOIN
(
SELECT SUM(Quantity) AS SUMQuantity, Item_ID
FROM Transaction
WHERE Transaction_Type_ID = 2
GROUP BY Item_ID
) ON A.Item_ID = B.Item_ID
WHERE
B.Transaction_Type_ID = 1
AND
B.Quantity - ISNULL(A.SUMQuantity, 0) > 0
If you always have rows where Transaction_Type_ID = 2, then you can remove LEFT JOIN and ISNULL. In your current code, you assume you always have rows.
It also looks like you're mixing entities in the same table based on Transaction_Type_ID. A simple SUM(Quantity) .. GROUP BY Item_ID would be more correct if
- sales are <0 quantity transaction
- restocks are >0 quantity transaction2
You should be able to do something like the following query. As I'm not totally sure what you are trying to do you may have to modify it to suit but it should at least be a good starting point.
select Transaction_Type_ID, Item_ID
, Quantity - QuantitySold
, Value * (Quantity - QuantitySold)
from
(
select B.Transaction_Type_ID, B.Item_ID
, sum(case when B.Transaction_Type_ID = 1 then Quantity else 0 end) QuantityPurchased
, sum(case when B.Transaction_Type_ID = 1 then Quantity * Price else 0 end) Value
, sum(case when B.Transaction_Type_ID = 2 then Quantity else 0 end) QuantitySold
from Transaction B
group by B.Transaction_Type_ID, B.Item_ID
) x;
This should work although untested:
SELECT B.Item_ID
B.Quantity - C.Quantity AS 'Quantity Left'
B.Price * (B.Quantity - C.Quantity) AS 'Purchase Amount Left'
FROM Transaction B
cross apply
SELECT SUM(Quantity) Quantity FROM Transactions A
WHERE A.Item_ID = B.Item_ID AND A.Transaction_Type_ID = 2) C
WHERE B.Transaction_Type_ID = 1 AND B.Quantity > C.Quantity