SQL присоединиться к диапазонам дат?

рассмотрим две таблицы:

сделки, с суммами в иностранной валюте:

     Date  Amount
========= =======
 1/2/2009    1500
 2/4/2009    2300
3/15/2009     300
4/17/2009    2200
etc.

ExchangeRates, со значением основной валюты (скажем, долларов) в иностранной валюте:

     Date    Rate
========= =======
 2/1/2009    40.1
 3/1/2009    41.0
 4/1/2009    38.5
 5/1/2009    42.7
etc.

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

для того, чтобы перевести иностранные суммы чтобы доллары, мне нужно соблюдать эти правила:

A. Если возможно, используйте самую последнюю предыдущую ставку; поэтому транзакция на 2/4/2009 использует ставку на 2/1/2009, а транзакция на 3/15/2009 использует ставку на 3/1/2009.

B. Если для предыдущей даты не определена ставка, используйте самую раннюю доступную ставку. Таким образом, транзакция 1/2/2009 использует ставку для 2/1/2009, поскольку ранее не была определена ставка.

этот завод...

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

... но (1) кажется, что соединение будет более эффективным и элегантным, и (2) оно не имеет дело с правилом B выше.

есть ли альтернатива использованию подзапроса для поиска соответствующей ставки? И есть ли элегантный способ справиться с правилом B, не связывая себя узлами?

6 ответов


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

теперь присоединение к этим "подготовленным" ставкам с транзакциями просто и эффективно.

что-то например:

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)

Примечания:

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

  • правило (B) реализуется путем установки даты первого известного обменного курса на минимальную дату, поддерживаемую SQL Server datetime, который должен (по определению, если это тип, который вы используете для Date столбец) - наименьшее значение вероятный.


предположим, что у вас есть расширенная таблица обменного курса, содержащая:

 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

мы можем обсудить детали того, должны ли первые две строки быть объединены, но общая идея заключается в том, что тривиально найти обменный курс для данной даты. Эта структура работает с оператором SQL 'BETWEEN', который включает в себя концы диапазонов. Часто лучшим форматом для диапазонов является "open-closed"; первая дата включена, а вторая исключена. Обратите внимание, что ограничение на строки данных - 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;

сложная часть-Создание таблицы ExtendedExchangeRate из данной таблицы ExchangeRate на лету. Если это вариант, то пересмотр структуры базовая таблица ExchangeRate для соответствия таблице ExtendedExchangeRate была бы хорошей идеей; вы разрешаете беспорядочные вещи, когда данные вводятся (один раз в месяц), а не каждый раз, когда необходимо определить обменный курс (много раз в день).

как создать расширенную таблицу курсов? Если ваша система поддерживает добавление или вычитание 1 из значения даты для получения следующего или предыдущего дня (и имеет таблицу с одной строкой, называемую "двойной"), то вариант на этом будет работать (без используя любые функции 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);

первая строка:

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;

результат:

0001-01-01  2009-01-31      40.10000

последние строки:

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;

результат:

2009-05-01  9999-12-31      42.70000

середине строк:

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

результат:

2009-02-01  2009-02-28      40.10000
2009-03-01  2009-03-31      41.00000
2009-04-01  2009-04-30      38.50000

обратите внимание, что подзапрос 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

количество нежелательных строк резко увеличивается по мере увеличения размера таблицы (для N > 2 строк есть (N-2) * (N - 3) / 2 нежелательных строки, Я считаю).

результатом ExtendedExchangeRate является (непересекающееся) объединение трех запросов:

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;

на тестовой СУБД (IBM и Informix с динамической сервер 11.50.FC6 на MacOS X 10.6.2), я смог преобразовать запрос в представление, но мне пришлось прекратить мошенничество с типами данных-путем принуждения строк к датам:

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;

Я не могу проверить это, но я думаю, что это будет работать. Он использует объединение с двумя подзапросами для выбора скорости по правилу A или правилу B.

Select t.Date, t.Amount, 
  ConvertedAmount = t.Amount/coalesce(    
    (Select Top 1 ex.Rate 
        From ExchangeRates ex 
        Where t.Date > ex.Date 
        Order by ex.Date desc )
     ,
     (select top 1 ex.Rate 
        From ExchangeRates  
        Order by ex.Date asc)
    ) 
From Transactions t

SELECT 
    a.tranDate, 
    a.Amount,
    a.Amount/a.Rate as convertedRate
FROM
    (

    SELECT 
        t.date tranDate,
        e.date as rateDate,
        t.Amount,
        e.rate,
        RANK() OVER (Partition BY t.date ORDER BY
                         CASE WHEN DATEDIFF(day,e.date,t.date) < 0 THEN
                                   DATEDIFF(day,e.date,t.date) * -100000
                              ELSE DATEDIFF(day,e.date,t.date)
                         END ) AS diff
    FROM 
        ExchangeRates e
    CROSS JOIN 
        Transactions t
         ) a
WHERE a.diff = 1

вычисляется разница между tran и датой скорости, затем отрицательные значения (условие b) умножаются на -10000, чтобы их можно было ранжировать, но положительные значения (условие a всегда имеет приоритет. затем мы выбираем минимальную разницу дат для каждой даты tran, используя предложение rank over.


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

решение tie-breaker с учетом вашей схемы:

SELECT      t.Date,
            t.Amount,
            r.Rate
            --//add your multiplication/division here

FROM        "Transactions" t

INNER JOIN  "ExchangeRates" r
        ON  r."ExchangeRateID" = (
                        SELECT TOP 1 x."ExchangeRateID"
                        FROM        "ExchangeRates" x
                        WHERE       x."SourceCurrencyISO" = t."SourceCurrencyISO" --//these are currency-related filters for your tables
                                AND x."TargetCurrencyISO" = t."TargetCurrencyISO" --//,which you should also JOIN on
                                AND x."Date" <= t."Date"
                        ORDER BY    x."Date" DESC)

вы должны иметь правильные индексы для этого запроса, чтобы быть быстрым. Также В идеале у вас не должно быть JOIN on "Date", но на "ID"-как поле (INTEGER). Дайте мне больше информации о схеме, я создам пример для вы.


нет ничего о соединении, которое будет более элегантным, чем TOP 1 коррелированный подзапрос в исходном сообщении. Однако, как вы говорите, он не удовлетворяет требованию B.

эти запросы работают (требуется SQL Server 2005 или более поздняя версия). См.SqlFiddle для этих.

SELECT
   T.*,
   ExchangeRate = E.Rate
FROM
  dbo.Transactions T
  CROSS APPLY (
    SELECT TOP 1 Rate
    FROM dbo.ExchangeRate E
    WHERE E.RateDate <= T.TranDate
    ORDER BY
      CASE WHEN E.RateDate <= T.TranDate THEN 0 ELSE 1 END,
      E.RateDate DESC
  ) E;

обратите внимание, что крест применяется с одним значением столбца функционально эквивалентен коррелированному подзапросу в SELECT статья, Как вы показали. Я просто предпочитаю крест. Применить сейчас, потому что он гораздо более гибкий и позволяет повторно использовать значение в нескольких местах, иметь несколько строк в нем (для пользовательского распаковки) и позволяет иметь несколько столбцов.

SELECT
   T.*,
   ExchangeRate = Coalesce(E.Rate, E2.Rate)
FROM
  dbo.Transactions T
  OUTER APPLY (
    SELECT TOP 1 Rate
    FROM dbo.ExchangeRate E
    WHERE E.RateDate <= T.TranDate
    ORDER BY E.RateDate DESC
  ) E
  OUTER APPLY (
    SELECT TOP 1 Rate
    FROM dbo.ExchangeRate E2
    WHERE E.Rate IS NULL
    ORDER BY E2.RateDate
  ) E2;

Я не знаю, какой из них может работать лучше, или если любой из них будет работать лучше, чем другие ответы на странице. С правильным индексом на столбцах даты они должны zing довольно хорошо-определенно лучше, чем любой Row_Number() решение.