web-dev-qa-db-ja.com

2つの異なる列にまたがるギャップを順番に見つけて削除します

セットベースのアプローチを使用して、この問題を解決しようとしました。ただし、各行を確認する必要があるため、カーソルを使用する必要があると思います。私が間違っている場合は私を訂正してください。

テーブル:

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

これを行うための最良の方法は何ですか?

2
eeSteve

事前に計算されたギャップを保存し、制約を使用して、事前に計算されたデータが常に最新であることを確認できます。

これが表と最初の間隔です

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つの制約があります。より複雑なものがどのように機能するかを示しましょう。もちろん、いくつかの制約は単純であるため、説明は必要ありません。

****

  • 設定の最初の間隔は1つだけです

****

制約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.

*/

****

  • 2つの異なるウィンドウは、前のウィンドウと同じウィンドウを参照することはできません。

****

繰り返しますが、同じ制約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;
3
A-K

再帰CTEはこのジョブに最適です。

この場合、カーソルは必要ありません。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で、重複しない最大のセグメントのみをプルします。

2
Nick Chammas

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;
0
Leigh Riffel