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()
решение.