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.
Related
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;
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
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!
I have 2 tables.
Table 1 = branch
containing
branch_id
branch_name
Table 2 = sales_data
containing
sales_id
sales_date
sales_branch
sales_profit
I need away of showing the total daily sales and total daily profit for each branch. I know how to bring back the result for a given week etc I am just really struggling on how to pull back the data.
I also need all the branches to be shown at all time and if they haven't sold anything to display a 0.
I have put together a quick image (http://i37.photobucket.com/albums/e71/dannyflap/screen_shot.jpg) of how I would like the finished. This is really driving me nuts :(
UPDATE
select sales_branch, WEEKDAY(sales_date), COUNT(sales_profit), SUM(sales_profit)
FROM sales_date
GROUP BY sales_branch, WEEKDAY(sales_date)
This then brings back the following example. Figures are made up.
sales_branch, day, units, profit:
| branch1 | 0 (as day) | 16 | 439 |
| branch1 | 1 (as day) | 12 | 651 |
| branch1 | 2 (as day) | 22 | 312 |
| branch1 | 3 (as day) | 61 | 614 |
| branch1 | 4 (as day) | 12 | 541 |
| branch1 | 5 (as day) | 24 | 102 |
| branch1 | 6 (as day) | 21 | 145 |
I guess you would have to do an outer join on table 1's branch_id and table 2's sales_branch and loop through the result to produce the table. With an outer join those branches who haven't sold anything will get a NULL, I think.
Is it the table generating you're having problem with? I don't think it's wise to do some spectacular SQL for this problem. My two cents.
You're going to need a foreign key in sales_data that maps to branch.
assuming sales_branch is something different than branch_name or branch_id, sales_data should contain:
sales_id sales_date sales_branch branch_id sales_profit
Also - you should post your code for getting back the result for a given week and we can help you build on that rather than give you the entire answer.
EDIT
Ok with modifying your sales_data table to have a foreign key branch_id to the branch table, this query should get you closer to what you're looking for.
SELECT b.branch_name, WEEKDAY(s.sales_date), COUNT(s.sales_profit), SUM(s.sales_profit)
FROM branch b
LEFT OUTER JOIN sales_data on s.branch_id = b.branch_id
GROUP BY b.branch_id, WEEKDAY(s.sales_date)
I am trying to find a good solution to accomplish the following:
I have a table which includes the name of various products, such as:
"Tide - Original Scent".
In addition, I also have the amount in there for e.g. 50 fl oz.
The problem I have right now is, that the product not only comes in containers of 50 fl oz but also in different sizes such as 75 fl oz and 100 fl oz. For each of these I have new rows in the product table:
id| productName | amount | unit
1 |"Tide - Original Scent" | 50 | "fl oz"
2 |"Tide - Original Scent" | 75 | "fl oz"
3 |"Tide - Original Scent" | 100 | "fl oz"
Now I have a web interface to perform a search on this table and whenever I search for "tide" I get all three rows - which is supposed to be like that of course. However I would like a different behavior and this is where I need your help:
Instead of returning all three rows I would like one row only. I would then need to be able to process it in php so that if the user clicks on "Tide - Original Scent" that the user is then prompted to select the size.
To add even more complexity to the task:
I also have products in the same table named:
"Tide - Mountain Spring".
In this case, it would be great to have some relations set up so I know that "Tide - Original Scent" is linked with "Tide - Mountain Spring". Within php I would then like to not only give the user the choice of selecting the size but also the (in this case) scent.
What would your recommendation be on how I can accomplish this (not the php part)?
How would your database look like?
Do I need to create a table where I map these products? How would this look like if you would create this :)
2 possibilities:
Don't store the sizes in that table - along with the other specific information. Move that to another table. Denormalize your structure.
or
Query but group by the name. For the size column, do a count(amount). If it's more than one, you can then populate a drop down with choices. This is good temporary fix.
SELECT productName, count(amount) AS numOfChoices FROM YOUR_TABLE
WHERE LOWER(productName) LIKE 'tide%'
GROUP BY productName
then after the choice is made
SELECT id, amount FROM YOUR_TABLE
WHERE id = "$selectedId"
to present a choice of sizes that will pin point which one.
I would personally setup my tables like this..
Products Table:
ID| Product ID | Product Name | Description
1 | 0404888282 | Tide - Original Scent | Smells Good
Quantity Table:
ID| Product ID | Size| Price | Quantity
1 | 0404888282 | 50 | 4.99 | 23
2 | 0404888282 | 75 | 5.99 | 120
3 | 0404888282 | 100 | 7.99 | 10
This structure you have a table for each unique item, another for the sizes and quantity of each size. Keeps the structure clean and easy to understand.
"Instead of returning all three rows I would like one row only."
SELECT DISTINCT productName FROM YOUR_TABLE WHERE LOWER(productName) LIKE 'tide%'
And you'll need a functional index on LOWER(productName) for good performance. Alternatively a case-insensitive collation sequence could be used on DBMSes that support that (e.g. MS SQL Server).
"I would then need to be able to process it in php so that if the user clicks on "Tide - Original Scent" that the user is then prompted to select the size."
SELECT amount FROM YOUR_TABLE WHERE productName = whatever_user_selected
"To add even more complexity to the task: I also have products in the same table named:
"Tide - Mountain Spring"."
The query above will also return that.
What you can do is :
$Query = 'SELECT productName, amount, unit FROM products';
$Data = array();
while($Assoc = mysql_fetch_assoc($Query)){
if(!isset($Data[$Assoc['productName']])){
$Data[$Assoc['productName']] = array();
}
$Size = $Assoc['amount'].' '$Assoc['unit'];
$Data[$Assoc['productName']][] = $Size;
}
// Now what you can do is :
foreach($Data as $ProductName => $Amount){
echo $ProductName.' has :<br />';
if(count($Amount) > 0){
foreach($Amount as $Key => $Value){
echo $Value.'<br />';
}
} else {
echo 'Nothing<br />';
}
}
This however doesn't solve the problem on MySQL's side. IT will work in PHP wihtout problem. It's not beautiful but it's working.
For the first problem, you could create two tables:
products - this is where you store all the information about a product except for the specifics such as different sizes, colors, etc.
attributes - you would link to the product and for each attribute you specify a value
products
id | description
---+------------
1 | crazy shirt
2 | clown shoe
attributes
product | name | value
--------+-------+-------
1 | color | green
1 | color | blue
1 | size | medium
2 | size | large
You can optimize the attributes table further by creating a attribute_names table (and even an attribute_values table).
For your second problem, you could either:
create a related product id column inside the products table; this would limit you to only one related product per product.
create a related product table in which you store combinations between two products.
related_products
product_id | related_product_id
-----------+-------------------
1 | 2
1 | 3
That would create a relationship between product 1 and products 2 and 3.
Hope this helps.
Create a foreign key to sublabels, and order them with a counter. This will look like.
Product Table:
Product ID (key)
Brand ID
Price
Brand Table:
Brand ID (key)
Brand
Sublable Table:
ID
Product ID
Order Index
Value
Size Table:
Size ID
Value
Unit
ProductSize Table:
Size ID
Product ID
Then, you'll divide into subcategories using subsequent sublabels.
Products
10 | 6 | 1.99
11 | 6 | 2.99
12 | 6 | 3.99
13 | 6 | 1.99
14 | 6 | 2.99
15 | 6 | 3.99
Brand
6 | Tide
Sublabel
30 | 10 | 1 | Original Scent
30 | 11 | 1 | Original Scent
30 | 12 | 1 | Original Scent
30 | 13 | 1 | Mountain Spring
30 | 14 | 1 | Mountain Spring
30 | 15 | 1 | Mountain Spring
Size Table
1 | 50 | fl.oz.
2 | 75 | fl.oz.
3 | 100 | fl.oz.
Product Size Table
1 | 10
1 | 13
2 | 11
2 | 14
3 | 12
3 | 15