web-dev-qa-db-ja.com

2つの時点から価格を選択するSQL

datetimeの価格変更の時系列と、N個のイベントの別の異なる時系列がある場合、N個のクエリを実行せずに、各イベントの時点で最後の既知の価格をクエリするにはどうすればよいですか?基本的に、最新の既知の価格に一致するある種のCROSS-JOIN BETWEENクエリを作成する必要があります。

たとえば、価格変更履歴:

Changed At (time) | Price (money)
                1 |    10
                5 |    20
               10 |    30
               20 |    40

イベント:

Event Time | Nearest Matched Price (from above)
         0 | n/a
         3 | 10
         6 | 20
        10 | 30
        15 | 30

実際の使用例では、コストと価格を使用して在庫の動きの時系列を追跡しましたが、請求書の行と一緒にコストを保存することを怠っていました。私の在庫移動は通常、請求書の直前に発生しました。

暦日/週/月の売上を報告する同様のカレンダーレポートクエリがありますが、CROSS JOIN条件(> now && < next-day)が1行しか返さないためにのみ機能します。騒々しいクエリを許してください(それをクリーンアップします):

SELECT [t3].[FirstDateOfWeek] AS [Date], [t3].[value] AS [Total], [t3].[value2] AS [Count]
FROM (
    SELECT SUM([t0].[Total]) AS [value], COUNT(*) AS [value2], [t2].[FirstDateOfWeek]
    FROM [dbo].[vw_Invoices] AS [t0]
    LEFT OUTER JOIN [dbo].[Cases] AS [t1] ON ([t1].[CaseId]) = [t0].[CaseId]
    CROSS JOIN [dbo].[Calendar] AS [t2]
    WHERE ([t0].[Date] >= [t2].[CalendarDate]) AND ([t0].[Date] < [t2].[NextDayDateTime]) AND ([t0].[Date] >= @p0) AND ([t0].[Date] < @p1) AND (NOT ([t0].[IsVoided] = 1))
    GROUP BY [t2].[FirstDateOfWeek]
    ) AS [t3]
ORDER BY [t3].[FirstDateOfWeek]  

これはSQLでも可能ですか?おそらく、抽出された時間でグループ化された「現在の合計」クエリを使用しますか?

1
Petrus Theron

UNION ALLクエリで価格データとイベントデータを組み合わせると、問題は最後のNULL以外の値を見つけることになります。 Itzik Ben-Ganがその問題について書いています ここ

Idと呼ばれるキー列とcol1と呼ばれるNULL可能値列を持つテーブルT1が与えられると、idの順序に基づいて最後の非NULLcol1値を返します。

問題に戻って、以下はサンプルデータをカバーするために行ったデータ準備です(簡単にするためにINT列を使用しましたが、日時を格納する列に簡単に切り替えることができるはずです):

_CREATE TABLE #X_PRICE_CHANGE (CHANGED_TIME INT, PRICE INT);

INSERT INTO #X_PRICE_CHANGE
VALUES (1, 10), (5, 20), (10, 30), (20, 40);

CREATE TABLE #X_EVENT (EVENT_TIME INT);

INSERT INTO #X_EVENT
VALUES (0), (3), (6), (10), (15);
_

ウィンドウ関数の問題を解決する1つの方法は次のとおりです。

_SELECT 
  CHANGED_TIME AS EVENT_TIME
, CURRENT_PRICE
FROM
(
    SELECT
      CHANGED_TIME
    , SRC
    , MAX(PRICE) OVER (PARTITION BY CHANGED_TIME_OF_LAST_PRICE) CURRENT_PRICE
    FROM 
    (
        SELECT 
          CHANGED_TIME
        , PRICE
        , SRC
        , MAX(CASE WHEN SRC = 'PRICE' THEN CHANGED_TIME ELSE NULL END) 
            OVER (ORDER BY CHANGED_TIME ASC, SRC DESC 
            ) CHANGED_TIME_OF_LAST_PRICE
        FROM
        (
            SELECT 
            CHANGED_TIME, PRICE, 'PRICE' AS SRC
            FROM #X_PRICE_CHANGE

            UNION ALL

            SELECT 
            EVENT_TIME, NULL, 'EVENT' AS SRC
            FROM #X_EVENT
        ) t
    ) tt
) ttt
WHERE ttt.SRC = 'EVENT';
_

コードを段階的に見ていきましょう。 t派生テーブルは、価格とイベントデータを_UNION ALL_と組み合わせるだけです。ここでエキサイティングなことは何もありません:

_╔══════════════╦═══════╦═══════╗
║ CHANGED_TIME ║ PRICE ║  SRC  ║
╠══════════════╬═══════╬═══════╣
║            1 ║ 10    ║ PRICE ║
║            5 ║ 20    ║ PRICE ║
║           10 ║ 30    ║ PRICE ║
║           20 ║ 40    ║ PRICE ║
║            0 ║ NULL  ║ EVENT ║
║            3 ║ NULL  ║ EVENT ║
║            6 ║ NULL  ║ EVENT ║
║           10 ║ NULL  ║ EVENT ║
║           15 ║ NULL  ║ EVENT ║
╚══════════════╩═══════╩═══════╝
_

tt派生テーブルは、MAXウィンドウ関数をtに適用します。ウィンドウ関数の目的は、最新の価格を含む各「EVENT」行のchanged_timeを見つけることです。

_╔══════════════╦═══════╦═══════╦════════════════════════════╗
║ CHANGED_TIME ║ PRICE ║  SRC  ║ CHANGED_TIME_OF_LAST_PRICE ║
╠══════════════╬═══════╬═══════╬════════════════════════════╣
║            0 ║ NULL  ║ EVENT ║ NULL                       ║
║            1 ║ 10    ║ PRICE ║ 1                          ║
║            3 ║ NULL  ║ EVENT ║ 1                          ║
║            5 ║ 20    ║ PRICE ║ 5                          ║
║            6 ║ NULL  ║ EVENT ║ 5                          ║
║           10 ║ 30    ║ PRICE ║ 10                         ║
║           10 ║ NULL  ║ EVENT ║ 10                         ║
║           15 ║ NULL  ║ EVENT ║ 10                         ║
║           20 ║ 40    ║ PRICE ║ 20                         ║
╚══════════════╩═══════╩═══════╩════════════════════════════╝
_

_CHANGED_TIME_が15の行について考えてみます。_CHANGED_TIME_OF_LAST_PRICE_の値は10なので、戻って_CHANGED_TIME_OF_LAST_PRICE = 10_の「PRICE」行の価格値を取得できれば、次のようになります。 _CHANGED_TIME_が15の行の適切な価格。これは、3番目の派生テーブルtttで発生することです。

_╔══════════════╦═══════╦═══════════════╗
║ CHANGED_TIME ║  SRC  ║ CURRENT_PRICE ║
╠══════════════╬═══════╬═══════════════╣
║            0 ║ EVENT ║ NULL          ║
║            1 ║ PRICE ║ 10            ║
║            3 ║ EVENT ║ 10            ║
║            5 ║ PRICE ║ 20            ║
║            6 ║ EVENT ║ 20            ║
║           10 ║ PRICE ║ 30            ║
║           10 ║ EVENT ║ 30            ║
║           15 ║ EVENT ║ 30            ║
║           20 ║ PRICE ║ 40            ║
╚══════════════╩═══════╩═══════════════╝
_

MAX()ウィンドウ関数は、各パーティションでNULL以外の値を1つだけ検出します。ここでは、MAX()を使用して、PRICE値を「PRICE」行からすべての「EVENT」行に_CHANGED_TIME_OF_LAST_PRICE_の値が同じで効果的に塗りつぶします。

最後に、「PRICE」のソースを持つ不要な行を結果から削除します。ウィンドウ関数から正しい結果を得るにはこれらの行が必要でしたが、最終的な結果セットには含めたくありません。フィルタリング後のtttの結果は次のとおりです。

_╔════════════╦═══════════════╗
║ EVENT_TIME ║ CURRENT_PRICE ║
╠════════════╬═══════════════╣
║          0 ║ NULL          ║
║          3 ║ 10            ║
║          6 ║ 20            ║
║         10 ║ 30            ║
║         15 ║ 30            ║
╚════════════╩═══════════════╝
_

パフォーマンスの観点から、注意すべき重要な点の1つは、時間列が各テーブルで一意である場合、 ROWまたはRANGE句 から パフォーマンスの向上 にROWSを使用できることです。 =。

本当に凝ったものにしたい場合は、データをBINARYとしてキャストすることにより、ウィンドウ関数の1つを削除できます。 INT以外の列で機能するように、コードを調整する必要がある場合があります。これが1つの実装です:

_SELECT
  CHANGED_TIME
, CURRENT_PRICE
FROM
(
    SELECT 
      CHANGED_TIME
    , PRICE
    , SRC
    , CAST(SUBSTRING(MAX(binval) OVER (ORDER BY CHANGED_TIME ASC, SRC DESC 
        ), 5, 4) AS INT) CURRENT_PRICE
    FROM
    (
        SELECT 
        CHANGED_TIME, PRICE, 'PRICE' AS SRC
        , CAST(CHANGED_TIME AS BINARY(4)) + CAST(PRICE AS BINARY(4)) AS binval
        FROM #X_PRICE_CHANGE

        UNION ALL

        SELECT 
        EVENT_TIME, NULL, 'EVENT' AS SRC
        , CAST(EVENT_TIME AS BINARY(4)) + CAST(NULL AS BINARY(4)) AS binval
        FROM #X_EVENT
    ) t
) tt
WHERE tt.SRC = 'EVENT';
_

そのテクニックについてもっと知りたい場合は、sqlmagの記事を読むことをお勧めします。

3
Joe Obbish

迅速で汚い、ライブインスタンスではテストされていません。

イベントデータの各行には、Priceから1行が必要です。これは最近発生した行ですが、beforeイベントのタイムスタンプです。 TSQLはtop 1 .. order by表記をサポートしています。価格ルックアップをサブクエリとしてSELECTリストに埋め込むことにより、イベントの行ごとに1回実行されます。イベントの値に価格を予測すると、最新のものが確実に返されます。このようなもの:

select
    e.event_id,
    e.event_time,
    ( select top 1     -- return one value
        p.price
      from Prices as p
      where p.price_time <= e.event_time -- ensure the price change happened at or before the event
      order by p.price_time desc  -- top ensure "top 1" picks the price with the gretest i.e. most recent, timestamp
    ) as price
from Events as e;

サブクエリはイベントの行ごとに1回実行されるため、非常に大きなセットではパフォーマンスが低下する可能性があります。 Price.price_timeにインデックスがあることを確認してください。

3
Michael Green