【问题标题】:MySQL perform subquery if count is zero如果计数为零,MySQL 执行子查询
【发布时间】:2015-08-17 13:18:07
【问题描述】:

我有三个表:monthly_revenuecurrenciesforeign_exchange

monthly_revenue 表

|------------------------------------------------------|
| 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 |
|---------------------------------------|

由此,我们可以看到平均外汇汇率:

  • 英镑 > 1 月美元为 1.61
  • EUR > 1 月美元为 1.33
  • GBP > 1 月欧元为 1.43

没有以美元为基础货币的汇率,也没有 2 月的汇率。

货币表

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

我想要实现的目标

monthly_revenue 表中的每一行都可以有不同的currency_id,因为所下的订单是不同的货币。我想以通用货币查看给定月份的所有收入。因此,与其以英镑查看 1 月份的所有收入,然后以美元分别查看 1 月份的所有收入,我想获得 1 月份所有收入的一个值 - 转换为美元(例如)。

可以使用以下公式为每一行计算(本示例使用一月):

收入值 x 1 月份基础货币和目标货币之间的平均汇率

如果我在 1 月份有 50 个订单,使用 4 种不同的货币,这让我可以查看任何单一货币的所有收入。

示例 - 获取 1 月份的所有收入,以美元为单位

这应该返回:

|------------------------------------------------------|
| 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 行不是美元(分别是英镑和欧元)。

我希望看到的是返回的每一行都包含转换为的平均外汇汇率,以及一个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 表中没有基础等于目标的记录)。在这种情况下,速率应该被硬定义为 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 的记录,看起来应该执行一个单独的子查询来获取最近一个月的平均汇率。

任何意见将不胜感激。如果需要任何进一步的信息,请发表评论。

谢谢。

编辑 Here is a SQFiddle 包含示例架构和突出问题的代码。

【问题讨论】:

  • “将记录的价值乘以产生收入的月份的平均外汇汇率”——这是一种标准的统计方法吗?!?!
  • 不,但为了这个问题的目的,这是一个简化。问题中的措辞有点偏离,但我会修改它。
  • 好的,你能提供一个同样的 sqlfiddle 吗?
  • 好的,请查看已编辑帖子中的链接。
  • 干得好。这几乎是现在应该如何在 SO 上提出此类问题的模型。

标签: mysql


【解决方案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 子句中出现混乱。即:

     ... 
     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
    

    【讨论】:

    • 你能提供一个 SQFIddle 吗?我无法让它工作。
    • @BigPun sqlfiddle.com/#!9/611e1/9 检查这个。请注意,您在最新查询中没有正确的分组操作(您按 r.id 分组,但使用 r.value 没有聚合,所以我添加了 sum )
    • 您的查询几乎完成了我所需要的,但并不完全。我已经修改了它 - sqlfiddle.com/#!9/604fc/1 但它的效率极低。您的版本仅提供上个月的平均费率 - 但如果我提前 3 个月报告,那么这将行不通。看看我的更新看看
    • @BigPun 这与我所做的查询不同。由于您的附加子查询,它大多效率低下。请参阅 - sqlfiddle.com/#!9/604fc/2 删除它,它已经是一半的时间。另一个问题是,由于您使用的是month(d),它不能在列上使用索引。通过在 foreign_exchange 中添加两列 - 年和月,您可以大大提高速度。
    • 同意,它效率低下,但它使我能够使用长达一年前的外汇汇率,这是要求之一。您的查询仅获取上个月的平均值。假设:我们现在是 8 月,但我想查看 2015 年的所有订阅收入。9 月、10 月、11 月和 12 月没有可用的汇率,因此您的查询将默认为“1”,这是不正确的。我的修改以牺牲性能为代价解决了这个问题——这就是我正在寻找的适当解决方案。
    【解决方案3】:

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

    这个小提琴基于你原来的小提琴,但我添加了 2 月和 3 月的一些费率值来测试和展示它是如何工作的。

     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
    

    如果您只需要特定月份的记录,您可以在ORDER 之前添加WHERE,如下所示:

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

    【讨论】:

    • 平均费率似乎不正确。在我的本地数据集中,我有 2015 年 8 月的收入记录以及当月的外汇汇率,但使用的平均值是 2012 年 11 月 1 日的(顺便说一句,这是给定货币最早可用的汇率之一)
    • 我不明白你的意思,你能否提供更正的数据集并指出返回的错误记录?
    【解决方案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;
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2018-04-16
      • 2011-11-21
      • 2011-09-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多