T-SQLで隣接するレコードのみを集約する
-
04-07-2019 - |
質問
次のデータを含むテーブルがあります(例では簡略化されています)
Row Start Finish ID Amount
--- --------- ---------- -- ------
1 2008-10-01 2008-10-02 01 10
2 2008-10-02 2008-10-03 02 20
3 2008-10-03 2008-10-04 01 38
4 2008-10-04 2008-10-05 01 23
5 2008-10-05 2008-10-06 03 14
6 2008-10-06 2008-10-07 02 3
7 2008-10-07 2008-10-08 02 8
8 2008-10-08 2008-11-08 03 19
日付は期間を表し、IDはその期間中にシステムがあった状態であり、金額はその状態に関連する値です。
私がしたいのは、同じ ID番号を持つ隣接行の金額を集計することですが、連続した実行を結合できるように同じ全体シーケンスを保持します。したがって、次のようなデータになりたいと思います。
Row Start Finish ID Amount
--- --------- ---------- -- ------
1 2008-10-01 2008-10-02 01 10
2 2008-10-02 2008-10-03 02 20
3 2008-10-03 2008-10-05 01 61
4 2008-10-05 2008-10-06 03 14
5 2008-10-06 2008-10-08 02 11
6 2008-10-08 2008-11-08 03 19
SPに配置できるT-SQLソリューションを探していますが、単純なクエリでそれを行う方法がわかりません。何らかの反復を必要とするかもしれませんが、その道をたどりたくありません。
この集計を行う理由は、プロセスの次のステップが、シーケンス内で発生する一意のIDでグループ化されたSUM()およびCount()を実行することであるため、最終データは次のようになります:
ID Counts Total
-- ------ -----
01 2 71
02 2 31
03 2 33
ただし、単純に行う場合
SELECT COUNT(ID), SUM(Amount) FROM data GROUP BY ID
元のテーブルには次のようなものが表示されます
ID Counts Total
-- ------ -----
01 3 71
02 3 31
03 2 33
これは私が望むものではありません。
正しい解決策はありません
他のヒント
「SQLでの時間指向データベースアプリケーションの開発」という本を読んだ場合、 RT Snodgrass (そのpdfは出版物の下で彼のWebサイトから入手可能)、 p165-166の図6.25を見ると、現在の例で同じID値と連続時間間隔を持つさまざまな行をグループ化するために使用できる重要なSQLが見つかります。
以下のクエリ開発はほぼ修正済みですが、最後に最初のSELECTステートメントに原因がある問題があります。間違った答えが与えられる理由をまだ追跡していません。 [誰かがDBMSでSQLをテストし、そこで最初のクエリが正しく機能するかどうかを教えていただければ、とても助かります!]
次のようになります:
-- Derived from Figure 6.25 from Snodgrass "Developing Time-Oriented
-- Database Applications in SQL"
CREATE TABLE Data
(
Start DATE,
Finish DATE,
ID CHAR(2),
Amount INT
);
INSERT INTO Data VALUES('2008-10-01', '2008-10-02', '01', 10);
INSERT INTO Data VALUES('2008-10-02', '2008-10-03', '02', 20);
INSERT INTO Data VALUES('2008-10-03', '2008-10-04', '01', 38);
INSERT INTO Data VALUES('2008-10-04', '2008-10-05', '01', 23);
INSERT INTO Data VALUES('2008-10-05', '2008-10-06', '03', 14);
INSERT INTO Data VALUES('2008-10-06', '2008-10-07', '02', 3);
INSERT INTO Data VALUES('2008-10-07', '2008-10-08', '02', 8);
INSERT INTO Data VALUES('2008-10-08', '2008-11-08', '03', 19);
SELECT DISTINCT F.ID, F.Start, L.Finish
FROM Data AS F, Data AS L
WHERE F.Start < L.Finish
AND F.ID = L.ID
-- There are no gaps between F.Finish and L.Start
AND NOT EXISTS (SELECT *
FROM Data AS M
WHERE M.ID = F.ID
AND F.Finish < M.Start
AND M.Start < L.Start
AND NOT EXISTS (SELECT *
FROM Data AS T1
WHERE T1.ID = F.ID
AND T1.Start < M.Start
AND M.Start <= T1.Finish))
-- Cannot be extended further
AND NOT EXISTS (SELECT *
FROM Data AS T2
WHERE T2.ID = F.ID
AND ((T2.Start < F.Start AND F.Start <= T2.Finish)
OR (T2.Start <= L.Finish AND L.Finish < T2.Finish)));
そのクエリからの出力は次のとおりです。
01 2008-10-01 2008-10-02
01 2008-10-03 2008-10-05
02 2008-10-02 2008-10-03
02 2008-10-06 2008-10-08
03 2008-10-05 2008-10-06
03 2008-10-05 2008-11-08
03 2008-10-08 2008-11-08
編集済み:最後から2番目の行に問題があります-存在しないはずです。そして、それがどこから来たのか(まだ)わかりません。
ここで、その複雑な式を別のSELECTステートメントのFROM句のクエリ式として扱う必要があります。これにより、上記の最大範囲と重複するエントリで特定のIDの金額値が合計されます。
SELECT M.ID, M.Start, M.Finish, SUM(D.Amount)
FROM Data AS D,
(SELECT DISTINCT F.ID, F.Start, L.Finish
FROM Data AS F, Data AS L
WHERE F.Start < L.Finish
AND F.ID = L.ID
-- There are no gaps between F.Finish and L.Start
AND NOT EXISTS (SELECT *
FROM Data AS M
WHERE M.ID = F.ID
AND F.Finish < M.Start
AND M.Start < L.Start
AND NOT EXISTS (SELECT *
FROM Data AS T1
WHERE T1.ID = F.ID
AND T1.Start < M.Start
AND M.Start <= T1.Finish))
-- Cannot be extended further
AND NOT EXISTS (SELECT *
FROM Data AS T2
WHERE T2.ID = F.ID
AND ((T2.Start < F.Start AND F.Start <= T2.Finish)
OR (T2.Start <= L.Finish AND L.Finish < T2.Finish)))) AS M
WHERE D.ID = M.ID
AND M.Start <= D.Start
AND M.Finish >= D.Finish
GROUP BY M.ID, M.Start, M.Finish
ORDER BY M.ID, M.Start;
これにより、以下が得られます。
ID Start Finish Amount
01 2008-10-01 2008-10-02 10
01 2008-10-03 2008-10-05 61
02 2008-10-02 2008-10-03 20
02 2008-10-06 2008-10-08 11
03 2008-10-05 2008-10-06 14
03 2008-10-05 2008-11-08 33 -- Here be trouble!
03 2008-10-08 2008-11-08 19
編集済み:これは、元の質問で要求されたCOUNTおよびSUM集計を実行するための正しいデータセットであるため、最終的な答えは次のとおりです。
>SELECT I.ID, COUNT(*) AS Number, SUM(I.Amount) AS Amount
FROM (SELECT M.ID, M.Start, M.Finish, SUM(D.Amount) AS Amount
FROM Data AS D,
(SELECT DISTINCT F.ID, F.Start, L.Finish
FROM Data AS F, Data AS L
WHERE F.Start < L.Finish
AND F.ID = L.ID
-- There are no gaps between F.Finish and L.Start
AND NOT EXISTS
(SELECT *
FROM Data AS M
WHERE M.ID = F.ID
AND F.Finish < M.Start
AND M.Start < L.Start
AND NOT EXISTS
(SELECT *
FROM Data AS T1
WHERE T1.ID = F.ID
AND T1.Start < M.Start
AND M.Start <= T1.Finish))
-- Cannot be extended further
AND NOT EXISTS
(SELECT *
FROM Data AS T2
WHERE T2.ID = F.ID
AND ((T2.Start < F.Start AND F.Start <= T2.Finish) OR
(T2.Start <= L.Finish AND L.Finish < T2.Finish)))
) AS M
WHERE D.ID = M.ID
AND M.Start <= D.Start
AND M.Finish >= D.Finish
GROUP BY M.ID, M.Start, M.Finish
) AS I
GROUP BY I.ID
ORDER BY I.ID;
id number amount
01 2 71
02 2 31
03 3 66
レビュー: ああ! Drat ... 3のエントリには、必要な「量」の2倍があります。以前の「編集された」部分は、物事がうまくいかなくなった場所を示します。最初のクエリが微妙に間違っているか(別の質問を意図しているのかもしれません)、または私が使用しているオプティマイザが誤動作しているように見えます。それでも、これに密接に関連した答えがあり、正しい値が得られるはずです。
記録用:Solaris 10上のIBM Informix Dynamic Server 11.50でテスト済み。ただし、その他の標準に適度に準拠したSQL DBMSでも正常に動作するはずです。
おそらく、カーソルを作成し、結果をループ処理して、使用しているIDを追跡し、途中でデータを蓄積する必要があります。 IDが変更されると、蓄積されたデータを一時テーブルに挿入し、プロシージャの最後にテーブルを返すことができます(そこからすべてを選択します)。テーブルベースの関数の方が良いかもしれません。その場合は、戻りのテーブルに挿入するだけです。
何らかの反復を必要とする可能性があると思うが、その道をたどりたくない。
これがあなたがとらなければならないルートだと思います。カーソルを使ってテーブル変数に入力します。多数のレコードがある場合は、永続テーブルを使用して結果を保存できます。データを取得する必要がある場合は、新しいデータのみを処理できます。
デフォルトの0のビットフィールドをソーステーブルに追加して、処理されたレコードを追跡します。テーブルでselect *を使用しているユーザーがいないと仮定すると、デフォルト値の列を追加してもアプリケーションの残りの部分には影響しません。
ソリューションのコーディングの支援が必要な場合は、この投稿にコメントを追加してください。
まあ、結合とカーソルの混合を使用して反復ルートを下ることに決めました。データテーブルをそれ自体に結合することにより、連続したレコードのみのリンクリストを作成できます。
INSERT INTO #CONSEC
SELECT a.ID, a.Start, b.Finish, b.Amount
FROM Data a JOIN Data b
ON (a.Finish = b.Start) AND (a.ID = b.ID)
その後、カーソルでリストを繰り返し処理し、データテーブルを更新して調整します(そして、データテーブルから不要なレコードを削除します)
DECLARE CCursor CURSOR FOR
SELECT ID, Start, Finish, Amount FROM #CONSEC ORDER BY Start DESC
@Total = 0
OPEN CCursor
FETCH NEXT FROM CCursor INTO @ID, @START, @FINISH, @AMOUNT
WHILE @FETCH_STATUS = 0
BEGIN
@Total = @Total + @Amount
@Start_Last = @Start
@Finish_Last = @Finish
@ID_Last = @ID
DELETE FROM Data WHERE Start = @Finish
FETCH NEXT FROM CCursor INTO @ID, @START, @FINISH, @AMOUNT
IF (@ID_Last<> @ID) OR (@Finish<>@Start_Last)
BEGIN
UPDATE Data
SET Amount = Amount + @Total
WHERE Start = @Start_Last
@Total = 0
END
END
CLOSE CCursor
DEALLOCATE CCursor
これはすべて機能し、使用している一般的なデータに対して許容可能なパフォーマンスを備えています。
上記のコードに小さな問題が1つ見つかりました。もともと、カーソルを介して各ループでデータテーブルを更新していました。しかし、これはうまくいきませんでした。レコードに対して1つの更新しか実行できず、(データを追加し続けるために)複数の更新がレコードの元の内容の読み取りに戻るようです。