EXPLAIN
Let's say we have this transaction table :
+----+-------------+--------+----------------+----------------+---------------------+
| id | id_customer | amount | id_where_trans | id_money_from | date_trans |
+----+-------------+--------+----------------+----------------+---------------------+
| 1 | 10 | 10 | 100 | NULL | 2015-07-20 10:20:30 |
| 2 | 10 | -2 | 100 | NULL | 2015-07-21 09:10:11 |
| 3 | 10 | 7 | 120 | NULL | 2015-07-24 18:22:25 |
| 4 | 10 | -11 | 120 | here the magic | 2015-07-24 18:22:26 |
+----+-------------+--------+----------------+----------------+---------------------+
Read this in "human" language:
id 1 => Customer Alessandro (id_customer 10) charge 10€ on his account in shop A (id_where trans = 100)
id 2 => Customer Alessandro spent 2€ on Shop A
id 3 => Customer Alessandro charge 7€ on his account in shop B
id 4 => Customer ALessandro spend 11€ in shop B.
At the end of period (month, for example), Shop B need to receive 8€ (+10-2) from another shop, in our example shop A.
/end of human read.
You imagine that insert is a simple (for id 4, for example):
INSERT INTO transaction (id_customer, amount, id_where_trans, date) VALUES (10,-11,120,2015-07-24 18:22:26)
GOAL
My goal is set - during the insert, and if possible / simpler via PHP - the SQL splitted in two or more SELECT:
INSERT INTO transaction (id_customer, amount, id_where_trans, id_money_from, date) VALUES (10,-8,120,100,2015-07-24 18:22:26)
INSERT INTO transaction (id_customer, amount, id_where_trans, id_money_from, date) VALUES (10,-3,120,NULL,2015-07-24 18:22:27)
+----+-------------+--------+----------------+------------+---------------------+
| id | id_customer | amount | id_where_trans | id_money_from | date |
+----+-------------+--------+----------------+------------+---------------------+
| 1 | 10 | 10 | 100 | NULL | 2015-07-20 10:20:30 |
| 2 | 10 | -2 | 100 | NULL | 2015-07-21 09:10:11 |
| 3 | 10 | 7 | 120 | NULL | 2015-07-24 18:22:25 |
| 4 | 10 | -8 | 120 | 100 | 2015-07-24 18:22:26 |
| 5 | 10 | -3 | 120 | NULL | 2015-07-24 18:22:27 |
+----+-------------+--------+----------------+------------+---------------------+
If I can get a table with that informations, I can run a query that make my final job correctly.
Basically, I need to calculate the id_shop where money are taken for discharge (in this case, -8 from 100, that made previous charge, and so is NOT NULL, and -3 from 120,itself, THIS IS NULL).
Please note that FIRST I consume / discharge the previous charge (id 4 now has id_money_from 100), AFTER I will consume / discharge others amounts (id 5 in effect is NULL, because that -3 has taken from id 3)
ASSUMPTION / MANDATARY
1) id are INCREMENTAL, no concurrency. Date are INCREMENTAL, date of id 4 is >= of id 3 and so on.
2) If amount of last recharge is made from same id where transaction is done (see id 1 and id 2) insert NULL (I need mandatary NULL)
3) First charge made, first need to be zero-ed, and so on.
MY LOGIC / PSEUDOCODE
1) If a transaction is negative, make a recursive getting LAST positive charge which, summed to the negative susequential, is not zero.
getted this charge, if id_where_trans==id_money_from we need insert, simply insert, with NULL
INSERT INTO transaction (id_customer, amount, id_where_trans, id_money_from, date) VALUES (10,-2,120,NULL,2015-07-24 18:22:26)
Recursive start here
If this amount is <= of last recharge, walking in database and split negative in two or more id_where_trans (problaby in real scenario this will be impossible, but I need to think that amount from discharge (id_money_from) could be splitted by 2, 3, x id_where_trans). For example, if SHOP A charge +1, Shop B charge +1, Shop C charge +1 and SHOP D DISCHARGE -3, we need to insert 3 different rows.
$amount_to_discharge = x;
$last_positive = $query->("SELECT * FROM transaction WHERE "); // make the SUB-SUM
if ($last_positive['amount'] >= $amount_to_discharge) {
$query->("INSERT INTO......");
} else {
// start recursive
$sql = "SELECT * FROM AMOUNT WHERE amount > 0 AND id < $last_positive['id']";
$new_search = $query->sql($sql);
// how implement correctly that recursive?
}
Thank you guy. Let me know if you need others explain!
Related
I have a table prices with where I store more times in a day more values referred to a customer like this:
Table prices:
| id | customer_id | value_1 | value_2 | value_3 | created_at |
-------------------------------------------------------------------------
| 1 | 10 | 12345 | 122 | 10 | 2021-08-11 10:12:40 |
| 2 | 10 | 22222 | 222 | 22 | 2021-08-11 23:56:20 |
| 3 | 12 | 44444 | 444 | 44 | 2021-08-12 08:12:10 |
| 4 | 10 | 55555 | 555 | 55 | 2021-08-13 14:11:20 |
| 5 | 10 | 66666 | 666 | 66 | 2021-08-13 15:15:30 |
| 6 | 10 | 77777 | 777 | 77 | 2021-08-13 16:12:50 |
I have some filters on that table to retrieve only records with date greater than X and/or lower than Y, sort records by value_1 or value_2, etc...
With that filters I have to take only 1 record for each day of a customer specified.
I'm able to get the record with the highest value_1 for example, by using sql function max() and group by date.
// Init query
$query = Price::query();
// Take the greatest value of value1
$query = $query->selectRaw(
'max(value_1) as highest_value_1, ' .
'date(created_at) as date'
);
// If defined, add a greater or equals
if ($from) $query->where("created_at", ">=", $from);
// If defined add a lower or equals
if ($to) $query->where("created_at", "<=", $to);
// Get results for current customer only, grupping by date and ordering it
$query = $query->where('customer_id', $id)->groupBy('date')
->orderBy('date', 'DESC');
// Fetch records
$records = $query->get();
But now I would like to have only the last record for each day of a customer specified.
I need an eloquent/sql solution because the date range to search may be large and the table has a lot of records.
How can I archive that?
Thanks
Not a complete solution (no customer filter nor laravel use), but I would use something like that in pure sql :
SELECT
CONCAT(EXTRACT(YEAR_MONTH FROM created_at),EXTRACT(DAY FROM created_at)) AS day,
MAX(created_at)
FROM mytable
GROUP BY day;
Of course, you may use other function to group by day (like string regexp or substr).
In credits table I have
(id, user_id, process, amount, date_add, date_exp, date_redeemed, remark)
SELECT * FROM credits WHERE user_id = 2;
+----+---------+---------+--------+------------+------------+---------------+----------+
| id | user_id | process | amount | date_add | date_exp | date_redeemed | remark |
+----+---------+---------+--------+------------+------------+---------------+----------+
| 22 | 2 | Add | 200.00 | 2018-01-01 | 2019-01-01 | | Credit1 |
| 23 | 2 | Add | 200.00 | 2018-03-31 | 2019-03-31 | | Credit2 |
| 24 | 2 | Deduct | 200.00 | | | 2018-04-28 | Redeemed |
| 25 | 2 | Add | 200.00 | 2018-07-11 | 2018-10-11 | | Campaign |
| 26 | 2 | Deduct | 50.00 | | | 2018-08-30 | Redeemed |
| 27 | 2 | Add | 200.00 | 2018-10-01 | 2019-09-30 | | Credit3 |
| 28 | 2 | Deduct | 198.55 | | | 2018-10-20 | Redeemed |
+----+---------+---------+--------+------------+------------+---------------+----------+
The following query I wrote will only calculate the balance, but I don't know whether the credit is expired and used before expired.
SELECT
u.id,
email,
CONCAT(first_name, ' ', last_name) AS name,
type,
(CASE
WHEN (SUM(amount) IS NULL) THEN 0.00
ELSE CASE
WHEN
(SUM(CASE
WHEN process = 'Add' THEN amount
END) - SUM(CASE
WHEN process = 'Deduct' THEN amount
END)) IS NULL
THEN
SUM(CASE
WHEN process = 'Add' THEN amount
END)
ELSE SUM(CASE
WHEN process = 'Add' THEN amount
END) - SUM(CASE
WHEN process = 'Deduct' THEN amount
END)
END
END) AS balance
FROM
users u
LEFT JOIN
credits c ON u.id = c.user_id
GROUP BY u.id;
Or I am doing it in the wrong way? Maybe I should have done the calculation in my backend instead of SQL?
EDIT 1:
I want to calculate the balance of every user's e-wallet, but the credit will expire,
IF it was expired AND not redeemed then exclude from the balance
ELSE IF used before expired AND redeem amount < expire amount
THEN (balance - (expire amount - redeem amount))
ELSE IF used before expired AND redeem amount > expire amount
THEN the usable balance will be deducted as expire amount is not enough to deduct redeemed amount
EDIT 2:
The query above will output 351.45, my expected output is 201.45. Which will not calculate the redemption on 2018-08-30 as the redeem amount is lower than the expired amount
EDIT 3:
User table:
+----+------------+-----------+----------+----------------+----------+
| id | first_name | last_name | type | email | password |
+----+------------+-----------+----------+----------------+----------+
| 2 | Test | Oyster | Employee | test#gmail.com | NULL |
+----+------------+-----------+----------+----------------+----------+
My output:
+----+----------------+-------------+----------+---------+
| id | email | name | type | balance |
+----+----------------+-------------+----------+---------+
| 2 | test#gmail.com | Test Oyster | Employee | 351.45 |
+----+----------------+-------------+----------+---------+
Expected output:
total (200+200+200) 600
redeemed amount 448.55 (200+50+198.55)
Remaining balance is 151.45
+----+----------------+-------------+----------+---------+
| id | email | name | type | balance |
+----+----------------+-------------+----------+---------+
| 2 | test#gmail.com | Test Oyster | Employee | 151.45 |
+----+----------------+-------------+----------+---------+
There are basic structural problems with your current table. So I would propose some alterations to the table structure and subsequently application code. Table structures for Wallet systems can be very detailed; but I would suggest minimum possible changes here. I am not suggesting that it would be ideal way; but it should work. Initially, I will layout some of the problems with the current approach.
Problems:
What if there are multiple Credits available which are not yet expired ?
Out of these available Credits, some may actually have been utilized already, but not yet expired. How do we ignore them for available balance ?
Also, some may have been partially utilized. How do we account for partial utilization ?
There may be a scenario where a redemption amount spans across multiple unexpired credits. Some may get partially utilized; while some may get fully utilized.
General Practice:
We generally follow FIFO (First In First Out) approach, to give maximum benefit to the customer. So the older credits (which has higher chance of getting expired without utilization) gets utilized first.
In order to follow FIFO, we will have to effectively use a Looping technique in the query/application code every-time, in order to compute basic things, such as, "Available Wallet Balance", "Expired and Underutilized Credit", etc. Writing a query for this will be cumbersome and possibly inefficient at bigger scales
Solution:
We can add one more column amount_redeemed in your current table. It basically represent the amount which has already been redeemed against a specific credit.
ALTER TABLE credits ADD COLUMN amount_redeemed DECIMAL (8,2);
So, a filled table would look something like below:
+----+---------+---------+--------+-----------------+------------+---------------+---------------+----------+
| id | user_id | process | amount | amount_redeemed | date_add | date_exp | date_redeemed | remark |
+----+---------+---------+--------+-----------------+------------+---------------+---------------+----------+
| 22 | 2 | Add | 200.00 | 200.00 | 2018-01-01 | 2019-01-01 | | Credit1 |
| 23 | 2 | Add | 200.00 | 200.00 | 2018-03-31 | 2019-03-31 | | Credit2 |
| 24 | 2 | Deduct | 200.00 | | | | 2018-04-28 | Redeemed |
| 25 | 2 | Add | 200.00 | 0.00 | 2018-07-11 | 2018-10-11 | | Campaign |
| 26 | 2 | Deduct | 50.00 | | | | 2018-08-30 | Redeemed |
| 27 | 2 | Add | 200.00 | 48.55 | 2018-10-01 | 2019-09-30 | | Credit3 |
| 28 | 2 | Deduct | 198.55 | | | | 2018-10-20 | Redeemed |
+----+---------+---------+--------+-----------------+------------+---------------+---------------+----------+
Notice that the amount_redeemed against Credit of id = 25 is 0.00, using FIFO approach. It got a chance for redemption on 2018-10-20, but by that time, it has expired already (date_exp = 2018-10-11)
So, now once we have this setup, you can do the following things in your application code:
Populate amount_redeemed value in existing rows in the table:
This will be a one time activity. For this, formulating a single query would be difficult (that is why we are here in the first place). So I would suggest you to do it one time in your application code (eg: PHP), using Loops and FIFO approach. Look at Point 3 below, to get an idea of how to do it in application code.
Get Current Available Balance:
Query for this becomes trivial now, as we just need to calculate Sum of amount - amount_redeemed for all Add process, which are not yet expired.
SELECT SUM(amount - amount_redeemed) AS total_available_credit
FROM credits
WHERE process = 'Add' AND
date_exp > CURDATE() AND
user_id = 2
Update amount_redeemed at the time of redemption:
In this, you can firstly get all available Credits, which has amount available for redemption, and not yet expired.
SELECT id, (amount - amount_redeemed) AS available_credit
FROM credits
WHERE process = 'Add' AND
date_exp > CURDATE() AND
user_id = 2 AND
amount - amount_redeemed > 0
ORDER BY id
Now, we can loop over the above query results, and utilize the amount accordingly
// PHP code example
// amount to redeem
$amount_to_redeem = 100;
// Map storing amount_redeemed against id
$amount_redeemed_map = array();
foreach ($rows as $row) {
// Calculate the amount that can be used against a specific credit
// It will be the minimum of available credit and amount left to redeem
$amount_redeemed = min($row['available_credit'], $amount_to_redeem);
// Populate the map
$amount_redeemed_map[$row['id']] = $amount_redeemed;
// Adjust the amount_to_redeem
$amount_to_redeem -= $amount_redeemed;
// If no more amount_to_redeem, we can finish the loop
if ($amount_to_redeem == 0) {
break;
} elseif ($amount_to_redeem < 0) {
// This should never happen, still if it happens, throw error
throw new Exception ("Something wrong with logic!");
exit();
}
// if we are here, that means some more amount left to redeem
}
Now, you can use two Update queries. First one would update amount_redeemed value against all the Credit id(s). Second one would Insert the Deduct row using the sum of all individual amount_redeemed value.
SELECT `id`, `email`, `NAME`, `type`,
(
( SELECT SUM(amount) FROM credit_table AS ct1 WHERE u.id = ct1.id AND process = 'ADD' AND date_exp > CURDATE()) -
( SELECT SUM(amount) FROM credit_table AS ct2 WHERE u.id = ct2.id AND process = 'Deduct' )
) AS balance
FROM
`user_table` AS u
WHERE
id = 2;
Hope it works as you wish
I Have 4 tables on my db
Table "User"
id | user
-----------
55 | Jhon
56 | Krish
57 | Boss
Table "Payout"
id | Payout_Date
------------------
1 | 31.10.2015
2 | 24.10.2015
3 | 17.10.2015
Table "Earning"
Userid | Date | Earning
------------------------------------
55 | 31.10.2015 | 5$
56 | 31.10.2015 | 1$
57 | 31.10.2015 | 3$
55 | 30.10.2015 | 5$
56 | 30.10.2015 | 12$
57 | 30.10.2015 | 0$
55 | 29.10.2015 | 5$
56 | 29.10.2015 | 4$
When I add a new Payout date (Payout_Date),
I want to find who earn >10$ in their Last payment date between new payout out date, If not pay before that user, his Last Payment_Date is No(get his all earning).
Then add that results to table "Payment" with new Payout_Date to Payment_Date. Their Last Payment_Date is From_Date.
Table "Payment"
id | Userid | From_Date | Payment_Date | Earning | Status
------------------------------------------------------------------
1 | 55 | 24.10.2015 | 31.10.2015 | 12$ | 0
2 | 56 | 24.10.2015 | 31.10.2015 | 17$ | 0
How to write that on Codeigniter Controller and Model?
You need to break the problem down into pieces: find current payout date, find last time user paid, add earnings since then, filter if > $10 .
Here is a sample of what a view may look like (I did not actually run this so there could be syntax errors)
SELECT Earning.Userid, sum(Earning.Earning) earning_since_last
FROM
/* Find current payout Date */
(SELECT MAX(Payout_Date) current_payout_date FROM PAYOUT) as curr_payout,
/* find last time user paid */
(SELECT Userid, MAX(Payout_Date) last_payout_date
FROM PAYOUT
GROUP BY Userid) last_payout,
Earning
WHERE Earning.date <= curr_payout.current_payout_date
and last_payout.Userid = Earning.Userid
and earning.date > last_payout.last_payout_date
GROUP BY Earning.Userid
HAVING SUM(Earning.Earning) >= 10
I want to achieve something like this using php and mysql
if the customer has an account with rewards and wants to spend rewards, how do i update the table so that it will going to subtract the spent reward to the table.
Definitely it will going to get the sum of reward by customer_id then subtract the spent reward. IF the first row(reward) is less than the spent value, it will going to subtract all then go to next row get the difference from previous result until the value of spent is equal to 0.
sample:
spent = 60
id_customer = 2
I have a table like this
id | id_customer | reward
1 | 2 | 50
2 | 2 | 20
3 | 3 | 100
4 | 4 | 5
the result should be something like this:
1st row: 50(value of first row) - 60 = 0 (with remaining 10)
2nd row: 20(value of 2nd row) - 10 (remaining points from first row) = 0
id | id_customer | reward
1 | 2 | 0
2 | 2 | 10
3 | 3 | 100
4 | 4 | 5
Hope that makes sense. Thanks
This is the logic of my solution (of course, maybe more than this one and a better ones):
Get the set of rows for that customer (id_customer = 2) and loop through the rows returned.
In each iteration, compare the value of field reward against the amount you like to subtract (60).
If the actual value is >= 60, update that row and exit. If not, update it with 0, update the remain value (60 - row value) and go to the next item in the iteration doing the same action.
In MySQL, I think the best way to do this is with variables. This should work:
declare #spent := 60;
update tablelikethis
set reward = (case when #spent = 0 then reward
when #spent >= reward
then (case when (#tmp := #spent) is null then NULL
when (#spent := #spent - reward) is null then NULL
else 0
end)
else (case when (#tmp := #spent) is null then NULL
when (#spent := 0) is null then NULL
else reward - #tmp
end)
end)
where id_customer = 2
order by id;
MySQL makes this a little hard to do in a single update, because you cannot use order by with a join. The variable version just has to deal with logic on whether the amount remaining for the reward is bigger or less than the amount remaining being spent.
I'd structure your database in a different way. You could create a column called "reward_points" in the customer table, and have a separate reward table. The structure is:
REWARD_TABLE
----------------------------------
reward_id | customer_id | reward
----------------------------------
1 | 2 | 50
2 | 2 | 20
3 | 3 | 100
4 | 4 | 5
CUSTOMER_TABLE
-------------------------------------------------
customer_id | name | reward_points
-------------------------------------------------
1 | Eddard Stark | 0
2 | Jaime Lannister | 70
3 | Joffrey Baratheon | 100
4 | Theon Greyjoy | 5
Then you could just update the CUSTOMER_TABLE with the new value. You could keep the REWARD_TABLE as a 'reward history'. Even better... upon purchase, you could add a negative transaction to the REWARD_TABLE, so when you would do a SELECT SUM(reward) asRewardFROM reward_table WHERE customer_id = 2 GROUP BY customer_id, it would count all the negative transactions as well, resulting in something, which is close to your concept.
I am creating a graph where I can get the total views everyday for a certain range, or as long it goes back.
The problem I am having is to fill a default number of 0 when no views has been made for a certain day, some days there may be absolutely no views in a day so I need MySQL to return a default of 0 when none is found - I have no idea how to do this.
This is the query I use to get the total views a day:
SELECT DATE(FROM_UNIXTIME(v.date)) AS date_views,
COUNT(v.view_id) AS total_views
FROM
(
views v
)
GROUP BY date_views
ORDER BY v.date DESC
My results return this:
+------------+-------------+
| date_views | total_views |
+------------+-------------+
| 2012-10-17 | 2 |
| 2012-10-15 | 5 |
| 2012-10-14 | 1 |
| 2012-10-10 | 7 |
+------------+-------------+
However there are missing days that I want to return 0 for it, as 2012-10-16, 2012-10-11, 2012-10-12, 2012-10-13 is not included.
So, for example:
+------------+-------------+
| date_views | total_views |
+------------+-------------+
| 2012-10-17 | 2 |
| 2012-10-16 | 0 |
| 2012-10-15 | 5 |
| 2012-10-14 | 1 |
| 2012-10-13 | 0 |
| 2012-10-12 | 0 |
| 2012-10-11 | 0 |
| 2012-10-10 | 7 |
+------------+-------------+
Would be returned.
How would this be approached?
When I did this a couple of years ago I created an empty array with the date as key and the default value 0. Then I simply looped through the result att changed the value for those dates I had.
for each($result as $row){
$date_stats_array[$row['date']] = $row['value'];
}
In situations like this I create a temporary table which I fill with all the dates you want. After that, you can use that table to join your original query against.
To fill the table you can use this procedure:
DROP PROCEDURE IF EXISTS filldates;
DELIMITER |
CREATE PROCEDURE filldates(dateStart DATE, dateEnd DATE)
BEGIN
WHILE dateStart <= dateEnd DO
INSERT INTO tablename (_date) VALUES (dateStart);
SET dateStart = date_add(dateStart, INTERVAL 1 DAY);
END WHILE;
END;
|
DELIMITER ;
CALL filldates('2011-01-01','2011-12-31');
Courtesy of https://stackoverflow.com/a/10132142/375087