web-dev-qa-db-ja.com

SQLは日付範囲に対して結合しますか?

次の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を処理するエレガントな方法はありますか?

20
Herb Caudill

最初に日付順に並べ替えられた為替レートで自己結合を実行し、日付の重複やギャップなしに各為替レートの開始日と終了日を取得できます(データベースにビューとして追加することもできます-私の場合、私は一般的なテーブル式を使用しています)。

現在、これらの「準備された」レートをトランザクションに結合することは簡単で効率的です。

何かのようなもの:

_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)は、可能な最小値です。

20
Lucero

以下を含む拡張為替レートテーブルがあるとします。

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

これはテストできませんが、うまくいくと思います。 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
1
Ray
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が常に優先されます。次に、各取引日の最小日差を選択します。ランクオーバー句を使用します。

0
Paul Creasey

多くのソリューションが機能します。ワークロードに最適な(最も速い)ものを本当に見つける必要があります。通常、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)を使用するのが理想的です。さらにスキーマ情報を教えてください。あなたのために例を作成します。

0
van

元の投稿の_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()ソリューションよりも間違いなく優れています。

0
ErikE