MySQL add balance from previous rows - mysql

I’ve tried a few things I’ve seen on here but it doesn’t work in my case, the balance on each row seems to duplicate.
Anyway I have a table that holds mortgage transactions, that table has a Column that stores an interest added value or a payment value.
So I might have:
Balance: 100,000
Interest added 100 - balance 100,100
Payment made -500 - balance 99,600
Interest added 100 - balance 99,700
Payment made -500 - balance 99,200
What I’m looking for is a query to pull all of these in date order newest first and summing the balance in a column depending on whether it has interest or payment (the one that doesn’t will be null) so at the end of the rows it will have the current liability
I can’t remember what the query I tried was but it ended up duplicating rows and the balance was weird
Sample structure and data:
CREATE TABLE account(
id int not null primary key auto_increment,
account_name varchar(50),
starting_balance float(10,6)
);
CREATE TABLE account_transaction(
id int not null primary key auto_increment,
account_id int NOT NULL,
date datetime,
interest_amount int DEFAULT null,
payment_amount float(10,6) DEFAULT NULL
);
INSERT INTO account (account_name,starting_balance) VALUES('Test Account','100000');
INSERT INTO account_transaction (account_id,date,interest_amount,payment_amount) VALUES(1,'2020-10-01 00:00:00',300,null);
INSERT INTO account_transaction (account_id,date,interest_amount,payment_amount) VALUES(1,'2020-10-01 00:00:00',null,-500);
INSERT INTO account_transaction (account_id,date,interest_amount,payment_amount) VALUES(1,'2020-11-01 00:00:00',300,null);
INSERT INTO account_transaction (account_id,date,interest_amount,payment_amount) VALUES(1,'2020-11-05 00:00:00',-500,null);
So interest will be added on to the rolling balance, and the starting balance is stored against the account - if we have to have a transaction added for this then ok. Then when a payment is added it can be either negative or positive to decrease the balance moving to each row.
So above example i'd expect to see something along the lines of:
I hope this makes it clearer

WITH
starting_dates AS ( SELECT id account_id, MIN(`date`) startdate
FROM account_transaction
GROUP BY id ),
combine AS ( SELECT 0 id,
starting_dates.account_id,
starting_dates.startdate `date`,
0 interest_amount,
account.starting_balance payment_amount
FROM account
JOIN starting_dates ON account.id = starting_dates.account_id
UNION ALL
SELECT id,
account_id,
`date`,
interest_amount,
payment_amount
FROM account_transaction )
SELECT DATE(`date`) `Date`,
CASE WHEN interest_amount = 0 THEN 'Balance Brought Forward'
WHEN payment_amount IS NULL THEN 'Interest Added'
WHEN interest_amount IS NULL THEN 'Payment Added'
ELSE 'Unknown transaction type'
END `Desc`,
CASE WHEN interest_amount = 0 THEN ''
ELSE COALESCE(interest_amount, 0)
END Interest,
COALESCE(payment_amount, 0) Payment,
SUM(COALESCE(payment_amount, 0) + COALESCE(interest_amount, 0))
OVER (PARTITION BY account_id ORDER BY id) Balance
FROM combine
ORDER BY id;
fiddle
PS. Source data provided (row with id=4) was altered according to desired output provided. Source structure was altered, FLOAT(10,6) which is not compatible with provided values was replaced with DECIMAL.
PPS. The presence of more than one account is allowed.

Related

Need to find Three most expensive path on average having code 'A' with delivery in May 2021

Statement:
Need to find Three most expensive path(from_to_ in table) on average that code (with_ in table) 'A' with delivery in May 2021? If two ties then include both.
Schema:
'observed_on', date,
'from_', varchar(3),
'to_', varchar(3),
'from_to_', varchar(8),
'with_', varchar(3),
'cart_no', varchar(8),
'deliver_on', date,
'd_charge', double,
Sample data:
click to view
Solution I tried:
SELECT
from_to_
,avg_price
FROM
(
SELECT
from_to_
,ROUND(AVG(d_charge),2) AS avg_price
,DENSE_RANK() OVER(ORDER BY ROUND(AVG(d_charge),2) DESC) rank_by_avgp
FROM
(
SELECT
*
FROM DELIVERY
WHERE deliver_on BETWEEN '2021-05-01' AND '2021-05-30'
AND with_ = 'A'
) AS A
GROUP BY from_to_
) AS bb
WHERE bb.rank_by_avgp <=3;
I know it's a workaround so I am looking for a better solution
#nishant, you have a quite poorly asked question here.
The issues with your question
The sample data, you reference, is a picture? Why not give text values or even better the DML statements to set the example up.
In the referenced dataset(picture) all the DELIVERY.deliver_on date values are before 2010. So the conditions you have here deliver_on BETWEEN '2021-05-01' AND '2021-05-30' will just not return anything for the example data.
If you say
I know it's a workaround so I am looking for a better solution
then based on what? It is a workaround for what? It looks to be producing correct results, so what is the problem with it? Do you want it to perform better or what?
You are not specifing the DB version. The different MySQL versions can have different solutions.
One possible solution
The example dataset setup:
DDL
CREATE TABLE DELIVERY (
observed_on DATE
, from_ VARCHAR(3)
, to_ VARCHAR(3)
, from_to_ VARCHAR(8)
, with_ VARCHAR(3)
, cart_no VARCHAR(8)
, deliver_on DATE
, d_charge DOUBLE
);
DML
INSERT INTO DELIVERY VALUES ('2012-01-19','Aus','Nzl','AusNzl','A','2118','2021-04-19',82.3);
INSERT INTO DELIVERY VALUES ('2012-01-19','Aus','Nzl','AusNzl','A','2118','2021-05-19',82.3);
INSERT INTO DELIVERY VALUES ('2012-01-19','Aus','Nzl','AusNzl','A','2118','2021-05-19',82.3);
INSERT INTO DELIVERY VALUES ('2013-01-19','Ind','Sla','IndSla','B','2233','2021-05-19',70.32);
INSERT INTO DELIVERY VALUES ('2013-01-19','Ind','Sla','IndSla','A','2233','2021-05-19',70.32);
INSERT INTO DELIVERY VALUES ('2013-01-19','Eur','Usa','EurUsa','C','2434','2021-05-19',67.53);
INSERT INTO DELIVERY VALUES ('2013-01-19','Eur','Usa','EurUsa','A','2434','2021-05-19',67.53);
INSERT INTO DELIVERY VALUES ('2013-01-19','Xyz','Usa','XyzUsa','A','2434','2021-05-19',67.53);
INSERT INTO DELIVERY VALUES ('2013-01-19','Xyz','Sla','XyzSla','A','2434','2021-05-19',67.51);
INSERT INTO DELIVERY VALUES ('2012-01-19','Aus','Nzl','AusNzl','A','2323','2021-05-19',82.3);
INSERT INTO DELIVERY VALUES ('2012-01-19','Aus','Nzl','AusNzl','A','2118','2021-06-19',82.3);
QUERY
SELECT from_to_
, avg_d_charge
, denserank_avg_d_charge
FROM /*SUB_to_calculate_the_denserank*/
(
SELECT from_to_
, ROUND(avg_d_charge, 2) AS avg_d_charge
, DENSE_RANK() OVER (
ORDER BY ROUND(avg_d_charge, 2) DESC
) denserank_avg_d_charge /*dense ranking*/
FROM /*SUB_to_calculate_the_averages*/
(
SELECT from_to_
, ROW_NUMBER() OVER (PARTITION BY from_to_) AS rownumber /*To filter for only one row per from_to_.*/
, AVG(d_charge) OVER (PARTITION BY from_to_) AS avg_d_charge /*Average caldulation*/
FROM /*DELIVERY*/
DELIVERY
WHERE 1 = 1
AND with_ = 'A' /* The "code" filter*/
AND DATE_SUB(deliver_on, INTERVAL DAYOFMONTH(deliver_on) - 1 DAY) = '2021-05-01' /* The 2021-05 filter*/
) SUB_to_calculate_the_averages
WHERE 1 = 1
AND rownumber = 1
) SUB_to_calculate_the_denserank
WHERE 1 = 1
AND denserank_avg_d_charge < 4;
The only main difference here from your solution is then that I do not use the aggegate GROUP BY here, only analytical functions. I prefer this quite often as it allows to carry later other attributes through the query without the need to apply the aggregate functions on them etc. But in the end then this comes down to performance and the requirements what/how should be done.

How to create calculation in MySQL between date and price

I'm want to calculate the date to count days and calculate with price or custom price in MySQL.
This is booking table
CREATE TABLE tbl_booking(
booking_id int(11) NOT NULL AUTO_INCREMENT,
user_id int(11),
comp_id int(11),
register_number varchar(100),
booking_status int(11),
pick_date date,
drop_date date,)
This is vehicle table
CREATE TABLE tbl_vehicle(
register_number varchar(100) NOT null,
vehicle_model varchar(255),
vehicle_type varchar(255),
vehicle_price varchar(10));
So, like this:
totalprice = (pick_date - drop_date) * vehicle_price
Join two tables on common column which seems to be register_number then use datediff to calculate the difference in days between two dates and multiply it by price to get total price for each booking:
select
b.*,
datediff(b.drop_date, b.pick_date) * v.price as total_price
from tbl_booking b
inner join tbl_vehicle v on
b.register_number = v.register_number
Remember that you should substract later date from an earlier one (drop - pick) instead of (pick - drop) since this will give you a negative number.
Use below :
DATEDIFF(pick_date , drop_date) * price
DATEDIFF will give you day different between two dates.
SELECT DATEDIFF("2017-06-25", "2017-06-15");
Output will be : 10
As you price is in another table hope you will be able to join and calculate.
You can convert the date field data into days, then subtract them, and multiply with price. Please note that the syntax can vary among the different implementations of SQL. With the correct MySQL syntax this would be:
SELECT (TO_DAYS(drop_date) - TO_DAYS(pick_date)) * price FROM tbl_booking;
Also note that according to your code, tbl_booking does not contain the price field, so you'll probably need to join with another table as well.

Handle expiry credits in a transaction table?

Consider table Credits
CREATE TABLE `Credits` (
`UserID` int(11) unsigned NOT NULL DEFAULT '0',
`Amount` int(11) NOT NULL DEFAULT '0',
`Created` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
`Expire` datetime NOT NULL DEFAULT '9999-12-31 23:59:59'
)
With data:
UserID Amount Created Expire
1 10 2016-01-14 2016-05-30
1 -2 2016-02-04 9999-12-31
1 3 2016-06-01 2016-09-30
Without the Expiry handing, to get the current amount of a user, it can be handled by a simple select
SELECT SUM(Amount) FROM Credits WHERE UserID = 1;
Now, I need to write a SELECT query, with an input parameter of date, and able to get the usable amount of credits at that time, like the following..
At..
2016-01-15 => 10
2016-02-06 => 8
2016-05-31 => 0
2016-06-03 => 3
Is it possible with just the above schema? Or I need to add extra field?
SQLFiddle: http://sqlfiddle.com/#!9/3a52b/3
When you make transaction with negative Amount, you should find corresponding transaction with positive Amount and set same expiration date. So that, you store expiration date of credits you spend. Your example table will look like this:
UserID Amount Created Expire
1 10 2016-01-14 2016-05-30
1 -2 2016-02-04 2016-05-30
1 3 2016-06-01 2016-09-30
That makes query for balance on any particular date look as following:
SELECT SUM(Amount) FROM Credits WHERE UserID = 1 and #date between Created and Expire;
Notice, you may have to split one transaction with negative amount to cover credits with different expiration date. For example, you have following table:
UserID Amount Created Expire
1 10 2016-01-14 2016-05-30
1 10 2016-02-04 2016-06-30
and you want to make transaction with Amount=-15, then you need to make two records:
UserID Amount Created Expire
1 -10 2016-04-26 2016-05-30
1 -5 2016-04-26 2016-06-30
To find out not yet spend or expired credits along with their expiration date, you can use following query:
select sum(Amount) as Amount, Expire
from Credits
where UserID = 1 and curdate() <= Expire
group by Expire
having sum(Amount) > 0
order by Expire
There's something of a design trap here. If someone has credits that expire on different dates, you need some sort of logic to work out exactly which credits were consumed - it matters, because the expiry could produce different balance outcomes depending on which credits were selected. If expenditures can be refunded, this becomes even messier.
Therefore, I suggest breaking this up into two tables. I have also taken the liberty of making some fields NOT NULL without DEFAULT - that is, making them mandatory to supply on INSERT - and dropping the identifier quoting for clarity.
CREATE TABLE Transactions (
TransactionID int(11) unsigned NOT NULL AUTO_INCREMENT,
Timestamp datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
Caption nvarchar(50) NOT NULL,
PRIMARY KEY (TransactionID)
)
CREATE TABLE Credits (
UserID int(11) unsigned NOT NULL,
Amount int(11) NOT NULL,
Expire datetime NOT NULL DEFAULT '9999-12-31 23:59:59',
ProducingTransactionID int(11) unsigned NOT NULL REFERENCES ,
ConsumingTransactionID int(11) unsigned NULL,
PRIMARY KEY (UserID, Amount, Expire, ProducingTransaction),
INDEX (UserID, ConsumingTransaction, Expire),
FOREIGN KEY (SourceTransactionID) REFERENCES Transactions (TransactionID),
FOREIGN KEY (SinkTransactionID) REFERENCES Transactions (TransactionID),
)
The idea here is that transactions are forced to be explicit about exactly which credits they are affecting, and these explicit choices are recorded. Credits cannot be added without a link back to the SourceTransactionID they are sourced from; when credits are used, the SinkTransactionID is simply populated with a link to the transaction in which they are used, and only credits where SinkTransactionID is null are potentially available balance.
Note that a Credits row should not be updated in any way other than to set SinkTransactionID, and therefore cannot be partially consumed by each of multiple transactions - instead, a transaction that wants to sink only part of a Credits row needs to insert a new Credits row containing the "change" on the partially used row and referencing itself as the source.
Point-in-time balance queries become slightly more complex, since now you have to join Transactions to filter out transactions that occurred after the intended point-in-time.
SELECT SUM(t.Amount)
from (select case amount
when current_date>expiry_date then 1
else 0
end as amount
FROM Credits WHERE UserID = 1 AND Expire <= CURDATE()
)t.
SELECT
CASE WHEN SUM(c.amount) > 0 then SUM(c.amount) else 0 end as total_amount
FROM Credits c
where c.userId = 1 and c.Expire <= CURDATE() -- or any date
Sqlfiddle demo.

How to sort a list of people by birth and death dates when data are incomplete

I have a list of people who may or may not have a birth date and/or a death date. I want to be able to sort them meaningfully - a subjective term - by birth date.
BUT - if they don't have a birth date but they to have a death date, I want to have them collated into the list proximal to other people who died then.
I recognize that this is not a discrete operation - there is ambiguity about where someone should go when their birth date is missing. But I'm looking for something that is a good approximation, most of the time.
Here's an example list of what I'd like:
Alice 1800 1830
Bob 1805 1845
Carol 1847
Don 1820 1846
Esther 1825 1860
In this example, I'd be happy with Carol appearing either before or after Don - that's the ambiguity I'm prepared to accept. The important outcome is that Carol is sorted in the list relative to her death date as a death date, not sorting the death dates in with the birth dates.
What doesn't work is if I coalesce or otherwise map birth and death dates together. For example, ORDER BY birth_date, death_date would put Carol after Esther, which is way out of place by my thinking.
I think you're going to have to calculate an average age people end up living (for those having both birth and death dates). And either subtract them from death date or add them to birth date for people who don't have the other one.
Doing this in one query may not be efficient, and perhaps ugly because mysql doesn't have windowing functions. You may be better of precalculating the average living age beforehand. But let's try to do it in one query anyway:
SELECT name, birth_date, death_date
FROM people
ORDER BY COALESCE(
birth_date,
DATE_SUB(death_date, INTERVAL (
SELECT AVG(DATEDIFF(death_date, birth_date))
FROM people
WHERE birth_date IS NOT NULL AND death_date IS NOT NULL
) DAY)
)
N.B.: I've tried with a larger dataset, and it is not working completely as I'd expect.
Try with this query (it needs an id primary key column):
SELECT * FROM people p
ORDER BY (
CASE WHEN birth IS NOT NULL THEN (
SELECT ord FROM (
SELECT id, #rnum := #rnum + 1 AS ord
FROM people, (SELECT #rnum := 0) r1
ORDER BY (CASE WHEN birth IS NOT NULL THEN 0 ELSE 1 END), birth, death
) o1
WHERE id = p.id
) ELSE (
SELECT ord FROM (
SELECT id, #rnum := #rnum + 1 AS ord
FROM people, (SELECT #rnum := 0) r2
ORDER BY (CASE WHEN death IS NOT NULL THEN 0 ELSE 1 END), death, birth
) o2
WHERE id = p.id
)
END)
;
What I've done is, basically, to sort the dataset two times, once by birth date and then by death date. Then I've used these two sorted lists to assign the final order to the original dataset, picking the place from the birth-sorted list at first, and using the place from the death-sorted list when a row has no birth date.
Here's a few problems with that query:
I didn't run it against lots of datasets, so I can't really guarantee it will work with any dataset;
I didn't check its performance, so it could be quite slow on large datasets.
This is the table I've used to write it, tested with MySQL 5.6.21 (I can't understand why, but SQL Fiddle is rejecting my scripts with a Create script error, so I can't provide you with a live example).
Table creation:
CREATE TABLE `people` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`name` VARCHAR(50) NOT NULL,
`birth` INT(11) NULL DEFAULT NULL,
`death` INT(11) NULL DEFAULT NULL,
PRIMARY KEY (`id`)
);
Data (I actually slightly changed yours):
INSERT INTO `people` (`name`, `birth`, `death`) VALUES ('Alice', 1800, NULL);
INSERT INTO `people` (`name`, `birth`, `death`) VALUES ('Bob', 1805, 1845);
INSERT INTO `people` (`name`, `birth`, `death`) VALUES ('Carol', NULL, 1847);
INSERT INTO `people` (`name`, `birth`, `death`) VALUES ('Don', 1820, 1846);
INSERT INTO `people` (`name`, `birth`, `death`) VALUES ('Esther', 1815, 1860);
you can use a subquery to pick a suitable birthdate for sorting purposes
and then a union to join with the records with a birthdate
for example:
select d1.name, null as birthdate, d1.deathdate, max(d2.birthdate) sort from
d as d1, d as d2
where d1.birthdate is null and d2.deathdate <=d1.deathdate
group by d1.name, d1.deathdate
union all
select name, birthdate, deathdate, birthdate from d
where birthdate is not null
order by 4
http://sqlfiddle.com/#!9/2d91c/1
Not sure if this will work, but worth a try (I can't test this on MySQL) so trying to guess:
order by case birth_date when null then death_date else birth_date end case

How to avoid duplicate registrations in MySQL

I wonder if it is possible to restrain users to insert duplicate registration records.
For example some team is registered from 5.1.2009 - 31.12.2009. Then someone registers the same team for 5.2.2009 - 31.12.2009.
Usually the end_date is not an issue, but start_date should not be between existing records start and end date
CREATE TABLE IF NOT EXISTS `ejl_team_registration` (
`id` int(11) NOT NULL auto_increment,
`team_id` int(11) NOT NULL,
`league_id` smallint(6) NOT NULL,
`start_date` date NOT NULL,
`end_date` date NOT NULL,
PRIMARY KEY (`team_id`,`league_id`,`start_date`),
UNIQUE KEY `id` (`id`)
);
I would check it in the code df the program, not the database.
If you want to do this in database, you can probably use pre-insert trigger that will fail if there are any conflicting records.
This is a classic problem of time overlapping. Say you want to register a certain team for the period of A (start_date) until B (end_date).
This should NOT be allowed in next cases:
the same team is already registered, so that the registered period is completely inside the A-B period (start_date >= A and end_date <= B)
the same team is already registered at point A (start_date <= A and end_date >= A)
the same team is already registered at point B (start_date <= B and end_date >= B)
In those cases, registering would cause time overlap. In any other it would not, so you're free to register.
In sql, the check would be:
select count(*) from ejl_team_registration
where (team_id=123 and league_id=45)
and ((start_date>=A and end_date<=B)
or (start_date<=A and end_date>=A)
or (start_date<=B and end_date>=B)
);
... with of course real values for the team_id, league_id, A and B.
If the query returns anything else than 0, the team is already registered and registering again would cause time overlap.
To demonstrate this, let's populate the table:
insert into ejl_team_registration (id, team_id, league_id, start_date, end_date)
values (1, 123, 45, '2007-01-01', '2007-12-31')
, (2, 123, 45, '2008-01-01', '2008-12-31')
, (3, 123, 45, '20010-01-01', '2010-12-31');
Let's check if we could register team 123 in leage 45 between '2009-02-03' and '2009-12-31':
select count(*) from ejl_team_registration
where (team_id=123 and league_id=45)
and ((start_date<='2009-02-03' and end_date>='2009-12-31')
or (start_date<='2009-03-31' and end_date>='2009-03-02')
or (start_date<='2009-12-31' and end_date>='2009-12-31')
);
The result is 0, so we can register freely.
Registering between e.g. '2009-02-03' and '2011-12-31' would not be possible.
I'll leave checking other values for you as a practice.
PS: You mentioned the end date is usually not an issue. As a matter of fact it is, since inserting an entry with invalid end date would cause overlapping as well.
Before doing your INSERT, do a SELECT to check.
SELECT COUNT(*) FROM `ejl_team_registration`
WHERE `team_id` = [[myTeamId]] AND `league_id` = [[myLeagueId]]
AND `start_date` <= NOW()
AND `end_date` >= NOW()
If that returns more than 0, then don't insert.