MySQL выполняет подзапрос, если счетчик равен нулю

У меня есть три таблицы: monthly_revenue, currencies и foreign_exchange.

Таблица ежемесячных_доступа

|------------------------------------------------------|
| id | product_id | currency_id | value | month | year |
|------------------------------------------------------|
| 1  | 1          | 1           | 100   | 1     | 2015 |
| 2  | 1          | 2           | 125   | 1     | 2015 |
| 3  | 1          | 3           | 115   | 1     | 2015 |
| 4  | 1          | 1           | 100   | 2     | 2015 |
| 5  | 1          | 2           | 125   | 2     | 2015 |
| 6  | 1          | 3           | 115   | 2     | 2015 |
|------------------------------------------------------|

таблица foreign_exchange

|---------------------------------------|
| id | base | target | rate | rate_date |
|---------------------------------------|
| 1  | GBP  | USD    | 1.6  |2015-01-01 |
| 2  | GBP  | USD    | 1.62 |2015-01-15 |
| 3  | GBP  | USD    | 1.61 |2015-01-31 |
| 4  | EUR  | USD    | 1.2  |2015-01-01 |
| 5  | EUR  | USD    | 1.4  |2015-01-15 |
| 6  | EUR  | USD    | 1.4  |2015-01-31 |
| 7  | GBP  | EUR    | 1.4  |2015-01-01 |
| 8  | GBP  | EUR    | 1.45 |2015-01-15 |
| 9  | GBP  | EUR    | 1.44 |2015-01-31 |
|---------------------------------------|

Из этого можно видеть средние значения fx:

  • GBP > USD в январе - 1.61
  • EUR > USD в январе - 1.33
  • GBP > EUR в январе - 1.43

Для доллара США в качестве базовой валюты нет курсов, и в феврале нет ставок.

таблица валют

|-----------|
| id | name |
|-----------|
| 1  | GBP  |
| 2  | USD  |
| 3  | EUR  |
|-----------|

Что я пытаюсь достичь

Каждая строка в таблице monthly_revenue может иметь другую currency_id, так как размещены ордера - разные валюты. Я хочу видеть весь доход за данный месяц в единой валюте. Таким образом, вместо того, чтобы смотреть на весь доход в январе в фунтах стерлингов, а затем отдельно глядя на весь доход в январе в долларах США, я хотел бы получить одну стоимость за весь доход в январе - конвертированный в доллары США (например).

Это может быть рассчитано для каждой строки, используя следующее (используя январь для этого примера):

значение дохода x средняя ставка fx за январь между базовой и целевой валютой

Если у меня есть 50 заказов в январе, в 4 разных валютах, это позволит мне видеть весь доход в любой валюте.

Пример - получить весь доход в январе, в долларах США

Это должно возвратиться:

|------------------------------------------------------|
| id | product_id | currency_id | value | month | year |
|------------------------------------------------------|
| 1  | 1          | 1           | 100   | 1     | 2015 |
| 2  | 1          | 2           | 125   | 1     | 2015 |
| 3  | 1          | 3           | 115   | 1     | 2015 |
|------------------------------------------------------|

Однако строки 1 и 3 не указаны в долларах США (это GBP и EUR соответственно).

То, что я хотел бы увидеть, - это каждая строка, возвращенная со средней скоростью обмена валют, которая преобразуется в столбец converted. Например:

|-------------------------------------------------------------------------|
| id | prod_id | currency_id | value | month | year | fx_avg | converted  |
|-------------------------------------------------------------------------|
| 1  | 1       | 1           | 100   | 1     | 2015 | 1.61   | 161        |
| 2  | 1       | 2           | 125   | 1     | 2015 | 1      | 125        |
| 3  | 1       | 3           | 115   | 1     | 2015 | 1.33   | 152.95     |
|-------------------------------------------------------------------------|

Где я нахожусь

В настоящее время я могу получить базовый расчет, используя запрос ниже, но не хватает пары ключевых функций:

  • Если нет доступных курсов валют (например, для будущих дат, где, конечно, курс FX недоступен), то вся строка игнорируется. В этом случае я хотел бы использовать последний средний месяц.

  • Если вычисление выполняется там, где целевая валюта совпадает с базовой валютой, вся строка игнорируется (поскольку в таблице FX нет записи, где база равна цели). В этом случае скорость должна быть жесткой, как 1.

Запрос до сих пор

SELECT 
    r.value * IFNULL(AVG(fx.rate),1) as converted, AVG(fx.rate) as averageFx, 
    r.*, fx.*
FROM 
    foreign_exchange fx, monthly_revenue r, order_headers h
WHERE 
    fx.base IN (SELECT name FROM currencies WHERE id = r.currency_id) AND 
    r.order_header_id = h.id AND 
    fx.target = 'USD' AND 
    MONTH(fx.rate_date) = r.month AND
    YEAR(fx.rate_date) = r.year AND
    r.year = 2015
GROUP BY r.id
ORDER BY month ASC

Если для FX нет записей, похоже, что для получения среднего значения последних месячных ставок необходимо выполнить отдельный подзапрос.

Любой ввод будет оценен. Если какая-либо дополнительная информация требуется, напишите комментарий.

Спасибо.

Изменить Вот SQFiddle, в котором есть примеры схем и кода, который подчеркивает проблему.

Ответ 1

Вот приблизительная функция, которая вычисляет ваш обмен для данной валюты и начала месяца:

DELIMITER //
CREATE FUNCTION MonthRate(IN _curr CHAR(3) CHARACTER SET ascii,
                          IN _date DATE)
    RETURNS FLOAT
    DETERMINISTIC
BEGIN
    -- Note:  _date must be the first of some month, such as '2015-02-01'
    DECLARE _avg FLOAT;
    DECLARE _prev FLOAT;
    -- First, try to get the average for the month:
    SELECT AVG(rate) INTO _avg FROM foreign_exchange
        WHERE base = _curr
          AND target = 'USD'
          AND rate_date >= _date
          AND rate_date  < _date + INTERVAL 1 MONTH;
    IF _avg IS NOT NULL THEN
        RETURN _avg;
    END;
    -- Fall back onto the last rate before the month:
    SELECT rate INTO _prev
        FROM foreign_exchange
        WHERE base = _curr
          AND target = 'USD'
          AND rate_date < _date
        ORDER BY _date
        LIMIT 1;
    IF _prev IS NOT NULL THEN
        RETURN _prev;
    END;
    SELECT "Could not get value -- ran off start of Rates table";
END;
DELIMITER ;

Возможно, есть синтаксические ошибки и т.д. Но, надеюсь, вы сможете с ним работать.

Вызывать функцию можно из остальной части кода.

Для производительности это было бы полезно:

INDEX(base, target, rate_date, rate)

Ответ 2

Создать представление:

 create view avg_rate as
   select base, target, year(fx.rate_date) year, month(fx.rate_date) month,
          avg(rate) avg_rate 
   from  foreign_exchange group by base, target

Присоедините его дважды, один раз для текущего месяца и один раз для предыдущего

 select r.id, r.month, 
        r.value * avg(coalesce(cr.avg_rate, pr.avg_rate, 1)) converted,
        avg(coalesce(cr.avg_rate, pr.avg_rate), 0) rate

 from monthly_revenue r, avg_rate cr, avg_rate pr, order_headers h

 where
  r.year = 2015 and
  cr.year = r.year and cr.month = r.month  and cr.target='USD' and 
  pr.year = r.year and pr.month = r.month - 1  and pr.target='USD' and
  r.order_header_id = h.id
 group by r.id
 order by r.month

Также мне лично не нравится этот способ написания запроса и предпочитаю использовать явные объединения при логическом условии группировки и не иметь беспорядка в where where. то есть:.

 ... 
 from monthly_revenue r
    inner join order_headers h on r.order_header_id = h.id
    left join avg_rate cr on cr.year = r.year and cr.month = r.month  and cr.target='USD'
    left join avg_rate pr on pr.year = r.year and pr.month = r.month - 1 and pr.target='USD'

 where r.year = 2015

Ответ 3

http://sqlfiddle.com/#!9/6a41a/1

Эта сценария основана на исходном, но я добавил некоторые значения скорости за февраль и март, чтобы проверить и показать, как это работает.

 SELECT t.*,
  IF(@first=t.id, @flag := @flag+1,@flag:=1) `flag`,
  @first:=t.id
FROM
(SELECT 
    coalesce(fx.rate,1) `rate`, (r.value * coalesce(fx.rate,1)) as converted, 
    r.*, fx.base,fx.target, fx.avg_date, fx.rate frate
FROM 
    monthly_revenue r
LEFT JOIN
    currencies
ON r.currency_id = currencies.id
LEFT JOIN 
  (SELECT AVG(rate) `rate`, 
 `base`, 
 `target`,
 STR_TO_DATE(CONCAT('1/',MONTH(rate_date),'/',YEAR(rate_date)), '%d/%m/%Y') avg_date
 FROM foreign_exchange 
 GROUP BY `base`, `target`, `avg_date`
  ) fx
ON currencies.name = fx.base
   AND fx.target = 'USD'
   AND fx.avg_date <= STR_TO_DATE(CONCAT('1/',r.month,'/',r.year), '%d/%m/%Y')
ORDER BY r.id, fx.avg_date DESC) t
HAVING `flag` = 1

и если вам нужны записи только для определенного месяца, вы можете добавить WHERE до ORDER следующим образом:

WHERE r.month = 1 and r.year = 2015
ORDER BY r.id, fx.avg_date DESC) t

Ответ 4

Вы можете проверить этот запрос на приведенной ссылке: http://sqlfiddle.com/#!9/33def/2

select id,product_id,currency_id,currency_name,
    value,month,year,@prev_fx_avg:=ifnull(fx_avg,@prev_fx_avg) fx_avg,
    value*@prev_fx_avg as converted 
from (SELECT 
    r.id,r.product_id,r.currency_id,c.name as currency_name,
    r.value,r.month,r.year,if(c.name="USD",1,temp.avg_rate) as fx_avg

FROM 
    monthly_revenue r
    left join currencies c on r.currency_id=c.id
    left join

    (select base , avg(rate) as avg_rate, MONTH(fx.rate_date) month,
     YEAR(fx.rate_date) year
     from foreign_exchange fx
     where target="USD"
     group by base,target,MONTH(fx.rate_date),
    YEAR(fx.rate_date)) temp on(r.month=temp.month and r.year=temp.year and c.name=temp.base)

    group by r.id
    order by r.currency_id,r.month ASC, r.year ASC) final,(select @prev_fx_avg:=-1) temp2;