次の2つのテーブルについて考えます。
トランザクション、外貨での金額:
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.可能であれば、最新の以前のレートを使用してください。したがって、2009年2月4日のトランザクションは2009年2月1日のレートを使用し、2009年3月15日のトランザクションは2009年3月1日のレートを使用します。
B.前の日付のレートが定義されていない場合は、利用可能な最も早いレートを使用します。したがって、以前のレートが定義されていないため、2009年1月2日のトランザクションは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
...(1)結合の方が効率的でエレガントなようです。(2)上記のルールBには対応していません。
適切なレートを見つけるためにサブクエリを使用する代わりの方法はありますか?そして、結び目に縛られることなく、ルールBを処理するエレガントな方法はありますか?
最初に日付順に並べ替えられた為替レートで自己結合を実行し、日付の重複やギャップなしに各為替レートの開始日と終了日を取得できます(データベースにビューとして追加することもできます-私の場合、私は一般的なテーブル式を使用しています)。
現在、これらの「準備された」レートをトランザクションに結合することは簡単で効率的です。
何かのようなもの:
_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
column)は、可能な最小値です。
以下を含む拡張為替レートテーブルがあるとします。
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
最初の2行を組み合わせる必要があるかどうかの詳細については説明できますが、一般的な考え方は、特定の日付の為替レートを見つけることは簡単なことです。この構造は、範囲の終了を含むSQLの「BETWEEN」演算子で機能します。多くの場合、範囲のより適切な形式は「オープン-クローズ」です。リストされている最初の日付が含まれ、2番目の日付は除外されます。データ行には制約があることに注意してください。(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;
トリッキーな部分は、所定のExchangeRateテーブルからその場でExtendedExchangeRateテーブルを作成することです。オプションの場合は、ExtendedExchangeRateテーブルと一致するように基本的なExchangeRateテーブルの構造を修正することをお勧めします。為替レートを決定する必要があるたび(1日に数回)ではなく、データを入力するときに(月に1回)厄介な問題を解決します。
拡張為替レート表を作成するにはどうすればよいですか?システムが日付値から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の結果は、3つのクエリの(素の)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;
テスト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;
これはテストできませんが、うまくいくと思います。 2つのサブクエリとの合体を使用して、ルール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
取引日と利率の日付の差が計算され、負の値(条件b)に-10000が乗算されるため、ランク付けできますが、正の値(条件aが常に優先されます。次に、各取引日の最小日差を選択します。ランクオーバー句を使用します。
多くのソリューションが機能します。ワークロードに最適な(最も速い)ものを本当に見つける必要があります。通常、1つのトランザクション、それらのリスト、それらすべてを検索しますか?
スキーマを考慮したタイブレーカーソリューションは次のとおりです。
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)
このクエリを高速にするには、適切なインデックスが必要です。また、"Date"
ではなくJOIN
を使用するのではなく、"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;
_
単一の列の値を持つCROSS APPLYは、機能的にはSELECT
句の相関サブクエリと同じです。 CROSS APPLYの方がはるかに柔軟で、複数の場所で値を再利用でき、複数の行を(カスタムアンピボット用に)持つことができ、複数の列を持つことができるので、私は今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()
ソリューションよりも間違いなく優れています。