Nested conditional statements on Mysql - mysql

I'm developing a query on phpmyadmin mysql in which I intend to display the running balance of a column say 'CurrentBalance'
The running balance of this column depends when the Activity is a deposit (+), a withdraw (-) , bet (-), payout (+)
What I come up is this
SELECT CONCAT("Trans ID ",`TransactionID`) AS ID,
`DateTime`,`Type` AS Activity, `Amount`, 0 AS Payout,
CASE WHEN (SELECT Activity) = "deposit" THEN `Amount`+ `playerdb`.`CurrentBalance`
ELSE CASE WHEN (SELECT Activity) = "withdraw" OR (SELECT Activity) = "bet"
THEN CASE WHEN (SELECT Payout) >0 THEN (SELECT Payout) + `playerdb`.`CurrentBalance`
ELSE `Amount` - `playerdb`.`CurrentBalance` END END END AS CurrentBalance
FROM `transactiondb` LEFT JOIN `playerdb` ON
`transactiondb`.`PlayerID` = `playerdb`.`PlayerID`
WHERE `transactiondb`.`PlayerID`=10078 UNION ALL
SELECT CONCAT("Bet ID ",`BetID`),`DateTime`,"Bet", `BetAmount`,`Payout`, (SELECT CurrentBalance) FROM `betdb` WHERE `PlayerID`=10078 ORDER BY `DateTime`
The Idea
http://postimg.org/image/x3fsxq2qz/
Doing the (SELECT CurrentBalance) on the 2nd SELECT statement yields this error
1054 - Unknown column 'CurrentBalance' in 'field list'
I need to get the CurrentBalance of the previous record so I tried using the alias
Is that possible?

This is a "teach a man to fish..." answer, since your question is not yet totally clear to me.
In general you can access the previous row with variables.
Have a look at this example:
SELECT
t.*
, #running_total := IF(#foo != foo, 0, #running_total + bar)
, #foo := foo
FROM a_table t
, (SELECT #running_total := 0, #foo := NULL) variable_initialization_subquery
ORDER BY foo;
As the subquery alias suggests, here
, (SELECT #running_total := 0, #foo := NULL) variable_initialization_subquery
we initialize the variables.
We ORDER BY foo because there's no reliable order in data in a database when you don't specify it clearly.
Then the SELECT clause is considered with one column after another. Here the order of the columns is important, too.
First we do this:
, #running_total := IF(#foo != foo, 0, #running_total + bar)
This calculates a running total for every foo. When the foo changes, the running total is reset to 0. The IF() function works like IF(<boolean condition>, <then>, <else>).
Here the variable #foo still has the value of the previous row. The value of the current row is assigned in this line:
, #foo := foo
I hope you get the idea, feel free to ask if anything is unclear. Oh, and you don't need those selects in the case...when... parts.

Just guessing... giving the man a fish - might be the wrong fish
SELECT CONCAT("Trans ID ",TransactionID) ID
, DateTime
, Type Activity
, Amount
, 0 Payout
, CASE WHEN Activity = "deposit"
THEN Amount + playerdb.CurrentBalance
WHEN Activity IN("withdraw","bet")
THEN
CASE WHEN Payout >0
THEN Payout + playerdb.CurrentBalance
ELSE Amount - playerdb.CurrentBalance
END
END CurrentBalance
FROM transactiondb
LEFT
JOIN playerdb
ON transactiondb.PlayerID = playerdb.PlayerID
WHERE transactiondb.PlayerID = 10078
UNION ALL
SELECT CONCAT("Bet ID ",BetID)
, DateTime
, "Bet"
, BetAmount
, Payout
, CurrentBalance
FROM betdb
WHERE PlayerID = 10078
ORDER
BY DateTime;

Related

How to display only the second purchase made per account

I have a transactions table which has shows various transactions made by several accounts. Some make only one, others more than that. At the moment the SQL I have prints out the first purchase of each account but i need it to print out the second made by each account
SELECT account_id
, purchase_date as second_purchase
, amount as second_purchase_amount
FROM Transactions t
WHERE purchase_date NOT IN (SELECT MIN(purchase_date)
FROM Transactions m
)
GROUP BY account_id
HAVING purchase_date = MIN(purchase_date);
What needs to change that the second purchase date and amount are chosen? I tried adding in a count for the account_id but it was giving me the wrong value.
You can use variables to assign row numbers and get the 2nd purchase.
SELECT account_id,purchase_Date,amount
FROM (
SELECT account_id
,purchase_date
,amount
--, #rn:=IF(account_id=#a_id and #pdate <> purchase_date,#rn+1,1) as rnum
,case when account_id=#a_id and #pdate <> purchase_date then #rn:=#rn+1
when account_id=#a_id and #pdate=purchase_date then #rn:=#rn
else #rn:=1 end as rnum
, #pdate:=purchase_date
, #a_id:=account_id
FROM Transactions t
CROSS JOIN (SELECT #rn:=0,#a_id:=-1,#pdate:='') r
ORDER BY account_id, purchase_date
) x
WHERE rnum=2
Explanation of how it works:
#rn:=0,#a_id:=-1,#pdate:='' - Declare 3 variables and initialize them, #rn for assigning the row numbers, #a_id to hold the account_id and #pdate to hold the purchase_date.
For the first row (ordered by account_id and purchase_date), account_id and #a_id, #pdate and purchase_date will be compared. As they wouldn't be equal, the when conditions fail and the else part would assign #rn=1. Also, the variable assignment happens after this. #aid and #pdate would be updated to current row's values. For the second row, if they are the same account and on a different date the first when condition will be executed and the #rn will be incremented by 1. If there are ties the second when condition would be executed and the #rn remains the same. You can run the inner query to check how the variables are assigned.
Number the rows and choose RowNumber = 2
select *
from (
select
#rn := case when #account_id = account_id then #rn + 1 else #rn := 1 end as RowNumber,
#account_id := account_id as account_id,
purchase_date
from
(select #rn := 1) x,
(select #acount_id :=account_id as account_id, purchase_date
from Transactions
order by account_id, purchase_date) y
) z
where RowNumber = 2;

adding sequential number to existing data specific to an ID MySQL

New to the forum.I wonder if somebody out there might be able to help with an issue I am having?
I need to add an addition column in a query to show a sequential number 1,2,3...ect but to have it be specific to an existing ID - The s_id based on the order of the s_id. Below show an example of the data I am working with.
'p_id' 's_id'
'327' '569'
'330' '569'
'1205' '569'
'350' '1000'
I am aiming to get it to look like this.
'Seq_no' 'p_id' 's_id'
'1' '327' '569'
'2' '330' '569'
'3' '1205' '569'
'1' '350' '1000'
I have looked at adding an auto increment bur cannot manage to tie it back to the back specifically to the s_id.
Any help would be greatly appreciated.
Best Regards
DSW
You can do it with a self join :
SELECT count(s.sid) as seq_no,t.p_id,t.s_id
FROM YourTable t
JOIN YourTable s
ON(t.s_id = s.s_id AND t.p_id >= s.p_id)
GROUP BY t.p_id,t.s_id
Assuming that you're sorting the dataset by (s_id,p_id) then here's one way (untested):
SELECT x.*
, CASE WHEN #prev = x.s_id THEN #i:=#i+1 ELSE #i:=1 END i
, #prev := s_id prev
FROM my_table x
, (SELECT #prev:=null,#i:=0) vars
ORDER
BY s_id,p_id;
A common approach is using MySQL user defined variables.
In your case you need two of them.
SELECT
#rn := IF(#sameSid = s_id, #rn := #rn + 1 ,
IF(#sameSid := s_id, 1, 1)
) AS Seq_no,
p_id,
s_id
FROM your_table
CROSS JOIN
(
SELECT #sameSid := 0, #rn := 1
) AS var
ORDER BY s_id , p_id

SQL OR clause priority

I have a table with user awards, which can be of various different types.
For example, here are the records for the qualification ID 94:
So as you can see, there are 2 users, one has records for the award type of "average", "min", "max" and "final", the other has the same but no "final" award.
What I want is to get only 1 row per user. If they have an award of type "final" I want that, otherwise I want the "average" one, I don't want "min" or "max" at all.
So as an example, here is the query with just a simple IN clause:
So based on that, what I want the result to be is for the user 34562 I want the row with the "final" award, and for the user 6256 i want the row with their "average" award, since they don't have a "final" record.
I'm sure this should be fairly simple, but i'm failing miserably this morning.
I think I should be able to select the final record, then do a UNION ALL, but I can't seem to work it out in my head. Can anyone point me in the right direction?
I should point out that whilst this is MySQL for me, it needs to be compatible with other database platforms.
Thanks.
An easy way would be to check if an average entry exists in the where clause:
SELECT * FROM Table t
WHERE qualid = 94
and (type = 'average' AND
not exists(SELECT * FROM Table t2
WHERE t.qualid=t2.qualid AND t.userid=t2.userid AND type = 'final')
OR type = 'final')
You can accomplish this using MySQL user defined variables which will be more scalable.
SELECT
t.*
FROM
(
SELECT
*,
IF(#sameUser = userid, #rn := #rn + 1,
IF(#sameUser := userid, #rn := 1,#rn := 1)
) AS row_number
FROM moodle.mdl_bcgt_user_qual_awards
CROSS JOIN (SELECT #sameUser := -1, #rn := 1) AS var
WHERE qualid = 94
AND type IN ('final','average')
ORDER BY userid,
CASE WHEN type = 'final' THEN 0 ELSE 1 END
) AS t
WHERE t.row_number <= 1
ORDER BY t.userid
EDIT:
using NOT EXISTS & UNION ALL
SELECT
*
FROM your_table
WHERE qualid = 94
AND type = 'final'
UNION ALL
SELECT
*
FROM your_table A
WHERE qualid = 94
AND type = 'average'
AND NOT EXISTS (
SELECT 1 FROM your_table B WHERE A.qualid = B.qualid AND B.userid = A.userid AND B.type = 'final'
)
You can try this for sql Server
Select * from (
SELECT *,ROW_NUMBER() over(PARTITION BY type ORDER BY type) rowNo
FROM mdl_bcgt_user_qual_awards
WHERE qualid = 94
AND type in ('final','average')
Order By type
) as t
Where rowNo=1
Hear we have done Order by on type as character meet our requirement if any other text then just add case and sort on the basis of that field.
Here is a pure MySQL solution, which should also be generally ANSI compliant across most RDBMS:
SELECT t1.*
FROM yourTable t1
INNER JOIN
(
SELECT qualid,
CASE WHEN MAX(CASE WHEN type = 'final' THEN 100
WHEN type = 'average' THEN 10
ELSE 1
END) = 100 THEN 'final'
WHEN MAX(CASE WHEN type = 'final' THEN 100
WHEN type = 'average' THEN 10
ELSE 1
END) = 10 THEN 'average'
ELSE NULL
END AS type
FROM yourTable
GROUP BY qualid
) t2
ON t1.qualid = t2.qualid AND
t1.type = t2.type
This query employs a trick, which is to aggregate a sum for each qualid group based on whether final, average, or neither be present. I assign a value of 100 for final, 10 for average, and 1 for neither. This allows us to assign a type to each group.

What are the subquery equivalents of SQL aggregate functions MAX/MIN/AVG/COUNT

Can someone show me how to represent the following SQL statements without the use of aggregate functions?
SELECT COUNT(column) FROM table;
SELECT AVG(column) FROM table;
SELECT MAX(column) FROM table;
SELECT MIN(column) FROM table;
MIN() and MAX() can be done with simple subqueries:
select (select column from table order by column is not null desc, column asc limit 1) as "MIN",
(select column from table order by column is not null desc, column desc limit 1) as "MAX"
COUNT() and AVG() require the use of variables, if you don't allow any aggregations:
select rn as "COUNT", sumcol / rnaas "AVG"
from (select t.*
from (select t.*,
(#rn := #rn + 1) as rn,
(#rna := #rna + if(column is not null, 1, 0)) as rna,
(#sum := #sum + coalesce(column, 0)) as sumcol
from table t cross join
(select #rn := 0, #rna := 0, #sum := 0) const
order by column
) t
order by rn desc
limit 1
) t
This latter formulation only works in MySQL.
EDIT:
The empty table is a challenge. Let's do this with a left outer join:
select cast(coalesce(rn, 0) as int) as "COUNT",
(case when rna > 0 then sumcol / rna end) as "AVG"
from (select 1 as n
) n left outer join
(select t.*
from (select t.*,
(#rn := #rn + 1) as rn,
(#rna := #rna + if(column is not null, 1, 0)) as rna,
(#sum := #sum + coalesce(column, 0)) as sumcol
from table t cross join
(select #rn := 0, #rna := 0, #sum := 0) const
order by column
) t
order by rn desc
limit 1
) t
on n.n = 1;
Notes. This will return 0 for the count if the table is empty. That is correct. If the table is empty, it will return NULL for the average, and that is also correct.
If the table is not empty, but the values are all NULL, then it will also return NULL. The types for the count are always integers, so that should be ok. The type of the average is more problematic, but the variables will return some sort of generic numeric type, which seems compatible in spirit.
min/max can be replaced with something like this:
select t1.pk_column,
t1.some_column
from the_table t1
where t1.some_column < ALL (select t2.some_column
from the_table t2
where t2.pk_column <> t2.pk_column);
For getting the max you need to replace < with >. pk_column is the primary key column of the table and is needed to avoid comparing each row to itself (it doesn't have to be a PK it only needs to be unique)
I don't think there is an alternative for count() or avg() (at least I can't think of one)
I used the_column and the_table because column and table are reserved words
SET #t1=0, #t2=0, #t3=0,#T4=0;
COUNT:
Select #t1:=#t1+1 as CNT from table
order by #t1:=#t1+1 DESC
LIMIT 1
Similar methods could be put together for Avg and max/min using limits...
Still thinking about Min/Max...
Not to supersede the excellent answer from Gordon Linoff, but there's a little more work involved to accurately emulate the AVG(), COUNT(), and SUM() functions. (The answer for the MIN and MAX functions in Gordon's answer are spot on.)
There's a corner case when the table is empty. In order to emulate the SQL aggregate functions, we need our query to return a single row. But at the same time, we need a test of whether or not the table contains at least one row.
Here's a query that is a more precise emulation:
-- create an empty table
CREATE TABLE `foo` (col INT);
-- TRUNCATE TABLE `foo`;
SELECT IF(s.ne IS NULL,0,s.rn) AS `COUNT(*)`
, IF(s.cc>0,s.tc,NULL) AS `SUM(col)`
, IF(s.cc>0,s.tc/s.cc,NULL) AS `AVG(col)`
FROM ( SELECT v.rn
, v.cc
, v.tc
, e.ne
FROM ( SELECT #rn := #rn + 1 AS rn
, #cc := #cc + (t.col IS NOT NULL) AS cc
, #tc := #tc + IFNULL(t.col,0) AS tc
FROM (SELECT #rn := 0, #cc := 0, #tc := 0) c
LEFT
JOIN `foo` t
ON 1=1
) v
LEFT
JOIN (SELECT 1 AS ne FROM `foo` z LIMIT 1) e
ON 1=1
ORDER BY v.rn DESC
LIMIT 1
) s
NOTES:
The purpose of the inline view aliased as e is to give us a way to determine whether or not the table contains any rows. If the table contains at least one row, we'll get a value of 1 returned as column ne (not empty). If the table is empty, that query won't return a row, and e.ne will be NULL, which is something we can test in the outer query.
In order to return a row, so we can return a value, like a 0 for a COUNT, we need to insure that we return at least one row from the inline view v. Since we are guaranteed exactly one row from the inline view aliased as c (which initializes our user defined variables), we'll use that as the "driving" table for a LEFT [OUTER] JOIN operation.
But, if the table is empty, our our row counter (#rn) coming out of v is going to have a value of 1. But we'll deal with that, we have the e.ne we can check to know if the count should really be returned as 0.
In order to calculate the average, we can't divide by the row counter, we have to divide by the number of rows where col was not null. We use the #cc user defined variable to keep track of the count of those rows.
Similarly, for the SUM (and the average) we need to accumulate only the non-NULL values. (If we were to add a NULL, it would turn the whole total to NULL, basically wiping out are accumulation. So, we're going to do a conditional test to check if t.col IS NULL, to avoid accidentally wiping out the accumulation. And our accumulator is going to be a 0 if there aren't any rows that are not null. But that's not a problem, because we'll make sure we check our #cc to see if there were any rows that were included. We're going to need to check it anyway, to avoid a "divide by zero" issue.
To test, run against the empty table foo. It will return a count of 0, and NULL for SUM and AVG, equivalent to the result we get from:
SELECT COUNT(*), SUM(col), AVG(col) FROM foo;
We can also test the query against a table containing only NULL values for col:
INSERT INTO `foo` (col) VALUES (NULL);
As well as some non-NULL values:
INSERT INTO `foo` (col) VALUES (2),(3),(5),(7),(11),(13),(17),(19);
And compare the results of the two queries.
This essentially the same as the answer from Gordon Linoff, with just a little more precision to work around the corner cases of NULL values and the empty table.

MySQL increment user variable when value changes

I have a table consisting of groups of, for example, five rows each. Each row in each group possesses a date value unique to that group.
What I want to do in my query, is go through the table, and increment a user variable (#count) when this date value changes. That's to say, #count should equal the group count, rather than the row count.
My current query looks like this, in case you're wondering:
SELECT #row := #row +1 AS rownum, date
FROM ( SELECT #row := 0 ) r, stats
Thanks a lot.
What about something like this?
SELECT
(CASE WHEN #date <> date THEN #row := #row +1 ELSE #row END) AS rownum,
#date:= date,
date
FROM ( SELECT #row := 0, #date := NOW() ) r, stats
You don't need a user variable to answer the query that you are doing. Is there a reason you want to use the user variable (for example, to emulate a ranking function?)
If not:
-- how many groups are there?
select count(distinct date) distinct_groups from table;
-- each group and count of rows in the group
select date, count(*) from table group by date;
-- if you want the Nth row from each group, assuming you have an auto_increment called id:
select *
from table
join ( select date, max(id) id
from table
group by date
) sq
on table.id = sq.id