WHILEループのないクエリ


18

以下のような予定表があります。各予定は、「新規」または「フォローアップ」として分類する必要があります。(患者の)最初の予約から30日以内の(患者の)予約はフォローアップです。30日後、予定は再び「新規」になります。30日以内の予定はすべて「フォローアップ」になります。

現在、whileループと入力してこれを行っています。
WHILEループなしでこれを実現するにはどうすればよいですか?

ここに画像の説明を入力してください

テーブル

CREATE TABLE #Appt1 (ApptID INT, PatientID INT, ApptDate DATE)
INSERT INTO #Appt1
SELECT  1,101,'2020-01-05' UNION
SELECT  2,505,'2020-01-06' UNION
SELECT  3,505,'2020-01-10' UNION
SELECT  4,505,'2020-01-20' UNION
SELECT  5,101,'2020-01-25' UNION
SELECT  6,101,'2020-02-12'  UNION
SELECT  7,101,'2020-02-20'  UNION
SELECT  8,101,'2020-03-30'  UNION
SELECT  9,303,'2020-01-28' UNION
SELECT  10,303,'2020-02-02' 

私はあなたの画像を見ることができませんが、確認したいのですが、お互いに20日ごとに3つのアポイントメントがある場合、最後のアポイントメントはまだ「フォローアップ」権限です。まだ途中から20日も経っていません。これは本当ですか?
pwilcox

@pwilcoxいいえ。3つ目は、図に示すように新しい予定です
LCJ

ループオーバーfast_forwardカーソルがおそらく最良の選択肢ですが、パフォーマンスは賢明です。
DavidדודוMarkovitz

回答:


14

再帰クエリを使用する必要があります。

30日の期間は、前から開始してカウントされます(再帰/風変わりな更新/ループなしでは実行できません)。そのため、既存の回答をすべて使用ROW_NUMBERしても失敗しました。

WITH f AS (
  SELECT *, rn = ROW_NUMBER() OVER(PARTITION BY PatientId ORDER BY ApptDate) 
  FROM Appt1
), rec AS (
  SELECT Category = CAST('New' AS NVARCHAR(20)), ApptId, PatientId, ApptDate, rn, startDate = ApptDate
  FROM f
  WHERE rn = 1
  UNION ALL
  SELECT CAST(CASE WHEN DATEDIFF(DAY,  rec.startDate,f.ApptDate) <= 30 THEN N'FollowUp' ELSE N'New' END AS NVARCHAR(20)), 
         f.ApptId,f.PatientId,f.ApptDate, f.rn,
         CASE WHEN DATEDIFF(DAY, rec.startDate, f.ApptDate) <= 30 THEN rec.startDate ELSE f.ApptDate END
  FROM rec
  JOIN f
    ON rec.rn = f.rn - 1
   AND rec.PatientId = f.PatientId
)
SELECT ApptId, PatientId, ApptDate, Category
FROM rec
ORDER BY PatientId, ApptDate;  

db <> fiddleデモ

出力:

+---------+------------+-------------+----------+
| ApptId  | PatientId  |  ApptDate   | Category |
+---------+------------+-------------+----------+
|      1  |       101  | 2020-01-05  | New      |
|      5  |       101  | 2020-01-25  | FollowUp |
|      6  |       101  | 2020-02-12  | New      |
|      7  |       101  | 2020-02-20  | FollowUp |
|      8  |       101  | 2020-03-30  | New      |
|      9  |       303  | 2020-01-28  | New      |
|     10  |       303  | 2020-02-02  | FollowUp |
|      2  |       505  | 2020-01-06  | New      |
|      3  |       505  | 2020-01-10  | FollowUp |
|      4  |       505  | 2020-01-20  | FollowUp |
+---------+------------+-------------+----------+

使い方:

  1. f-開始点を取得します(アンカー-すべての患者IDごと)
  2. rec-現在の値と前の値の差が30より大きい場合、再帰部分、PatientIdのコンテキストでカテゴリと開始点を変更
  3. メイン-ソートされた結果セットを表示する

類似のクラス:

Oracleでの条件付き SUM-ウィンドウ関数の制限

セッションウィンドウ(Azure Stream Analytics)

特定の条件が満たされるまで合計を実行 -風変わりな更新


補遺

本番環境でこのコードを使用しないでください!

しかし、cteを使用する以外に言及する価値がある別のオプションは、「ラウンド」で一時テーブルを使用して更新することです。

「単一」ラウンドで実行できます(風変わりな更新):

CREATE TABLE Appt_temp (ApptID INT , PatientID INT, ApptDate DATE, Category NVARCHAR(10))

INSERT INTO Appt_temp(ApptId, PatientId, ApptDate)
SELECT ApptId, PatientId, ApptDate
FROM Appt1;

CREATE CLUSTERED INDEX Idx_appt ON Appt_temp(PatientID, ApptDate);

クエリ:

DECLARE @PatientId INT = 0,
        @PrevPatientId INT,
        @FirstApptDate DATE = NULL;

UPDATE Appt_temp
SET  @PrevPatientId = @PatientId
    ,@PatientId     = PatientID 
    ,@FirstApptDate = CASE WHEN @PrevPatientId <> @PatientId THEN ApptDate
                           WHEN DATEDIFF(DAY, @FirstApptDate, ApptDate)>30 THEN ApptDate
                           ELSE @FirstApptDate
                      END
    ,Category       = CASE WHEN @PrevPatientId <> @PatientId THEN 'New'
                           WHEN @FirstApptDate = ApptDate THEN 'New'
                           ELSE 'FollowUp' 
                      END
FROM Appt_temp WITH(INDEX(Idx_appt))
OPTION (MAXDOP 1);

SELECT * FROM  Appt_temp ORDER BY PatientId, ApptDate;

db <> fiddle風変わりな更新


1
あなたの論理は私のものと非常によく似ています。大きな違いを説明できますか?
pwilcox

@pwilcox私がこの回答を書いたとき、すべての既存の回答が機能していない単純なrow_numberを使用していたので、私は自分のバージョンを提供しました
Lukasz Szozda

ええ、私は答えに早すぎました。それについてコメントしてくれたThx。
Irdis

2
SQLサーバーがRANGE x PRECEDING句を正しく実装するまでは、rcteがこれに対する唯一の解決策であると思います。
Salman A

1
@LCJひと味違うの更新は、「文書化されていない」行動に基づいており、それは、予告(なし任意の時点で変更される可能性がred-gate.com/simple-talk/sql/learn-sql-server/...
ルカシュSzozda

5

再帰的なcteでこれを行うことができます。最初に、各患者内のapptDateで注文する必要があります。それは、ありふれたcteによって達成できます。

次に、再帰cteのアンカー部分で、各患者の最初の注文を選択し、ステータスを「新規」としてマークし、apptDateを最新の「新規」レコードの日付としてマークします。

再帰cteの再帰部分で、次のアポイントメントまでインクリメントし、現在のアポイントメントと最新の「新しい」アポイントメント日付の間の日数の差を計算します。30日を超える場合は、「新規」とマークし、最新の新しい予定日をリセットします。それ以外の場合は、「フォローアップ」としてマークし、新しい予定日以降の既存の日をそのまま渡します。

最後に、基本クエリで、必要な列を選択するだけです。

with orderings as (

    select       *, 
                 rn = row_number() over(
                     partition by patientId 
                     order by apptDate
                 ) 
    from         #appt1 a

),

markings as (

    select       apptId, 
                 patientId, 
                 apptDate, 
                 rn, 
                 type = convert(varchar(10),'new'),
                 dateOfNew = apptDate
    from         orderings 
    where        rn = 1

    union all
    select       o.apptId, o.patientId, o.apptDate, o.rn,
                 type = convert(varchar(10),iif(ap.daysSinceNew > 30, 'new', 'follow up')),
                 dateOfNew = iif(ap.daysSinceNew > 30, o.apptDate, m.dateOfNew)
    from         markings m
    join         orderings o 
                     on m.patientId = o.patientId 
                     and m.rn + 1 = o.rn
    cross apply  (select daysSinceNew = datediff(day, m.dateOfNew, o.apptDate)) ap

)

select    apptId, patientId, apptDate, type
from      markings
order by  patientId, rn;

Abhijeet Khandagaleの回答がより簡単なクエリでニーズを満たしているようだったので(少し書き直した後)、最初にこの回答を削除したことを述べておきます。しかし、あなたのビジネス要件と追加されたサンプルデータについての彼のコメントを参考にして、これはあなたのニーズを満たしていると信じているので、私は削除を取り消しました。


4

それが正確に実装したものかどうかはわかりません。しかし、cteを使用する以外に言及する価値がある別のオプションは、「ラウンド」で一時テーブルと更新を使用することです。したがって、すべてのステータスが正しく設定されていないときに一時テーブルを更新し、反復的な方法で結果をビルドします。単にローカル変数を使用して、反復回数を制御できます。

したがって、各反復を2つの段階に分割します。

  1. 新規レコードに近いすべてのフォローアップ値を設定します。これは、正しいフィルタを使用するだけでかなり簡単です。
  2. ステータスが設定されていない残りのレコードについては、同じ患者IDのグループから最初に選択できます。また、最初の段階で処理されていないため、新しいものであると言います。

そう

CREATE TABLE #Appt2 (ApptID INT, PatientID INT, ApptDate DATE, AppStatus nvarchar(100))

select * from #Appt1
insert into #Appt2 (ApptID, PatientID, ApptDate, AppStatus)
select a1.ApptID, a1.PatientID, a1.ApptDate, null from #Appt1 a1
declare @limit int = 0;

while (exists(select * from #Appt2 where AppStatus IS NULL) and @limit < 1000)
begin
  set @limit = @limit+1;
  update a2
  set
    a2.AppStatus = IIF(exists(
        select * 
        from #Appt2 a 
        where 
          0 > DATEDIFF(day, a2.ApptDate, a.ApptDate) 
          and DATEDIFF(day, a2.ApptDate, a.ApptDate) > -30 
          and a.ApptID != a2.ApptID 
          and a.PatientID = a2.PatientID
          and a.AppStatus = 'New'
          ), 'Followup', a2.AppStatus)
  from #Appt2 a2

  --select * from #Appt2

  update a2
  set a2.AppStatus = 'New'
  from #Appt2 a2 join (select a.*, ROW_NUMBER() over (Partition By PatientId order by ApptId) rn from (select * from #Appt2 where AppStatus IS NULL) a) ar
  on a2.ApptID = ar.ApptID
  and ar.rn = 1

  --select * from #Appt2

end

select * from #Appt2 order by PatientID, ApptDate

drop table #Appt1
drop table #Appt2

更新。Lukaszから提供されたコメントを読んでください。はるかに賢い方法です。答えはアイデアとして残しておきます。


4

再帰的な共通式はループを回避してクエリを最適化する優れた方法だと思いますが、場合によってはパフォーマンスが低下する可能性があるため、可能であれば回避する必要があります。

以下のコードを使用して問題を解決し、より多くの値をテストしますが、実際のデータでもテストすることをお勧めします。

WITH DataSource AS
(
    SELECT *
          ,CEILING(DATEDIFF(DAY, MIN([ApptDate]) OVER (PARTITION BY [PatientID]), [ApptDate]) * 1.0 / 30 + 0.000001) AS [GroupID]
    FROM #Appt1
)
SELECT *
     ,IIF(ROW_NUMBER() OVER (PARTITION BY [PatientID], [GroupID] ORDER BY [ApptDate]) = 1, 'New', 'Followup')
FROM DataSource
ORDER BY [PatientID]
        ,[ApptDate];

ここに画像の説明を入力してください

アイデアは非常に単純です。グループ(30日)でレコードを分離する必要newがありfollow upsます。ステートメントがどのように作成されるかを確認します。

SELECT *
      ,DATEDIFF(DAY, MIN([ApptDate]) OVER (PARTITION BY [PatientID]), [ApptDate])
      ,DATEDIFF(DAY, MIN([ApptDate]) OVER (PARTITION BY [PatientID]), [ApptDate]) * 1.0 / 30
      ,CEILING(DATEDIFF(DAY, MIN([ApptDate]) OVER (PARTITION BY [PatientID]), [ApptDate]) * 1.0 / 30 + 0.000001) 
FROM #Appt1
ORDER BY [PatientID]
        ,[ApptDate];

ここに画像の説明を入力してください

そう:

  1. まず、各グループの最初の日付を取得し、現在の日付との日数の差を計算します
  2. 次に、グループを取得したい- * 1.0 / 30追加されます
  3. 30、60、90などの日数は整数になり、新しい期間を開始したいと思いました+ 0.000001。また、天井関数を使用してsmallest integer greater than, or equal to, the specified numeric expression

それでおしまい。そのようなグループがある場合ROW_NUMBER、開始日を見つけてそれを作成しnew、残りをのままにするために単に使用しますfollow ups


2
さて、質問は少し異なり、このアプローチは単純化しすぎです。しかし、タンブリングウィンドウ
Lukasz Szozda

パフォーマンスについてもです。再帰は遅くなると思います。
gotqn

3

皆を尊重し、私見では、

There is not much difference between While LOOP and Recursive CTE in terms of RBAR

Recursive CTEand Window Partition functionall in one を使用する場合、パフォーマンスはそれほど向上しません。

Appidであるint identity(1,1)か、それは常に増加している必要がありますclustered index

他の利点とは別に、それはまたAPPDate、その患者のすべての連続した列がより大きくなければならないことを確実にします。

このようにするとAPPIDinequality>、<などの演算子をAPPDateに置くよりも効率的なクエリで簡単に操作できます。inequalityAPPIDに>、<のような演算子を置くと 、SQLオプティマイザを支援します。

また、次のようなテーブルには2つの日付列があるはずです

APPDateTime datetime2(0) not null,
Appdate date not null

これらは最も重要なテーブルの最も重要な列であるため、キャスト、変換はそれほど多くありません。

したがって Non clustered index、Appdateで作成できます

Create NonClustered index ix_PID_AppDate_App  on APP (patientid,APPDate) include(other column which is not i predicate except APPID)

他のサンプルデータを使用してスクリプトをテストし、どのサンプルデータに対して機能しないかを調べます。動作しない場合でも、スクリプトロジック自体で修正できると確信しています。

CREATE TABLE #Appt1 (ApptID INT, PatientID INT, ApptDate DATE)
INSERT INTO #Appt1
SELECT  1,101,'2020-01-05'  UNION ALL
SELECT  2,505,'2020-01-06'  UNION ALL
SELECT  3,505,'2020-01-10'  UNION ALL
SELECT  4,505,'2020-01-20'  UNION ALL
SELECT  5,101,'2020-01-25'  UNION ALL
SELECT  6,101,'2020-02-12'  UNION ALL
SELECT  7,101,'2020-02-20'  UNION ALL
SELECT  8,101,'2020-03-30'  UNION ALL
SELECT  9,303,'2020-01-28'  UNION ALL
SELECT  10,303,'2020-02-02' 

;With CTE as
(
select a1.* ,a2.ApptDate as NewApptDate
from #Appt1 a1
outer apply(select top 1 a2.ApptID ,a2.ApptDate
from #Appt1 A2 
where a1.PatientID=a2.PatientID and a1.ApptID>a2.ApptID 
and DATEDIFF(day,a2.ApptDate, a1.ApptDate)>30
order by a2.ApptID desc )A2
)
,CTE1 as
(
select a1.*, a2.ApptDate as FollowApptDate
from CTE A1
outer apply(select top 1 a2.ApptID ,a2.ApptDate
from #Appt1 A2 
where a1.PatientID=a2.PatientID and a1.ApptID>a2.ApptID 
and DATEDIFF(day,a2.ApptDate, a1.ApptDate)<=30
order by a2.ApptID desc )A2
)
select  * 
,case when FollowApptDate is null then 'New' 
when NewApptDate is not null and FollowApptDate is not null 
and DATEDIFF(day,NewApptDate, FollowApptDate)<=30 then 'New'
else 'Followup' end
 as Category
from cte1 a1
order by a1.PatientID

drop table #Appt1

3

質問では明確に扱われていませんが、予約日を30日間のグループで単純に分類することはできないことを理解するのは簡単です。ビジネスには意味がありません。また、アプリケーションIDも使用できません。今日のために新しい約束をすることができます2020-09-06。これが私がこの問題に対処する方法です。最初に、最初の予定を取得してから、各予定と最初のアプリの間の日付の差を計算します。0の場合、「新規」に設定します。30以下の場合「フォローアップ」。30より大きい場合は、「未決定」として設定し、「未決定」がなくなるまで次のラウンドチェックを実行します。そのためには、whileループが本当に必要ですが、それは各アポイントメント日付をループするのではなく、少数のデータセットのみをループします。実行計画を確認しました。行が10行しかない場合でも、クエリのコストは再帰CTEを使用する場合よりも大幅に低くなりますが、Lukasz Szozdaの補遺メソッドほど低くはありません。

IF OBJECT_ID('tempdb..#TEMPTABLE') IS NOT NULL DROP TABLE #TEMPTABLE
SELECT ApptID, PatientID, ApptDate
    ,CASE WHEN (DATEDIFF(DAY, MIN(ApptDate) OVER (PARTITION BY PatientID), ApptDate) = 0) THEN 'New' 
    WHEN (DATEDIFF(DAY, MIN(ApptDate) OVER (PARTITION BY PatientID), ApptDate) <= 30) THEN 'Followup'
    ELSE 'Undecided' END AS Category
INTO #TEMPTABLE
FROM #Appt1

WHILE EXISTS(SELECT TOP 1 * FROM #TEMPTABLE WHERE Category = 'Undecided') BEGIN
    ;WITH CTE AS (
        SELECT ApptID, PatientID, ApptDate 
            ,CASE WHEN (DATEDIFF(DAY, MIN(ApptDate) OVER (PARTITION BY PatientID), ApptDate) = 0) THEN 'New' 
            WHEN (DATEDIFF(DAY, MIN(ApptDate) OVER (PARTITION BY PatientID), ApptDate) <= 30) THEN 'Followup'
            ELSE 'Undecided' END AS Category    
        FROM #TEMPTABLE
        WHERE Category = 'Undecided'
    )
    UPDATE #TEMPTABLE
    SET Category = CTE.Category
    FROM #TEMPTABLE t
        LEFT JOIN CTE ON CTE.ApptID = t.ApptID
    WHERE t.Category = 'Undecided'
END

SELECT ApptID, PatientID, ApptDate, Category 
FROM #TEMPTABLE

2

これがお役に立てば幸いです。

WITH CTE AS
(
    SELECT #Appt1.*, RowNum = ROW_NUMBER() OVER (PARTITION BY PatientID ORDER BY ApptDate, ApptID) FROM #Appt1
)

SELECT A.ApptID , A.PatientID , A.ApptDate ,
Expected_Category = CASE WHEN (DATEDIFF(MONTH, B.ApptDate, A.ApptDate) > 0) THEN 'New' 
WHEN (DATEDIFF(DAY, B.ApptDate, A.ApptDate) <= 30) then 'Followup' 
ELSE 'New' END
FROM CTE A
LEFT OUTER JOIN CTE B on A.PatientID = B.PatientID 
AND A.rownum = B.rownum + 1
ORDER BY A.PatientID, A.ApptDate

読み取り可能な形式でコードを編集してくれて@ x00に感謝します。携帯電話を使用して回答を投稿しているため、適切なインデントを付けることができませんでした。
Abhijeet Khandagale

これは本質的に正しい答えだと思います。しかし、それは説明されていないという点で質の悪い答えであり、コードの内側の部分の変更がうまくいく場合、コードには不必要な外側のクエリがあります。これらの問題を解決できる場合は、投票していただきます。
pwilcox

1
@pwilcox、貴重な提案をありがとう、私は答えを編集し、現時点で投稿しました。旅行中、ラップトップを持っていないので、1、2日で説明を投稿します。
Abhijeet Khandagale

1
@AbhijeetKhandagaleこれはビジネス要件を完全には満たしていません。失敗したシナリオを質問に追加しました。患者303の場合、2月2日の予定はフォローアップです。しかし、クエリはそれが「新規」であることを示しています
LCJ

1

Caseステートメントを使用できます。

select 
      *, 
      CASE 
          WHEN DATEDIFF(d,A1.ApptDate,A2.ApptDate)>30 THEN 'New' 
          ELSE 'FollowUp' 
      END 'Category'
from 
      (SELECT PatientId, MIN(ApptId) 'ApptId', MIN(ApptDate) 'ApptDate' FROM #Appt1 GROUP BY PatientID)  A1, 
      #Appt1 A2 
where 
     A1.PatientID=A2.PatientID AND A1.ApptID<A2.ApptID

問題は、このカテゴリは最初の予定に基づいて割り当てられるべきか、それとも以前の予定に基づいて割り当てられるべきかということです。つまり、患者に3つの予約があった場合、3番目の予約を最初の予約と比較する必要がありますか?

あなたが最初に述べた問題は、私が答えた方法です。そうでない場合は、を使用してくださいlag

また、DateDiff週末も例外ではありません。これが平日のみの場合は、独自のスカラー値関数を作成する必要があります。


1
これは2つの連続した予定をリンクしません。これはappt 1をすべての後続の予定にリンクし、それらすべての日数を計算します。APPT 1は今3、4は、APPT 2は3、4との関係を持って、2との関係...持っているとしてあなたは、このあまりにも多くのレコードを返したい
steenbergh

いい視点ね。A1の副選択を行うように回答を更新しました。
ユーザー

1
期待どおりの結果は得られません。2月20日の予定は「フォローアップ」
LCJ

質問が不明確です...ポスターの説明は次のとおりです:「(その患者の)最初の予約から30日以内のすべての予約(患者について)はフォローアップです。30日後、予約は再び「新規」です。30日以内の予約「フォローアップ」になります。」1月5日は確かに2月20日から30日以上離れています、すなわち新しいです。ただし、2月12日から30日はかかりません。提供された表ではなく、彼が書いたものに対する解決策を提供します。ユーザーがテーブルが提供するものに合わせたい場合は、ラグを使用する必要があります。また、明確にする必要があります...
ユーザー

1

ラグ関数の使用


select  apptID, PatientID , Apptdate ,  
    case when date_diff IS NULL THEN 'NEW' 
         when date_diff < 30 and (date_diff_2 IS NULL or date_diff_2 < 30) THEN  'Follow Up'
         ELSE 'NEW'
    END AS STATUS FROM 
(
select 
apptID, PatientID , Apptdate , 
DATEDIFF (day,lag(Apptdate) over (PARTITION BY PatientID order by ApptID asc),Apptdate) date_diff ,
DATEDIFF(day,lag(Apptdate,2) over (PARTITION BY PatientID order by ApptID asc),Apptdate) date_diff_2
  from #Appt1
) SRC

デモ-> https://rextester.com/TNW43808


2
これは現在のサンプルデータで機能しますが、別のサンプルデータを指定すると、誤った結果が生じる可能性があります。関数apptDateorder by列として使用する場合でもlag(実際にはidが何も保証されていないため)、フォローアップの予定を追加することで簡単に壊れる可能性があります。たとえば、このRextesterデモを参照してください。でも
頑張っ

ありがとうございました。IDの代わりに日付を使用する必要がありました。しかし、なぜapptID = 6 25.01.2020-12.02.2020-> 18日->フォローアップが間違っているのか。
Digvijay S

2
それはでNewなくてであるべきだからFollowUpです。その患者の最初の予約から30日以上です...各New予約から30日を数えて、New再度使用する必要があります...
Zohar Peled

はい。ありがとうございました。:(日付の有効期間を確認するために、新規に作成する必要があります。
Digvijay S

1
with cte
as
(
select 
tmp.*, 
IsNull(Lag(ApptDate) Over (partition by PatientID Order by  PatientID,ApptDate),ApptDate) PriorApptDate
 from #Appt1 tmp
)
select 
PatientID, 
ApptDate, 
PriorApptDate, 
DateDiff(d,PriorApptDate,ApptDate) Elapsed,
Case when DateDiff(d,PriorApptDate,ApptDate)>30 
or DateDiff(d,PriorApptDate,ApptDate)=0 then 'New' else 'Followup'   end Category   from cte

私は正しいです。著者は不正確でした、経過を参照してください

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.