I have a table of invoices that can be in multiple currencies that looks like this:
| id | issue_date | total | currency |
|----|------------|-------|----------|
| 1 | 2020-04-20 | 1234 | EUR |
| 2 | 2020-04-26 | 2345 | USD |
| 1 | 2020-04-27 | 9876 | EUR |
| 3 | 2020-04-28 | 3456 | RON |
And i have a currency table that holds currency exchange rates that looks like this:
| id | date | currency_id | rate |
|----|------------|-------------|---------|
| 1 | 2020-04-20 | EUR | 1 |
| 2 | 2020-04-20 | USD | 1.08600 |
| 3 | 2020-04-20 | RON | 4.83560 |
What I would like to achieve is to calculate each invoice price based on its issue_date, currency and a target currency.
All currency exchange rates are based on EUR so its value will always be 1. Currencies are updated daily but there are dates missing (during weekend exchange rates don't update) so calculation needs to be based on most recent exchange rate until invoice.issue_date
So what I tried was this:
SELECT
`i`.`id`,
`i`.`total`,
`i`.`currency`,
`exr1`.`rate` as `invoice_rate`,
`exr2`.`rate` AS `target_rate`,
`i`.`total` * `exr1`.`rate` as `euro_price`,
`i`.`total` * `exr1`.`rate` / `exr2`.`rate` AS `target_price`
FROM `invoices` as `i`
LEFT JOIN `exchange_rates` AS `exr1`
ON
`exr1`.`currency_id` = `i`.`currency` AND
`exr1`.`date` = `i`.`issue_date`
LEFT JOIN `exchange_rates` as `exr2`
ON
`exr2`.`currency_id` = 'RON' AND
`exr2`.`date` = `i`.`issue_date`
GROUP BY
`i`.`id`,
`invoice_rate`,
`target_rate`
ORDER BY `i`.`issue_date` DESC
Problem nr. 1
Because there are no exhange rates for the exact invoice dates I get null values. I tried changing the LEFT JOIN ON to something like exr1.date <= i.issue_date but GROUP BY invoice doesn't work anymore (i get duplicates).
Problem nr. 2
For rows that have exchange rates on that exact day I get wrong values because based on the target currency I need to either multiply or divide:
i.total * exr1.rate * exr2.rate AS usd_price vs i.total * exr1.rate / exr2.rate AS usd_price
https://www.db-fiddle.com/f/e5GnVnry5sAiXwbuScV6JT/19
This is a (rare) case where a dependent subquery is the way to go. Here's the overall query (https://www.db-fiddle.com/f/e5GnVnry5sAiXwbuScV6JT/21)
SELECT id,
total,
currency,
rate,
total / rate euro_price
FROM ( SELECT i.id,
i.total,
i.currency,
(SELECT e.rate
FROM exchange_rates e
WHERE e.currency_id = i.currency
AND e.date <= i.issue_date
ORDER BY e.date DESC
LIMIT 1) rate
FROM invoices i
) d
The dependent subquery is this:
SELECT e.rate
FROM exchange_rates e
WHERE e.currency_id = i.currency
AND e.date <= i.issue_date
ORDER BY e.date DESC
LIMIT 1
It finds the exchange rate for the most recent date equal to or before the issue_date. It's called dependent because it refers to column values in its outer query.
This isn't going to be fast. A covering index on exchange_rates(currency_id, date DESC, rate) will help. Like this.
CREATE INDEX lookup ON exchange_rates(currency_id, date DESC, rate);
I used a nested query so the outer query can simply refer to rate as a column when it needs to, rather than repeating the dependent subquery.
Also note I think you want to divide, not multiply, when computing your euro_price.
I left the second rate lookup to you.
**Pro tip* Only use the backtick marks when your table or column name is a reserved word in the query language. Your queries are MUCH easier to read without them.
Related
I have the following tables:
Let's assume the first table represents a booking made by a customer for a hotel.
+------------+-----------+-----------+--------------+----------------+
| booking_id | client_id | room_type | arrival_date | departure_date |
+------------+-----------+-----------+--------------+----------------+
| 1 | 1 | 1 | 2016-05-30 | 2016-06-03 |
+------------+-----------+-----------+--------------+----------------+
The second table contains the prices for different types of room. The prices vary for different periods of time.
+---------------+-----------+------------+------------+-------+
| room_price_id | room_type | start_date | end_date | price |
+---------------+-----------+------------+------------+-------+
| 1 | 1 | 2016-03-01 | 2016-05-31 | 150 |
| 2 | 1 | 2016-06-01 | 2016-08-31 | 200 |
+---------------+-----------+------------+------------+-------+
My question is: how can I calculate the total cost for a booking if the price is not the same during the whole period of the booking? (in my case the total cost would be 700)
I read the following links: MySQL: Select all dates between date range and get table data matching dates and Select data from date range between two dates, but I don't get it on how to solve my problem since the entries in the table with the prices contain only date ranges and not explicit dates as in the first link.
The trick with your question is in formulating the join condition between the booking and prices tables. Once this is out of the way, a simple aggregation gets you the result you want. The join condition should be true whenever any point in a given price range falls within the range of the arrival or departure date.
Then, to count days we use the greater of the arrival date or the start date, and the lesser of the departure date or the end date.
SELECT
b.booking_id,
SUM((DATEDIFF(LEAST(b.departure_date, p.end_date),
GREATEST(b.arrival_date, p.start_date)) + 1) * p.price) AS total
FROM booking b
INNER JOIN prices p
ON b.room_type = p.room_type AND
(p.start_date BETWEEN b.arrival_date AND b.departure_date OR
p.end_date BETWEEN b.arrival_date AND b.departure_date)
GROUP BY b.booking_id;
The output for your sample data is 900. This is correct because:
2 days x 150 = 300
3 days x 200 = 600
total = 900
Demo here:
SQLFiddle
And here is another demo which highlights the case where a stay overlaps an entire price range.
I have two tables (Invoices and taxes) in mysql:
Invoices:
- id
- account_id
- issued_at
- total
- gross_amount
- country
Taxes:
- id
- invoice_id
- tax_name
- tax_rate
- taxable_amount
- tax_amount
I'm trying to retrive a report like this
rep_month | country | total_amount | tax_name | tax_rate(%) | taxable_amount | tax_amount
--------------------------------------------------------------------------------------
2017-01-01 | ES | 1000 | TAX1 | 21 | 700 | 147
2017-01-01 | ES | 1000 | TAX2 | -15 | 700 | 105
2016-12-01 | FR | 100 | TAX4 | 20 | 30 | 6
2016-12-01 | FR | 100 | B2B | 0 | 70 | 0
2017-01-01 | GB | 2500 | TAX3 | 20 | 1000 | 200
The idea behind this is that an invoice has a has_many relation with taxes. So an invoice can have or not taxes. The report should show the total amount collected (total_amount) for a given country (regardess if it includes taxes)
and indicate which part of that total amount is taxable (taxable_amount) for an specific tax.
My current approach is this one:
SELECT
DATE_FORMAT(invoices.issued_at, '%Y-%m-01') AS rep_month,
invoices.country AS country
( SELECT sum(docs.gross_amount)
FROM invoices AS docs
WHERE docs.country = invoices.country
AND DATE_FORMAT(docs.issue_date, '%Y-%m-01') = rep_month
) AS total_amount,
taxes.tax_name AS tax_name,
taxes.tax_rate AS tax_rate,
SUM(taxes.taxable_amount) AS taxable_amount,
SUM(taxes.tax_amount) AS tax_amount
FROM invoices
JOIN taxes ON invoices.id = taxes.document_id
AND documents.issue_date BETWEEN '2016-01-01' AND '2017-12-31'
GROUP BY account_id, rep_month, country, tax_name, tax_rate
ORDER BY country desc
Well, this works but for a real dataset (thousands of records) it's really slow as the select subquery for retrieving the total_amount is being run for each row of the report.
I cannot make a LEFT JOIN taxes with a direct SUM(gross_amount) as the GROUP BY groups by tax name and rate and I need to show the total collected per country regardless if the amount was taxed or not. Is there a faster alternative to this?
I do not know the exact use case of using this query but the issue is the way with which you're trying to structure the DB, you're trying to get the entire data in one go.
Ideally, you should run the query you have and store in a different table (summary table) and then query directly from the summary table whenever you want. And if you have a new entry in the Invoices table then you can use the query to run either on every entry or periodically update the summary table via a cronjob.
I am hoping I am just stumped because its the end of the work day on a Monday, and someone here can give me a hand.
Basically I have 2 tables that have invoice information and a table that has payment information. Using the following I get the first part of my display.
SELECT d.id, i.id as invid, i.company_id, d.total, created, adjustment FROM tbl_finance_invoices as i
LEFT JOIN tbl_finance_invoice_details as d ON d.invoice_id = i.id
WHERE company_id = '69350'
UNION
SELECT id, 0, comp_id, amount_paid, uploaded_date, 'paid' FROM tbl_finance_invoice_paid_items
WHERE comp_id = '69350'
ORDER BY created
What I want to do is:
Create a new column called "Balance" that adds total to the previous total by the created column regardless of how the rest of the table is sorted.
To give a quick example, my current output is something like:
id | invid | company_id | total | created | adjustment
12 | 16 | 1 | 40 | 01/01/16| 0
100| 0 | 1 | 10 | 01/05/16| 0
50 | 20 | 1 | 50 | 05/01/16| 0
What my goal is would be:
id | invid | company_id | total | created | adjustment | balance |Notes
12 | 16 | 1 | 40 | 01/01/16| 0 | 40 | 0 + 40
100| 0 | 1 | 10 | 01/05/16| 1 | 50 | 40 + 10
50 | 20 | 1 | 50 | 05/01/16| 0 | 100 | 50 + 50
And regardless of sorting by id, invid, total, created, etc, the balance would always be tied to the created date.
So if I added a "Where adjustment = '1'" to my sql, I would get:
100| 0 | 1 | 10 | 01/05/16| 1 | 50 | 40 + 10
Since the OP confirmed my understanding in comments, I'm basing my answer on the following assumption:
The running total would be tied to the order of created_date. The
running total would only be affected by company id as a filtering
criterion, all other filters should be disregarded for that
calculation.
Since the running total may have a different order by and filtering criteria than the rest of the query, therefore the running total calculation has to be placed in a subquery.
The other assumption I have to make is that there cannot be more than one invoice with the same created date for a single customer id, since the original query in the OP does not have any group by or summing either.
I prefer to use the approach suggested by #OMG Ponies in this post on SO, where he initiates the mysql variable holding the running total in a subquery, thus there is no need to initialize the variable in a separate set statement.
SELECT d.id, i.id as invid, i.company_id, rt.total, rt.cumulative_sum, rt.created, adjustment
FROM tbl_finance_invoices as i
LEFT JOIN tbl_finance_invoice_details as d ON d.invoice_id = i.id
LEFT JOIN
(SELECT d.total, created, #running_total := #running_total + t.count AS cumulative_sum
FROM tbl_finance_invoices as i
LEFT JOIN tbl_finance_invoice_details as d ON d.invoice_id = i.id
JOIN (SELECT #running_total := 0) r -- no join condition, so this produces a carthesian join
WHERE company_id = '69350'
ORDER BY created) rt
ON i.created=rt.created --this is also an assumption, I do not know which original table holds the created field
WHERE company_id = '69350' and adjustment=1
ORDER BY d.id
If you need to take the amounts from the tbl_finance_invoice_paid_items into account as well, then you need to add that to the subquery.
I want to ask about SQL in mysql. Im stack over 1 hour :(
I have sql :
SELECT TZL.IsMissed, COUNT(TZL.ChatID) as Amount FROM tblLog TZL group by TZL.IsMissed
And the result :
| IsMissed | Amount |
| 0 | 100 |
| 1 | 500 |
I want add one more column after Amount column, let say the name of new column is SumAmount. i want SumAmount value is SUM of the Amount Column.
| IsMissed | Amount | SumAmount |
| 0 | 100 | 600 |
| 1 | 500 | 600 |
I already try sql like below :
SELECT
tbl.*,SUM(tbl.Amount) as SumAmount
FROM
(
SELECT
TZL.IsMissed,
COUNT(TZL.ChatID) AS Amount
FROM
tblLog TZL
GROUP BY
TZL.IsMissed
) tbl
GROUP BY
tbl.IsMissed
WITH ROLLUP
But with ROLLUP the result is add a new one row, not column. Anyone can teach me for this ?
Thanks for answer
There are several ways to approach this. I would calculate the value in the from clause:
SELECT TZL.IsMissed, COUNT(TZL.ChatID) as Amount, tt.SumAmount
FROM tblLog TZL CROSS JOIN
(SELECT COUNT(*) as SumAmount FROM tblLog) tt
GROUP BY TZL.IsMissed, tt.SumAmount;
I have 2 tables, one containing products, one purchases. I'm trying to find the average of the last 3 purchase prices for each product. So in the example below, for the product 'beans' I would want to return the average of the last 3 purchase prices before the product time 1230854663, i.e. average of customer C,D,E (239)
Products
+-------+------------+
| Name | time |
+-------+------------+
| beans | 1230854764 |
+-------+------------+
Purchases
+----------+------------+-------+
| Customer | time | price |
+----------+------------+-------+
| B | 1230854661 | 207 |
| C | 1230854662 | 444 |
| D | 1230854663 | 66 |
| E | 1230854764 | 88 |
| A | 1230854660 | 155 |
+----------+------------+-------+
I've come up with a nested select query which nearly gets me there i.e. it works if I hard code the time:
SELECT products.name,(SELECT avg(temp.price) FROM (select purchases.price from purchases WHERE purchases.time < 1230854764 order by purchases.time desc limit 3) temp) as av_price
from products products
But if the query references product.time rather than a hard coded time such as below I get an error that column products.time does not exist.
SELECT products.name,(SELECT avg(temp.price) FROM (select purchases.price from purchases WHERE purchases.time < products.time order by purchases.time desc limit 3) temp) as av_price from products products
I'm not sure if I'm making a simple mistake with nested queries or I'm going about this in totally the wrong way and should be using joins or some other construct, either way I'm stuck. Any help would be greatfully received.
The only problem in your query is you haven't mentioned products table in your inner query.
SELECT products.name,(SELECT avg(temp.price)
FROM (select purchases.price from purchases,products
WHERE purchases.time < products.time order by purchases.time desc limit 3) temp) as
av_price from products products