针对日期范围的SQL连接?

Her*_*ill 20 sql t-sql sql-server join date-range

考虑两个表:

交易,金额为外币:

     Date  Amount
========= =======
 1/2/2009    1500
 2/4/2009    2300
3/15/2009     300
4/17/2009    2200
etc.
Run Code Online (Sandbox Code Playgroud)

ExchangeRates,以外币的主要货币(比如美元)的价值:

     Date    Rate
========= =======
 2/1/2009    40.1
 3/1/2009    41.0
 4/1/2009    38.5
 5/1/2009    42.7
etc.
Run Code Online (Sandbox Code Playgroud)

可以输入任意日期的汇率 - 用户可以每天,每周,每月或不定期地输入汇率.

为了将外国金额换算成美元,我需要遵守这些规则:

A.如果可能,请使用最近的先前费率; 因此,2009年2月4日的交易使用2009年2月1日的汇率,2009年3月15日的交易使用3/1/2009的汇率.

B.如果没有为前一个日期定义的费率,请使用最早的可用费率.因此,1/2/2009的交易使用了2009年2月1日的汇率,因为没有定义更早的汇率.

这有效......

Select 
    t.Date, 
    t.Amount,
    ConvertedAmount=(   
        Select Top 1 
            t.Amount/ex.Rate
        From ExchangeRates ex
        Where t.Date > ex.Date
        Order by ex.Date desc
    )
From Transactions t
Run Code Online (Sandbox Code Playgroud)

......但是(1)似乎联合会更有效率和优雅,(2)它不涉及上面的规则B.

是否有替代使用子查询来找到合适的速率?是否有一种优雅的方式来处理规则B,而不是将自己束缚在结?

Luc*_*ero 20

您可以先对按日期排序的汇率进行自我加入,这样您就可以获得每个汇率的开始和结束日期,而日期中没有任何重叠或差距(可能会将其作为数据库的视图添加 - 在我的情况下,我只是使用一个公用表表达式).

现在,通过交易加入这些"准备好的"费率是简单而有效的.

就像是:

WITH IndexedExchangeRates AS (           
            SELECT  Row_Number() OVER (ORDER BY Date) ix,
                    Date,
                    Rate 
            FROM    ExchangeRates 
        ),
        RangedExchangeRates AS (             
            SELECT  CASE WHEN IER.ix=1 THEN CAST('1753-01-01' AS datetime) 
                    ELSE IER.Date 
                    END DateFrom,
                    COALESCE(IER2.Date, GETDATE()) DateTo,
                    IER.Rate 
            FROM    IndexedExchangeRates IER 
            LEFT JOIN IndexedExchangeRates IER2 
            ON IER.ix = IER2.ix-1 
        )
SELECT  T.Date,
        T.Amount,
        RER.Rate,
        T.Amount/RER.Rate ConvertedAmount 
FROM    Transactions T 
LEFT JOIN RangedExchangeRates RER 
ON (T.Date > RER.DateFrom) AND (T.Date <= RER.DateTo)
Run Code Online (Sandbox Code Playgroud)

笔记:

  • 你可以用GETDATE()遥远的未来取代日期,我假设在这里不知道未来的利率.

  • 规则(B)是通过将第一个已知汇率的日期设置为SQL Server支持的最小日期来实现的,该日期datetime应该(根据定义,如果它是您用于该Date列的类型)是可能的最小值.

  • 有没有理由不在最后一行使用 BETWEEN ? (2认同)
  • 是的,因为 BETWEEN 会返回错误的结果,它等价于 `(T.Date &gt;= RER.DateFrom) AND (T.Date &lt;= RER.DateTo)` 这不是你想要的,因为交易日期等于汇率日期将导致然后加入 2 行(其中之一将使用错误的汇率)。 (2认同)

Jon*_*ler 5

假设您有一个扩展的汇率表,其中包含:

 Start Date   End Date    Rate
 ========== ========== =======
 0001-01-01 2009-01-31    40.1
 2009-02-01 2009-02-28    40.1
 2009-03-01 2009-03-31    41.0
 2009-04-01 2009-04-30    38.5
 2009-05-01 9999-12-31    42.7
Run Code Online (Sandbox Code Playgroud)

我们可以讨论前两行是否应该合并的细节,但总体思路是找到给定日期的汇率是微不足道的。此结构与 SQL 'BETWEEN' 运算符一起使用,该运算符包括范围的结尾。通常,更好的范围格式是“开闭”;列出的第一个日期被包括在内,第二个被排除在外。请注意,数据行有一个限制——(a) 日期范围的覆盖范围没有间隙,(b) 覆盖范围没有重叠。强制执行这些约束并非完全微不足道(礼貌的轻描淡写 - 减数分裂)。

现在基本查询是微不足道的,情况 B 不再是特例:

SELECT T.Date, T.Amount, X.Rate
  FROM Transactions AS T JOIN ExtendedExchangeRates AS X
       ON T.Date BETWEEN X.StartDate AND X.EndDate;
Run Code Online (Sandbox Code Playgroud)

棘手的部分是根据给定的 ExchangeRate 表动态创建 ExtendedExchangeRate 表。如果这是一个选项,那么修改基本 ExchangeRate 表的结构以匹配 ExtendedExchangeRate 表将是一个好主意;您可以在输入数据时(每月一次)解决凌乱的问题,而不是每次需要确定汇率时(一天多次)。

如何创建扩展汇率表?如果您的系统支持从日期值中加或减 1 以获得第二天或前一天(并且有一个名为“Dual”的单行表),那么对此的变体将起作用(不使用任何 OLAP 函数):

CREATE TABLE ExchangeRate
(
    Date    DATE NOT NULL,
    Rate    DECIMAL(10,5) NOT NULL
);
INSERT INTO ExchangeRate VALUES('2009-02-01', 40.1);
INSERT INTO ExchangeRate VALUES('2009-03-01', 41.0);
INSERT INTO ExchangeRate VALUES('2009-04-01', 38.5);
INSERT INTO ExchangeRate VALUES('2009-05-01', 42.7);
Run Code Online (Sandbox Code Playgroud)

第一排:

SELECT '0001-01-01' AS StartDate,
       (SELECT MIN(Date) - 1 FROM ExchangeRate) AS EndDate,
       (SELECT Rate FROM ExchangeRate
         WHERE Date = (SELECT MIN(Date) FROM ExchangeRate)) AS Rate
FROM Dual;
Run Code Online (Sandbox Code Playgroud)

结果:

0001-01-01  2009-01-31      40.10000
Run Code Online (Sandbox Code Playgroud)

最后一行:

SELECT (SELECT MAX(Date) FROM ExchangeRate) AS StartDate,
       '9999-12-31' AS EndDate,
       (SELECT Rate FROM ExchangeRate
         WHERE Date = (SELECT MAX(Date) FROM ExchangeRate)) AS Rate
FROM Dual;
Run Code Online (Sandbox Code Playgroud)

结果:

2009-05-01  9999-12-31      42.70000
Run Code Online (Sandbox Code Playgroud)

中间行:

SELECT X1.Date     AS StartDate,
       X2.Date - 1 AS EndDate,
       X1.Rate     AS Rate
  FROM ExchangeRate AS X1 JOIN ExchangeRate AS X2
       ON X1.Date < X2.Date
 WHERE NOT EXISTS
       (SELECT *
          FROM ExchangeRate AS X3
         WHERE X3.Date > X1.Date AND X3.Date < X2.Date
        );
Run Code Online (Sandbox Code Playgroud)

结果:

2009-02-01  2009-02-28      40.10000
2009-03-01  2009-03-31      41.00000
2009-04-01  2009-04-30      38.50000
Run Code Online (Sandbox Code Playgroud)

请注意, NOT EXISTS 子查询相当重要。没有它,“中间行”结果是:

2009-02-01  2009-02-28      40.10000
2009-02-01  2009-03-31      40.10000    # Unwanted
2009-02-01  2009-04-30      40.10000    # Unwanted
2009-03-01  2009-03-31      41.00000
2009-03-01  2009-04-30      41.00000    # Unwanted
2009-04-01  2009-04-30      38.50000
Run Code Online (Sandbox Code Playgroud)

随着表格大小的增加,不需要的行数急剧增加(对于 N > 2 行,我相信有 (N-2) * (N - 3) / 2 个不需要的行)。

ExtendedExchangeRate 的结果是三个查询的(不相交的)UNION:

SELECT DATE '0001-01-01' AS StartDate,
       (SELECT MIN(Date) - 1 FROM ExchangeRate) AS EndDate,
       (SELECT Rate FROM ExchangeRate
         WHERE Date = (SELECT MIN(Date) FROM ExchangeRate)) AS Rate
FROM Dual
UNION
SELECT X1.Date     AS StartDate,
       X2.Date - 1 AS EndDate,
       X1.Rate     AS Rate
  FROM ExchangeRate AS X1 JOIN ExchangeRate AS X2
       ON X1.Date < X2.Date
 WHERE NOT EXISTS
       (SELECT *
          FROM ExchangeRate AS X3
         WHERE X3.Date > X1.Date AND X3.Date < X2.Date
        )
UNION
SELECT (SELECT MAX(Date) FROM ExchangeRate) AS StartDate,
       DATE '9999-12-31' AS EndDate,
       (SELECT Rate FROM ExchangeRate
         WHERE Date = (SELECT MAX(Date) FROM ExchangeRate)) AS Rate
FROM Dual;
Run Code Online (Sandbox Code Playgroud)

在测试 DBMS(MacOS X 10.6.2 上的 IBM Informix Dynamic Server 11.50.FC6)上,我能够将查询转换为视图,但我不得不停止欺骗数据类型 - 通过将字符串强制转换为日期:

CREATE VIEW ExtendedExchangeRate(StartDate, EndDate, Rate) AS
    SELECT DATE('0001-01-01')  AS StartDate,
           (SELECT MIN(Date) - 1 FROM ExchangeRate) AS EndDate,
           (SELECT Rate FROM ExchangeRate WHERE Date = (SELECT MIN(Date) FROM ExchangeRate)) AS Rate
    FROM Dual
    UNION
    SELECT X1.Date     AS StartDate,
           X2.Date - 1 AS EndDate,
           X1.Rate     AS Rate
      FROM ExchangeRate AS X1 JOIN ExchangeRate AS X2
           ON X1.Date < X2.Date
     WHERE NOT EXISTS
           (SELECT *
              FROM ExchangeRate AS X3
             WHERE X3.Date > X1.Date AND X3.Date < X2.Date
            )
    UNION 
    SELECT (SELECT MAX(Date) FROM ExchangeRate) AS StartDate,
           DATE('9999-12-31') AS EndDate,
           (SELECT Rate FROM ExchangeRate WHERE Date = (SELECT MAX(Date) FROM ExchangeRate)) AS Rate
    FROM Dual;
Run Code Online (Sandbox Code Playgroud)