How can I calculate prices based on currency table in one select?

Viewed 61

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

1 Answers

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