28

次の 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) join の方が効率的でエレガントなように思われ、(2) 上記のルール B を扱っていません。

サブクエリを使用して適切なレートを見つける代わりの方法はありますか? そして、自分を複雑にせずにルール B を処理するエレガントな方法はありますか?

4

6 に答える 6

31

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

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

何かのようなもの:

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) 可能な限り最小の値である必要があります。

于 2010-02-21T16:17:50.070 に答える
6

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

 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' 演算子で機能します。多くの場合、範囲のより適切な形式は「open-closed」です。リストの最初の日付が含まれ、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 を加算または減算して翌日または前日を取得することをサポートしている場合 (および「デュアル」と呼ばれる単一行テーブルがある場合)、これのバリエーションが機能します (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;
于 2010-02-21T17:51:30.357 に答える
1

これをテストすることはできませんが、うまくいくと思います。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
于 2010-02-21T16:28:51.120 に答える
0
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が常に優先されます。次に、トランの日付ごとに最小の日付差を選択します。ランクオーバー句を使用します。

于 2010-02-21T16:57:08.510 に答える
0

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;

単一の列値を持つCROSSAPPLYは、SELECTあなたが示したように、句の相関サブクエリと機能的に同等であることに注意してください。CROSS APPLYの方がはるかに柔軟性があり、値を複数の場所で再利用でき、複数の行を(カスタムのピボット解除用に)使用でき、複数の列を使用できるため、今はCROSSAPPLYが好きです。

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;

どちらがパフォーマンスが優れているのか、またはどちらかがページ上の他の回答よりもパフォーマンスが優れているのかどうかはわかりません。日付列に適切なインデックスがあれば、それらはかなりうまくいくはずです-どのRow_Number()ソリューションよりも間違いなく優れています。

于 2013-02-11T07:28:55.857 に答える
0

多くのソリューションが機能します。ワークロードに最も適した (最も速い) ものを実際に見つける必要があります。通常、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)

このクエリを高速にするには、適切なインデックスが必要です。また、理想的には、JOINon"Date"ではなく on の"ID"ようなフィールド ( INTEGER) を使用する必要があります。スキーマ情報をもっと教えてください。例を作成します。

于 2010-02-21T18:16:15.070 に答える