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.
Курсы обмена могут быть введены для произвольных дат - пользователь может вводить их ежедневно, еженедельно, ежемесячно или нерегулярно.
Чтобы перевести иностранные суммы в доллары, мне необходимо соблюдать эти правила:
а. Если возможно, используйте последний предыдущий курс; поэтому транзакция 2/4/2009 использует ставку за 2/1/2009, а транзакция 3/15/2009 использует ставку за 3/1/2009.
В. Если для предыдущей даты нет ставки, используйте самую раннюю доступную ставку. Таким образом, транзакция на 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, не привязывая себя к узлам?
Ответы
Ответ 1
Сначала вы можете сделать самостоятельное соединение по обменным курсам, которые упорядочены по дате, чтобы у вас были начальные и конечные даты каждого обменного курса, без каких-либо совпадений или пробелов в датах (возможно, добавьте это как представление к ваша база данных - в моем случае я просто использую общее табличное выражение).
Теперь объединение этих "подготовленных" ставок с транзакциями является простым и эффективным.
Что-то вроде:
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
) - наименьшее возможное значение.
Ответ 2
Предположим, что у вас была расширенная таблица обменных курсов, содержащая:
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", который включает в себя концы диапазонов. Часто лучший формат для диапазонов - "открытый-закрытый"; первая включенная дата включена, а вторая исключена. Обратите внимание, что есть ограничение на строки данных - есть (а) отсутствие пробелов в охвате диапазона дат и (б) отсутствие перекрытий в охвате. Выполнение этих ограничений не является полностью тривиальным (вежливое преуменьшение - мейоз).
Теперь базовый запрос тривиален, а Case 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 - это (непересекающийся) 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;
Ответ 3
Я не могу проверить это, но я думаю, что это сработает. Он использует coalesce с двумя подзапросами для выбора скорости по правилу 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
Ответ 4
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
Вычисляется разница между датой перехода и курсом, затем отрицательные значения (условие b) умножаются на -10000, так что они все равно могут быть ранжированы, но положительные значения (условие всегда имеет приоритет. Затем мы выбираем минимальную разницу дат для каждый тран с использованием позиции ранга.
Ответ 5
Многие решения будут работать. Вы действительно должны найти тот, который работает лучше всего (быстрее) для вашей рабочей нагрузки: обычно вы ищете для одной транзакции, список из них, все из них?
Решение тай-брейкера с учетом вашей схемы:
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
). Дайте мне больше информации о схеме, я создам вам пример.
Ответ 6
Нет ничего о соединении, которое будет более элегантным, чем коррелированный подзапрос 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()
.