SQL присоединяется к диапазонам дат? - PullRequest
19 голосов
/ 21 февраля 2010

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

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

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

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

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

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

Чтобы перевести иностранные суммы в доллары, мне необходимо соблюдать следующие правила:

A. Если возможно, используйте самый последний предыдущий курс; таким образом, транзакция от 04.02.2009 использует тариф за 01.02.2009, а транзакция от 15.03.2009 использует курс за 3/1/2009.

B. Если для предыдущей даты не определена ставка, используйте самую раннюю из доступных. Таким образом, транзакция на 1/2/2009 использует ставку на 01.02.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 ]

20 голосов
/ 21 февраля 2010

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

Теперь объединить эти «подготовленные» тарифы с транзакциями просто и эффективно.

Что-то вроде:

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) будет наименьшим возможным значением.

3 голосов
/ 21 февраля 2010

Предположим, у вас есть расширенная таблица обменных курсов, которая содержит:

 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, который включает в себя концы диапазонов. Часто лучшим форматом для диапазонов является «открытый-закрытый»; первая указанная дата включена, а вторая исключена. Обратите внимание, что существует ограничение на строки данных: (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 из значения даты для получения следующего или предыдущего дня (и имеет одну таблицу строк с именем «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);

Первый ряд:

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 является (непересекающийся) 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;

В тестовой СУБД (IBM Informix Dynamic Server 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;
1 голос
/ 21 февраля 2010

Я не могу это проверить, но думаю, что это сработает. Он использует объединение с двумя подзапросами для выбора скорости по правилу 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
0 голосов
/ 11 февраля 2013

В объединении нет ничего более элегантного, чем коррелированный подзапрос 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;

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

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;

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

0 голосов
/ 21 февраля 2010

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

Решение для разрешения конфликтов с учетом вашей схемы:

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 на "Date", а на "ID" -подобном поле (INTEGER). Дайте мне больше информации о схеме, я создам пример для вас.

0 голосов
/ 21 февраля 2010
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

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

...