セットベースのアプローチを使用して、この問題を解決しようとしました。ただし、各行を確認する必要があるため、カーソルを使用する必要があると思います。私が間違っている場合は私を訂正してください。
テーブル:
Project, item, method, start, end
テーブルには複数のプロジェクト、複数のアイテムが含まれていますが、簡単にするために、ここでは1つのプロジェクト、1つのアイテムに減らしました。
データは次のようになります。
ABC, widget1, XY, 1000, 1033
ABC, widget1, XY, 1033, 1062
ABC, widget1, XY, 1062, 1112
ABC, widget1, XY, 1112, 1163
ABC, widget1, WW, 1163, 1223
ABC, widget1, WW, 1223, 1288
ABC, widget1, WW, 1288, 1334
ABC, widget1, XY, 1334, 1383
ABC, widget1, XY, 1383, 1425
この結果を返すクエリを作成したいと思います。
ABC, widget1, XY, 1000, 1163
ABC, widget1, WW, 1163, 1334
ABC, widget1, XY, 1334, 1425
これを行うための最良の方法は何ですか?
事前に計算されたギャップを保存し、制約を使用して、事前に計算されたデータが常に最新であることを確認できます。
これが表と最初の間隔です
CREATE TABLE dbo.IntegerSettings(SettingID INT NOT NULL,
IntValue INT NOT NULL,
StartedAt DATETIME NOT NULL,
FinishedAt DATETIME NOT NULL,
PreviousFinishedAt DATETIME NULL,
CONSTRAINT PK_IntegerSettings_SettingID_FinishedAt PRIMARY KEY(SettingID, FinishedAt),
CONSTRAINT UNQ_IntegerSettings_SettingID_PreviousFinishedAt UNIQUE(SettingID, PreviousFinishedAt),
CONSTRAINT FK_IntegerSettings_SettingID_PreviousFinishedAt
FOREIGN KEY(SettingID, PreviousFinishedAt)
REFERENCES dbo.IntegerSettings(SettingID, FinishedAt),
CONSTRAINT CHK_IntegerSettings_PreviousFinishedAt_NotAfter_StartedAt CHECK(PreviousFinishedAt <= StartedAt),
CONSTRAINT CHK_IntegerSettings_StartedAt_Before_FinishedAt CHECK(StartedAt < FinishedAt)
);
GO
INSERT INTO dbo.IntegerSettings(SettingID, IntValue, StartedAt, FinishedAt, PreviousFinishedAt)
VALUES(1, 1, '20070101', '20070103', NULL);
これには、ビジネスルールを実装するために連携する5つの制約があります。より複雑なものがどのように機能するかを示しましょう。もちろん、いくつかの制約は単純であるため、説明は必要ありません。
****
****
制約UNQ_IntegerSettings_SettingID_PreviousFinishedAtは、まさにそれを保証します。最初の間隔には前の間隔がありません。つまり、PreviousFinishedAt IS NULLです。UNIQUE制約は、設定ごとにそのような行が1つだけ存在できることを保証します。自分で確認してください。
INSERT INTO dbo.IntegerSettings(SettingID, IntValue, StartedAt, FinishedAt, PreviousFinishedAt)
VALUES(1, 1, '20070104', '20070105', NULL);
/*
Server: Msg 2627, Level 14, State 2, Line 1
Violation of UNIQUE KEY constraint 'UNQ_IntegerSettings_SettingID_PreviousFinishedAt'. Cannot insert duplicate key in object 'dbo.IntegerSettings'.
The statement has been terminated.
*/
****
****
制約CHK_IntegerSettings_PreviousFinishedAt_NotAfter_StartedAtは、まさにそれを保証します。自分で見て:
INSERT INTO dbo.IntegerSettings(SettingID, IntValue, StartedAt, FinishedAt, PreviousFinishedAt)
VALUES(1, 2, '20070104', '20070109', '20070105')
/*
Server: Msg 547, Level 16, State 1, Line 1
INSERT statement conflicted with TABLE CHECK constraint 'CHK_IntegerSettings_PreviousFinishedAt_NotAfter_StartedAt'. The conflict occurred in database 'RiskCenter', table 'IntegerSettings'.
The statement has been terminated.
*/
****
****
繰り返しますが、同じ制約UNQ_IntegerSettings_SettingID_PreviousFinishedAtは、以下に示すように、それを正確に保証します。
INSERT INTO dbo.IntegerSettings(SettingID, IntValue, StartedAt, FinishedAt, PreviousFinishedAt)
VALUES(1, 3, '20070104', '20070115', '20070103')
Msg 2627, Level 14, State 1, Line 1
Violation of UNIQUE KEY constraint 'UNQ_IntegerSettings_SettingID_PreviousFinishedAt'. Cannot insert duplicate key in object 'dbo.IntegerSettings'.
The statement has been terminated.
これは、重複がないことを意味します。
ご覧のとおり、時間枠ごとに、最大で1つ前に、最大で1つ後に続くことができます。次の間隔は、前の間隔が終了する前に開始することはできません。これらの2つのステートメントを合わせると、重複がないことを意味します。
****
****
次の制約を置き換えるだけで、ギャップを完全に禁止できます。
CONSTRAINT CHK_IntegerSettings_PreviousFinishedAt_NotAfter_StartedAt CHECK(PreviousFinishedAt <= StartedAt),
次のように、より厳密なものを使用します。
CONSTRAINT CHK_IntegerSettings_PreviousFinishedAt_EqualTo_StartedAt CHECK(PreviousFinishedAt = StartedAt),
ただし、ギャップを許可すると、ギャップを取得するクエリは次のように非常に単純でパフォーマンスが高くなります。
SELECT PreviousFinishedAt AS GapStart, StartedAt AS GapEnd
FROM dbo.IntegerSettings
WHERE StartedAt > PreviousFinishedAt;
この場合、カーソルは必要ありません。CTEは、この問題を解決するためのカーソルベースまたはループベースのアプローチよりもはるかに優れたパフォーマンスを発揮すると思います。
次のクエリは、まさに必要なものを提供します。 SQL Server 2008でテストしましたが、セットアップブロックを無視し、@table
をターゲットテーブルの名前に置き換えると、Oracle、SQL Server、またはなどのCTEをサポートするすべてのプラットフォームに対してこれを実行できるはずです。 PostgreSQL。
-- setup
DECLARE @table TABLE (
project VARCHAR(10) NOT NULL
, item VARCHAR(20) NOT NULL
, method CHAR(2) NOT NULL
, start INT NOT NULL
, [end] INT NOT NULL
);
INSERT INTO @table
VALUES
('ABC', 'widget1', 'XY', 1000, 1033)
, ('ABC', 'widget1', 'XY', 1033, 1062)
, ('ABC', 'widget1', 'XY', 1062, 1112)
, ('ABC', 'widget1', 'XY', 1112, 1163)
, ('ABC', 'widget1', 'WW', 1163, 1223)
, ('ABC', 'widget1', 'WW', 1223, 1288)
, ('ABC', 'widget1', 'WW', 1288, 1334)
, ('ABC', 'widget1', 'XY', 1334, 1383)
, ('ABC', 'widget1', 'XY', 1383, 1425)
;
-- query
WITH connected_ranges AS (
SELECT
right_range.project
, right_range.method
, right_range.item
, right_range.start
, right_range.[end]
FROM
@table left_range
RIGHT OUTER JOIN @table right_range
ON right_range.project = left_range.project
AND right_range.item = left_range.item
AND right_range.method = left_range.method
AND right_range.start = left_range.[end]
WHERE left_range.project IS NULL
UNION ALL
SELECT
right_range.project
, right_range.method
, right_range.item
, left_range.start
, right_range.[end]
FROM
connected_ranges left_range
INNER JOIN @table right_range
ON right_range.project = left_range.project
AND right_range.item = left_range.item
AND right_range.method = left_range.method
AND right_range.start = left_range.[end]
)
--SELECT *
--FROM connected_ranges
--ORDER BY
-- project
-- , method
-- , item
-- , start
-- , [end]
--;
SELECT
project
, method
, item
, start
, MAX([end]) AS [end]
FROM connected_ranges
GROUP BY
project
, method
, item
, start
;
私が行ったことを要約すると、再帰CTEを使用して、左端のエッジから右に向かって、すべての隣接するセグメントを結合します。次に、最後のSELECT
で、重複しない最大のセグメントのみをプルします。
Oracleでこれを解決する3つの方法を次に示します。 1つ目は、(インデックスがないと仮定して)1回の全表スキャンのみを実行する必要がある分析SQLソリューションです。 2つ目はNickChammasのCTEソリューションをOracle構文に変換したもので、3つ目はPipelined関数内でPL/SQLカーソルのFORループを使用したソリューションです。すべてのソリューションは、期待される結果を生み出します。これらのいくつかの行では、分析ソリューションが最もよく機能し、次に再帰的なCTEが続きます。どちらを操作するのが最も簡単かは、見る人の目にあります。
分析ソリューション
SELECT Project, Item, Method, Min("Start") "Start", Max("End") "End" FROM (
SELECT Project, Item, Method, "Start", "End"
, MAX(Change) OVER (PARTITION BY Project, Item ORDER BY "Start", "End") ChangeGroup
FROM (
SELECT Project, Item, Method, "Start", "End"
, CASE WHEN Method =
LAG(Method) OVER (PARTITION BY Project, Item ORDER BY "Start", "End") THEN NULL
ELSE Row_Number() OVER (ORDER BY Project, Item, "Start", "End")
END Change
FROM T1
)
)
GROUP BY Project, Item, Method, ChangeGroup
ORDER BY 1, 2, 4, 5;
CTEソリューション
WITH connected_ranges (Project, Method, Item, "Start", "End") AS (
SELECT right_range.project, right_range.method, right_range.item, right_range."Start"
, right_range."End"
FROM T1 left_range
RIGHT OUTER JOIN T1 right_range
ON right_range.project = left_range.project
AND right_range.item = left_range.item
AND right_range.method = left_range.method
AND right_range."Start" = left_range."End"
WHERE left_range.project IS NULL
UNION ALL
SELECT right_range.project, right_range.method, right_range.item, left_range."Start"
, right_range."End"
FROM connected_ranges left_range
INNER JOIN T1 right_range
ON right_range.project = left_range.project
AND right_range.item = left_range.item
AND right_range.method = left_range.method
AND right_range."Start" = left_range."End"
)
SELECT project, item, Method, "Start", MAX("End") AS "End"
FROM connected_ranges
GROUP BY project, method, item, "Start"
ORDER BY 1, 2, 4, 5;
PL/SQLカーソルFORループソリューション
CREATE OR REPLACE
CREATE OR REPLACE FUNCTION PipeResult Return
BEGIN
For vLoop In (
SELECT Project, Item, Method, "Start", "End"
, LAG(Method) OVER (PARTITION BY Project, Item ORDER BY "Start", "End") LagMethod
) Loop
Pipe Row
End Loop;
END;
/
CREATE OR REPLACE PACKAGE Example AUTHID DEFINER AS
Type tRow Is Record (
Project Varchar2(3)
, Item Varchar2(7)
, Method Varchar2(2)
, "Start" Number(4)
, "End" Number(4)
);
Type tTable Is Table of tRow;
Function PipelineResult(pDate In Date DEFAULT sysdate) Return tTable Pipelined;
END Example;
/
CREATE OR REPLACE PACKAGE BODY Example AS
Function PipelineResult(pDate In Date DEFAULT sysdate) Return tTable Pipelined AS
vSavedRow tRow;
Begin
For vRow IN (
SELECT Project, Item, Method, "Start", "End"
, LEAD(Method) OVER (PARTITION BY Project, Item ORDER BY "Start", "End") LeadMethod
FROM T1
ORDER BY Project, Item, "Start", "End"
)
Loop
If (vSavedRow.Method IS NULL) Then
vSavedRow.Project := vRow.Project;
vSavedRow.Item := vRow.Item;
vSavedRow.Method := vRow.Method;
vSavedRow."Start" := vRow."Start";
End If;
vSavedRow."End" := vRow."End";
If (vRow.Method <> vRow.LeadMethod OR vRow.LeadMethod IS NULL) Then
Pipe Row(vSavedRow);
vSavedRow.Method := NULL;
End If;
End Loop;
End;
END Example;
/
SELECT * FROM TABLE(Example.PipelineResult);
デモンストレーション環境:
DROP TABLE T1;
CREATE TABLE T1 AS (
SELECT 'ABC' Project, 'widget1' Item, 'XY' Method, 1000 "Start",
1033 "End"FROM dual);
INSERT INTO T1 VALUES ('ABC','widget1','XY',1033,1062);
INSERT INTO T1 VALUES ('ABC','widget1','XY',1062,1112);
INSERT INTO T1 VALUES ('ABC','widget1','XY',1112,1163);
INSERT INTO T1 VALUES ('ABC','widget1','WW',1163,1223);
INSERT INTO T1 VALUES ('ABC','widget1','WW',1223,1288);
INSERT INTO T1 VALUES ('ABC','widget1','WW',1288,1334);
INSERT INTO T1 VALUES ('ABC','widget1','XY',1334,1383);
INSERT INTO T1 VALUES ('ABC','widget1','XY',1383,1425);
COMMIT;