MySql how to automatically apply discounts? - php

Lets say client ordered 3 products
Order products table:
id | order_id | product_id | price
1 | 1 | 2 | 10
2 | 1 | 2 | 10
3 | 1 | 2 | 10
Orders table:
id | total_price
1 | 30
and now I want MySql to apply special discount that makes 3rd item free during specific period of time. So as result I will get
id | total_price
1 | 20
I'm using MySql 5.6, php(symfony 2, doctrine ORM) and wondering what is the best way to create and handle such scenarios.

To apply discounts during periods you have to design code to support this. A design could look as follows:
Create a discount table:
CREATE TABLE discount(
discount_id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
rule VARCHAR(400),
start_date DATE,
end_date DATE
);
The discount rule can be a JSON string containing the instructions. These instructions can differ every time so this way your table is the same all the time.
A possible structure for the JSON for this discount is:
{"ruleType":"multipleProductDiscount","ruleValue":"3"}
You can then use a CASE statement to apply the different discount rules when you process them in the code.
The start/end date are just a way to prevent of having to load and process all the rules every time. So a query retrieving the data from the discount table only gets active data instead:
SELECT * FROM discount WHERE somedate BETWEEN start_date AND end_date;

Related

How to save, handle the order total amount in an orders, ordersDetails schema?

When I started designing my application database schema few months ago I have been told not to store the same data/calculated data in more than one place in the database(normalization). If I do, I will make a scope of bugs when I update the data in one place and left the other without updating. So I did an orders table and ordersDetails table. Something like this..
-- orders table
+-----+---------+----------+
| ID | clintID | date |
+-----+---------+----------+
| 1 | 1 |2018-02-22|
| 2 | 1 |2018-02-23|
| 3 | 2 |2018-02-24|
+-----+---------+----------+
-- orderDetail table
+-----+---------+------------+----------+----------+
| ID | orderID | itemNumber | quantity | unitPrice|
+-----+---------+------------+----------+----------+
| 1 | 1 | 12345 | 3 | 100.75 |
| 2 | 1 | 12346 | 3 | 100.75 |
| 3 | 2 | 12347 | 3 | 100.75 |
| 4 | 2 | 12345 | 3 | 100.75 |
| 5 | 3 | 12347 | 3 | 100.75 |
| 6 | 3 | 12345 | 3 | 100.75 |
+-----+---------+------------+----------+----------+
And to make the the queries easier for me I made a view "allOrdersSummary" like
-- allOrdersSummary
SELECT
orders.*, SUM(orderDetail.quantity * orderDetail.unitPrice) totalAmount
FROM orders INNER JOIN orderDetail ON orders.ID = orderDetail.orderID
GROUP BY orders.ID;
and I used this view later for my queries, but now I started to get the MAX_JOIN_SIZE error.
So I thought of saving the calculated total order amount along with the orders table ID, clintID, date, totalAmount and whenever I change something in the orderDeatils table I update the calculated totalAmount column in the orders table, I don't know if this is good or bad!
This problem -I don't know if this is considered a problem or not- is encountered many times, for example to know the unread messages of the client making the request I have to do sum(messages) unread from messages where to = ? and isRead = 0
A) should I make another column for calculated totalAmount in the orders table or it is a normal thing in databases to calculate the totalAmount from the orderDetails table every time I need it ?
B) If you recommend making another column in the orders table, what is the best way to update it every time a change happens in the orderDetails table ? should I update it at the PHP layer whenever I update the orderDetails table, or this is something that needs a stored procedure ?
Yes, it is normal to store pre-calculated values, based on other data in the database, in a database. But not necessarily for the reason you mention. I never had a problem with MAX_JOIN_SIZE.
The main, and probably only, reason for storing calculated values is speed. So you do it for values that don't change that often and that may be used in queries that use a lot of data and may therefore be too slow if you didn't use them.
For instance: If you want to know the average value of all the orders in your database the query would be a lot faster if you already have the order totals.
Why, and how, you update the values is completely up to you. However you have got to be consistent about it. If you use the MVC pattern it would make sense to integrate it in the controller. Or in simple terms: Whenever a form is submitted that could change one of the values, out of which the pre-calculated value is computed, you need to recompute it.
This is a clear demonstration where 'normalization' is not entirely maintained. It's not really pretty, but sometimes worth it. You could, of course, argue, that the calculated value represents 'new' information, and therefore does not offend against 'normalization'.
You have an "inflate-deflate" problem.
JOIN the two tables to make a much larger temporary table.
GROUP BY to shrink back to one row per row of the original (orders) table.
This avoids the problem:
SELECT *,
( SELECT SUM(quantity * unitPrice
FROM orderDetail WHERE orderID = orders.ID
) AS totalAmount
FROM orders;
Please let me know how your experience is with this one. It is one of the simplest examples of the inflate-deflate problem.

Second Column with sequence numbers in database

I am designing a system where I have multiple shops where each shop should have its own set of sequential numbers for its invoices. Obviously my primary ID column will be sequential for all invoices in the system so obviously I will need another column to store this "shop specific" invoice number. What is the best manner to store and get the next ID for this shop specific number? For example would it be safe to simply get it right from the invoices table doing something like: SELECT MAX(INV_NUM) FROM INVOICES WHERE SHOP_ID = # and add one, and subsequently create the new invoice record? Or would there be issues with the timing if 2 instances of my script were to run at the same time? For example the first script fetches the next ID, but before it gets the chance to create the new invoice record the second instance requests the next ID and gets the same one as the first script... I was then thinking about just storing the last used number in a separate table and as soon as I request the next invoice number immediately write the new next order number and persist it so that the time between fetching the next order number and creating the record that the next request would rely on is kept to an absolute minimum... literally 3 lines of code:
$nextId = $shop->getLastId() + 1;
$shop->setLastId($nextId);
$em->persist();
Invoices
------------------------------
| ID | INV_NUM | SHOP_ID |
------------------------------
| 1 | 99 | 1 |
| 2 | 100 | 2 |
| 3 | 100 | 1 |
Shops
-------------------
| ID | LAST_ID |
-------------------
| 1 | 100 |
| 2 | 100 |
If you're using Doctrine, which I assume you are since you're using Symfony then you can lifecycle events to listen for changes in your entities. Before saving you can then update your second column to the incremented value.
Regarding race conditions, to be sure you don't have bad data in your database you can put a unique constraint on your shop ID and invoice number.

MySQL - Selecting the sum amount based on specific condition

I've searched for a few hours now, but couldn't find relative solution to a specific algorithm I am working on. To simplify the obstacle, I would like to present the information in just one table.
_____________________________________
| User | Item | price | qty |
-------------------------------------
| Annie | Dress | 80 | 1 |
| Bob | Jeans | 65 | 3 |
| Cathy | Shoes | 60 | 4 |
| David | Shirts | 40 | 6 |
| Annie | Shoes | 60 | 2 |
| Bob | Shirts | 55 | 2 |
| Cathy | Jeans | 65 | 1 |
| David | Ties | 20 | 5 |
-------------------------------------
Problem # 1: Show users whose total price for shopping at the store is 300 or more and quantity of their purchase is less than or equal to 3. These shoppers will be mailed a coupon for $40.
Problem # 2: Show users whose total qty is greater than or equal to 7 and the total for price is 275 or more. These shoppers will be mailed a coupon for $20.
The rows within the table are not transaction specific. The table can represent separate transactions within a month. We're just trying to find certain returning customers who we would like to reward for shopping with us.
I'm not sure if this can be done only via MySQL, or if I need to have separate queries and store rows into arrays and compare them one by one.
What I have tried so far are the followings:
SELECT * FROM table where SUM(price) as Total >= 300 AND SUM(qty) <=3;
I've also tried the following after the research:
SELECT SUM(price) as Total FROM table WHERE SUM(qty) <=3;
I keep getting syntax errors in MySQL shell. You don't have to solve the problems for me, but if you can guide me through the logic on how to solve the problems, I'd appreciate it very much.
Lastly I'd like to ask once, can I solve this with only MySQL or do I need to store the rows into PHP arrays and compare each indexes?
You can't use an aggregate function in the WHERE clause, you have to use HAVING. WHERE operates on individual rows during the selection, HAVING operates on the final results after aggregating.
SELECT *, SUM(price*qty) as Total
FROM table
GROUP BY user
HAVING Total >= 300 AND SUM(qty) <= 3
SUM is an aggregate function, meaning it applies to a group of clubbed rows. S say i am grouping the table data based on NAME then sum function would sum all the price of one NAME.
Having said this, if you think logically it would not make any sense to put the sum(price) in a WHERE clause because where clause would not know which SUM(PRICE) for which NAME to operate on(where clause operates only after a temporary view has been generated).
So we have the HAVING clause in SQL. This is used to compare the results of aggregrate function at each step of aggregation.
Consider it like this:
In where clause, when the ANNIE row from your DB is returned, it does not know what SUM(PRICE) means.
While in HAVING clause the SUM(PRICE)>300 condition is executed only when SQL has finished grouping all the ANNIE data into one group and calculated the SUM(PRICE) for her.
For question 1:
SELECT USER, SUM(PRICE)
FROM table
GROUP BY user
HAVING SUM(PRICE) >= 300 AND SUM(QTY) <= 3
For Question 2:
SELECT USER, SUM(PRICE)
FROM table
GROUP BY user
HAVING SUM(PRICE) >= 275AND SUM(QTY) >=7

yii get sum from a table related to a related table

I have these existing tables to work with, can't change them. I have given everything but cannot get a workaround. It is complex even for me to explain the problem so please bear with me.
I am using Oracle 10g with Yii 1.1.x, WIndows, Wamp.
Models have been created. I need to get the sum of PRESALES for given SALESEXECs between a date range.
SALESEXEC
------------------------------------------
id | 1
name | salesguy1
relation
'SALES_TOTAL' => array(self::STAT, 'CURSALES', 'id', 'select'=>'SUM(AMOUNT)'),
The above works, I can get my total current sales against each SALESEXEC
I also need a PRESALES_TOTAL, totally stuck there
CURSALES
------------------------------------------
jobnumber | AJOB2014 | AJOB2014
customernumber | cus1 | cus2
amount | 1000 | 1000
saledate | 01-08-2014 | 01-09-2014
salesexec_id | 1 | 1
SALES EXEC is in a 1-many relation with CURSALES.
PRESALES
------------------------------------------
jobnumber | AJOB2014 | AJOB2014
customernumber | cus1 | cus2
amount | 500 | 700
salesexec_id | 1 | 1
This table stores all previous sales. It is related to CURSALES by the customernumber and jobnumber.
I need the sum of this PRESALES amount as a property of SALESEXEC.
As in the example, if I pass the salesdate range as 01-08-2014 and 15-08-2014, it should include PRESALES 1st row in the SUM but not the 2nd row because it has cus2 which maps to 2nd row of CURSALES but the saledate for it is outside my given date range.
I tried this in SALESEXEC model
'PRESALES_TOTAL' => array(self::STAT, 'PRESALES', 'id', 'select'=>'SUM(AMOUNT)'),
but it returns all previous sales without matching the date range and the jobnumber and customernumber
I tried 'through' clause in the relation but it is not valid for STAT
In effect, I will pass a list of SALESEXEC id and a date range, it will need to go to CURSALES and pick up data matching the date range, then need to go to PRESAL and get the total of previous year's sales of those customernumber and jobnumber).
I need to display list of SALESEXEC with their total sales and total previous sales in a CGridView.
Thanks a lot!

Calculate price of rental period

I am working on a project for which I need to calculate prices of holiday homes available in a selected rental period. I need some help with building a SQL query that combines the following tables and convert the data into an output containing the price for the requested period for each house. It should contain the stay costs, and the additional cost types together with the amount the renter should pay for every cost_type.
I have a table costprofiles which enables the house owner to have multiple prices throughout the year:
+----------------+----------+--------------+
| costprofile_id | house_id | profile_name |
+----------------+----------+--------------+
| 1 | 312 | summer |
+----------------+----------+--------------+
| 2 | 312 | winter |
+----------------+----------+--------------+
I have a table called costprofile_items which is linked to a costprofile via the foreign key costprofile_id. This table contains all different amounts a renter should pay to the owner if the price of the selected period uses this cost_type. Each additional amount can be calculated in four different ways:
per night
per stay
per person
per person per night
The way each amount contributes to the total rent price is stored in the calculation_type column. This is what the costprofile_items table looks like:
+---------------------+----------------+--------+-------------+----------------------+
| costprofile_item_id | costprofile_id | amount | cost_type | calculation_type |
+---------------------+----------------+--------+-------------+----------------------+
| 1 | 1 | 20 | usage_cost | per_night |
+---------------------+----------------+--------+-------------+----------------------+
| 2 | 1 | 8.5 | cleaning | per_stay |
+---------------------+----------------+--------+-------------+----------------------+
| 3 | 1 | 0.82 | tourist_tax | per_person_per_night |
+---------------------+----------------+--------+-------------+----------------------+
I also have the table prices in which each row represents a price per night that can be used between the start_date and the end_date (the weekday of the start_date equals the weekday of arrival at the house and the weekday of end_date equals the weekday of departure). The row also contains a column nights that determines how long a sub period needs to be in order to use this price. This is what the table looks like:
+----------+----------+----------------+------------+------------+-----------+--------+
| price_id | house_id | costprofile_id | start_date | end_date | per_night | nights |
+----------+----------+----------------+------------+------------+-----------+--------+
| 1 | 1 | 1 | 2014-08-04 | 2014-12-01 | 60 | 7 |
+----------+----------+----------------+------------+------------+-----------+--------+
| 2 | 1 | 1 | 2014-08-08 | 2014-12-05 | 70 | 3 |
+----------+----------+----------------+------------+------------+-----------+--------+
| 3 | 1 | 2 | 2014-12-01 | 2015-03-02 | 0 | 1 |
+----------+----------+----------------+------------+------------+-----------+--------+
In the table you can see that for the given house you can book the period from 8 till 11 August and this will cost 3*70 = €210 for the stay. If you are with 4 persons the additional costs are 3*20 = €60 for electricity/gas usage, €8.5 for cleaning and 0.82*4*3 = €9.84 for tourist tax. So the total cost of your weekend will be €288.34. It also should be possible to combine this weekend with for example 2 times the weekly price as described in the first row of the table. In this case the price from 8 till 25 August would be 288.34 + 2*582.96 = €1454.26. Note that the calculation types per_stay and per_person only need to be selected from the first sub period, so the cleaning in the last example is only paid once.
The last table I use for calculating prices is the table prices_per_group. This table is connected to prices via the foreign key price_id. In the prices table above you can see in the last row that the price per night equals 0. In that case the owner had given a price per night for every number of persons that he accepts in his house during this period this price is active. This is the way those different prices are stored:
+--------------------+----------+------------+-----------+
| price_per_group_id | price_id | group_size | per_night |
+--------------------+----------+------------+-----------+
| 1 | 3 | 5 | 50 |
+--------------------+----------+------------+-----------+
| 2 | 3 | 4 | 45 |
+--------------------+----------+------------+-----------+
As you can see a week starting at 1 December (or any Monday after that, but before 2 March) will cost €50 per night if you are with 5 persons or €45 if you are with 4.
I hope it is clear now how I am trying to store and compute all different prices.
I have managed to get these calculations working, by first querying all cost types of every available house with the following query:
SELECT * FROM (
SELECT prices.house_id,
prices.price_id,
prices.costprofile_id,
prices.nights,
prices.start_date,
prices.end_date,
MIN(
prices.per_night + COALESCE(prices_per_group.per_night, 0)
) AS per_night /* Add the price per night from prices and prices_per_group (if one has a non-zero value the other is always zero) */
FROM prices
LEFT JOIN prices_per_group ON prices.price_id = prices_per_group.price_id
WHERE prices.house_id IN (
/* Query that returns a set with the ids of all available houses here */
)
AND (
prices_per_group.price_id IS NULL OR /* If true, no row in prices_per_group is pointing to the price_id currently being evaluated */
prices_per_group.group_size >= 4 /* If true, the group_size satisfies the requested number of persons */
)
GROUP BY prices.price_id
) AS possible_prices
INNER JOIN costprofile_items ON costprofile_items.costprofile_id = possible_prices.costprofile_id
ORDER BY price_id ASC
After that I used PHP to loop through all rows containing price information for a certain house. I started at the start_date and made steps using the first usable price row it could find and repeated that until I am at the end_date. The problem with my current method is that it is too slow. For 1000 houses the webserver needs 0.3sec execution time. Maybe some optimization can be done in my PHP code, but I was hoping someone could help me with putting this all together in SQL. This way for example sorting by price is easier to implement and just asking for the large result after quickly executing the above query makes my execution time jump up to 0.12sec.
All help and advice is welcome
In the end I decided to cache all prices instead of live computing them. This results in much better performance and allows for much more complex pricing than can be computed on the fly inside queries. Every night a cronjob runs that fills up 21 tables (a table for each possible rental duration). The duration pricing tables contain key,value pairs of arrival date and corresponding computed price for that duration. Optionally you can add a column for group size, resulting in a price per duration, per group size, per arrival date. It takes quite some database records, but if you create indices this is blazing fast.

Categories