web-dev-qa-db-ja.com

開始範囲からすべての重複範囲を選択します

私の質問は これ に似ていますが、(私は)かなりの違いがあります。ベース範囲があり、それと相互に競合する他のすべての範囲をテーブル内で検索したい。つまり、範囲を形成するアイテムですが、実際には違いはありません。

アスタリスク付きの行が開始範囲です。範囲1、2、および3は、それを拡張することになっている範囲です。結果の範囲はXである必要があります。

1 | 
3 |    ===1====            ====
5 | ==2==    ====*====           ====
6 |             ====3====     =====          
--+-------------------------------------
  | |<--------X-------->|

私はこれを書いた:

WITH   cte
AS (
    SELECT DATA1.ID, DATA1.STARTDATE, DATA1.ENDDATE
    FROM DATA1
    WHERE 
    DATA1.ID = @PARID AND 
    DATA1.STARTDATE > @STARTDATE AND 
    DATA1.ENDDATE < @ENDDATE 

    UNION ALL

    SELECT DATA1.ID, DATA1.STARTDATE, DATA1.ENDDATE
    FROM cte 
    INNER JOIN DATA1 ON (DATA1.ID = cte.ID)
    WHERE 
    DATA1.ID = @PARID AND 
    (cte.STARTDATE < DATA1.ENDDATE AND cte.ENDDATE > DATA1.ENDDATE)
)
SELECT *
FROM cte

しかし、2番目のSELECTはSTARTDATEENDDATEcteから何を使用する必要があるかわからないため、いじくり回しても実際には機能しないことに気付きました。また、再帰的なSQLではサブクエリを使用できません。

これは、以前に一連の再帰関数を介して.NETのクライアントで計算しましたが、O(N ^ 2)では、非常に低速でした。ここでの目的は、計算をサーバーに移動し、計算を最適化することです。

私がここでやろうとしていることはすべて実行可能ですか?


SQLフィドル 。 (単純なデータ構造を使用していない)マシンで実行した場合と同じようにクエリを実行するのに問題がありますが、少なくともサンプルデータを追加しました。私はそれを自分のマシンで得たのと同じ結果を生み出すようにしていきます。

入力範囲が2018-08-24 12:00:00から2018-08-31 12:00:00の場合、正しい出力はID 2, 3, 4, 6になります。

5
rancor1223

単純な再帰的ソリューションは、タスクを2つの部分に分割します。

  1. 重複する範囲のチェーンに従って、最も早い開始日を見つけます
  2. 重複する範囲のチェーンに従って、最新の終了日を見つけます

詳細は次のとおりです。

1.開始日が最も早い重複のチェーンをたどる

  1. 開始範囲を見つける
  2. 開始日が現在より前の最も遅い重複範囲を見つける
  3. ステップ2に進む

コード例:

DECLARE @STARTDATE DATETIME = '2018-08-24 12:00:00';
DECLARE @ENDDATE DATETIME = '2018-08-31 12:00:00';

WITH MinStart AS
(
    -- Starting range
    SELECT TOP (1) 
        D.ID,
        D.STARTDATE
    FROM dbo.DATA1 AS D
    WHERE 
        D.STARTDATE >= @STARTDATE
        AND D.ENDDATE <= @ENDDATE
    ORDER BY 
        D.ID ASC

    UNION ALL

    SELECT
        P1.ID,
        P1.STARTDATE
    FROM 
    (
        -- Overlapping ranges with an earlier start date
        -- Numbered starting with the highest start date
        -- Tie-break dates using ID
        SELECT
            RN = ROW_NUMBER() OVER (
                ORDER BY D.STARTDATE DESC, D.ID ASC),
            D.ID,
            D.STARTDATE
        FROM MinStart AS R
        JOIN dbo.DATA1 AS D
            ON D.ENDDATE >= R.STARTDATE
            AND D.STARTDATE < R.STARTDATE
    ) AS P1
    WHERE
        -- Highest start date only
        P1.RN = 1
)
SELECT
    MS.ID,
    MS.STARTDATE
FROM MinStart AS MS
OPTION (MAXRECURSION 0);

2.最新の終了日に至る一連の重複を追跡する

  1. 開始範囲を見つける
  2. 現在よりも後の終了日が最も早い重複範囲を見つける
  3. ステップ2に進む

コード例:

DECLARE @STARTDATE DATETIME = '2018-08-24 12:00:00';
DECLARE @ENDDATE DATETIME = '2018-08-31 12:00:00';

WITH MaxEnd AS
(
    -- Starting range
    SELECT TOP (1) 
        D.ID, 
        D.ENDDATE
    FROM dbo.DATA1 AS D
    WHERE
        D.STARTDATE >= @STARTDATE
        AND D.ENDDATE <= @ENDDATE
    ORDER BY 
        D.ID ASC

    UNION ALL

    SELECT
        P1.ID,
        P1.ENDDATE
    FROM 
    (
        -- Overlapping ranges with a later end date
        -- Numbered starting with the lowest end date
        -- Tie-break dates using ID
        SELECT
            RN = ROW_NUMBER() OVER (
                ORDER BY D.ENDDATE ASC, D.ID ASC),
            D.ID,
            D.ENDDATE
        FROM MaxEnd AS R
        JOIN dbo.DATA1 AS D
            ON D.ENDDATE > R.ENDDATE
            AND D.STARTDATE < R.ENDDATE
    ) AS P1
    WHERE 
        -- Lowest end date only
        P1.RN = 1
)
SELECT
    ME.ID,
    ME.ENDDATE
FROM MaxEnd AS ME
OPTION (MAXRECURSION 0);

適切なインデックス付けにより、両方のクエリは次のような実行プランを使用します。

enter image description here

提供されたサンプルデータの結果は次のとおりです。

╔════╦═════════════════════════╗
║ID║STARTDATE║
╠════╬═════════════════════════╣
║3║2018-08-24 12:00 :00.000║
║6║2018-08-23 12:00:00.000║
║2║2018-08-22 12:00:00.000║
╚═══ ═╩═════════════════════════╝
╔════╦═══════════ ══════════════╗
║ID║ENDDATE║
╠════╬══════════════ ═══════════╣
║3║2018-08-29 12:00:00.000║
║6║2018-08-30 12:00:00.000║ 
║4║2018-09-02 12:00:00.000║
╚════╩════════════════════ ═════╝

したがって、ID 2、3、4、および6が使用されました。最終的な範囲は2018-08-22 12:00:00.000(見つかった最も低い開始日)から2018-09-02 12:00:00.000(見つかった最高の終了日)。

注:上記のコードには、サンプルデータの重複による日付のタイブレーク(ID列を使用)が含まれています。これは、実際の問題を解決するために必要な場合とそうでない場合があります。

使用されたインデックスは次のとおりです。

CREATE TABLE dbo.DATA1
(
  ID integer PRIMARY KEY,
  STARTDATE datetime NOT NULL,
  ENDDATE datetime NOT NULL,
);

CREATE UNIQUE INDEX i1 ON dbo.DATA1 (STARTDATE DESC, ID ASC) INCLUDE (ENDDATE);
CREATE UNIQUE INDEX i2 ON dbo.DATA1 (ENDDATE ASC, ID DESC) INCLUDE (STARTDATE);

デモ:db <> fiddle

これらのタイプのクエリには本当に最適なインデックス作成 難しい場合があります であることに注意してください。それでも、上記のアプローチは、この領域の多くの実用的なタスクによく機能します。

間隔クエリと述語の適切なサポートがSQL Serverに追加されるまで、より高いパフォーマンスのソリューションでは、かなりの追加作業が必要になります。見る:

4
Paul White 9

T-SQLのソリューションを紹介します。

Fiddle SQL Server 2017

問題:

少なくとも1つのアイテムが要求された期間と重複している時間に接続されている入力テーブルのすべてのレコードを検索します。

解決策:

ステップ1:すべての関連アイテムが何らかの方法で(オーバーラップまたは満たすことによって)時間的に関連付けられている日付範囲を決定します。

with packed as (
    select  min(StartDate) StartDate, max(EndDate) EndDate from (
        select StartDate, EndDate, sum(GroupStart) over (order by StartDate) Grp from (
            select StartDate, EndDate
                , case when max(EndDate) 
                    over (order by EndDate, StartDate rows between unbounded preceding and 1 preceding)
                        > StartDate
                    then 0 else 1 end GroupStart
            from dbo.DATA1
        ) p0
    ) p1
    group by Grp
)

ステップ2:要求された期間に関連する日付範囲を決定します

relevant as (
        select * from packed
        where StartDate < @ENDDATE
        and @STARTDATE < EndDate
)

ステップ3:関連する日付範囲で接続されているすべてのアイテムを出力します

select d.* from dbo.DATA1 d
inner join relevant r on d.StartDate < r.EndDate and r.StartDate < d.EndDate
where d.StartDate < @ENDDATE and @STARTDATE < d.EndDate
order by d.StartDate, d.EndDate, d.Id;

セットアップ:

CREATE TABLE dbo.DATA1
(
  ID integer PRIMARY KEY,
  STARTDATE datetime NOT NULL,
  ENDDATE datetime NOT NULL,
);

CREATE UNIQUE INDEX i1 ON dbo.DATA1 (STARTDATE DESC, ID ASC) INCLUDE (ENDDATE);
CREATE UNIQUE INDEX i2 ON dbo.DATA1 (ENDDATE ASC, ID DESC) INCLUDE (STARTDATE);

INSERT INTO DATA1 (ID, STARTDATE, ENDDATE) VALUES 
  (1, '2018-08-17 12:00:00', '2018-08-20 12:00:00'),
  (2, '2018-08-22 12:00:00', '2018-08-25 12:00:00'),
  (3, '2018-08-24 12:00:00', '2018-08-29 12:00:00'),
  (4, '2018-08-26 12:00:00', '2018-09-02 12:00:00'),
  (5, '2018-09-05 12:00:00', '2018-09-25 12:00:00'),
  (6, '2018-08-23 12:00:00', '2018-08-30 12:00:00'),
  (7, '2018-08-17 12:00:00', '2018-08-20 12:00:00'),
  (8, '2018-08-22 12:00:00', '2018-08-25 12:00:00'),
  (9, '2018-08-24 12:00:00', '2018-08-29 12:00:00'),
  (10, '2018-08-26 12:00:00', '2018-09-02 12:00:00'),
  (11, '2018-09-05 12:00:00', '2018-09-25 12:00:00'),
  (12, '2018-08-23 12:00:00', '2018-08-30 12:00:00');

完全なクエリ:

DECLARE @STARTDATE DATETIME = '2018-08-24 12:00:00';
DECLARE @ENDDATE DATETIME = '2018-08-31 12:00:00';

with packed as (
    select  min(StartDate) StartDate, max(EndDate) EndDate from (
        select StartDate, EndDate, sum(GroupStart) over (order by StartDate) Grp from (
            select StartDate, EndDate
                , case when max(EndDate) 
                    over (order by EndDate, StartDate rows between unbounded preceding and 1 preceding)
                        > StartDate
                    then 0 else 1 end GroupStart
            from dbo.DATA1
        ) p0
    ) p1
    group by Grp
)
, relevant as (
        select * from packed
        where StartDate < @ENDDATE
        and @STARTDATE < EndDate
)
select d.* from dbo.DATA1 d
inner join relevant r on d.StartDate < r.EndDate and r.StartDate < d.EndDate
where d.StartDate < @ENDDATE and @STARTDATE < d.EndDate
order by d.StartDate, d.EndDate, d.Id;
0
user179304

SQLを待ち望んでいますFiddleテストを許可するために、 http://sqlfiddle.com/#!18/10ab4/5 をご覧ください。

DECLARE @STARTDATE DATETIME = '2018-08-24 12:00:00';
DECLARE @ENDDATE DATETIME = '2018-08-29 12:00:00';
declare @PARID int = 3;

with ghp as (
  SELECT DATA1.ID, DATA1.STARTDATE, DATA1.ENDDATE
    from DATA1
    where ID = @PARID ),
     ghp2 as (
 select 'A' class, min(data1.startdate) minstart, max(data1.enddate) maxend
    FROM DATA1, ghp
    WHERE data1.enddate >= ghp.startdate
      and data1.startdate <= ghp.enddate
 union all
 select 'B', data1.startdate, data1.enddate
    from data1, ghp2
    WHERE (    data1.enddate   >= ghp2.minstart
           and data1.startdate <  ghp2.minstart )),
     ghp3 as (
 select 'C' class, min(data1.startdate) minstart, max(data1.enddate) maxend
    FROM DATA1, ghp
    WHERE data1.enddate >= ghp.startdate
      and data1.startdate <= ghp.enddate
 union all
 select 'D', data1.startdate, data1.enddate
    from data1, ghp2
    WHERE (    data1.enddate   > ghp2.maxend
           and data1.startdate <=  ghp2.maxend )
 )
select * from ghp2
union all
select * from ghp3
0
Gerard H. Pille