Запрос без цикла 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' 

Я не вижу вашего изображения, но я хочу подтвердить, что если есть 3 встречи, каждые 20 дней друг от друга, последняя по-прежнему остается «правильной», потому что, хотя прошло более 30 дней с первой, до середины еще 20 дней. Это правда?
pwilcox

@pwilcox Нет. Третьим будет новое назначение, как показано на рисунке
LCJ

Хотя цикл по fast_forwardкурсору, вероятно, будет вашим лучшим вариантом, с точки зрения производительности.
Дэвид דודו Марковиц

Ответы:


14

Вам нужно использовать рекурсивный запрос.

Период 30 дней отсчитывается, начиная с prev (и нет, это невозможно сделать без рекурсии / необычного обновления / цикла). Именно поэтому все существующие ответы с использованием только 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;  

ДБ <> Fiddle Demo

Вывод:

+---------+------------+-------------+----------+
| 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 - получить начальную точку (привязка - для каждого PatientId)
  2. rec - рекурсивная часть, если разница между текущим значением и предыдущим> 30 изменяет категорию и начальную точку в контексте PatientId
  3. Main - отображать отсортированные результаты

Подобный класс:

Условная сумма в Oracle - ограничение оконной функции

Окно сеанса (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 Quirky


1
ваша логика выглядит очень похоже на мою. Можете ли вы описать какие-либо существенные различия?
pwilcox

@pwilcox Когда я писал этот ответ, каждый существующий использовал простой row_number, который не работал, поэтому я предоставил свою собственную версию
Лукаш Шозда,

Да, я был слишком быстр с ответом. Спасибо за комментарий к этому.
Ирдис

2
Я считаю, что rcte является единственным решением для этого, пока SQL-сервер правильно не реализует RANGE x PRECEDINGпредложение.
Салман А

1
Причудливое обновление @LCJ основано на «недокументированном» поведении и может измениться в любой момент без уведомления ( red-gate.com/simple-talk/sql/learn-sql-server/… )
Лукаш Шозда,

5

Вы можете сделать это с помощью рекурсивного cte. Вы должны сначала заказать apptDate для каждого пациента. Это может быть достигнуто заурядным циклом.

Затем, в анкерной части вашего рекурсивного 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, это использовать временную таблицу и обновлять ее в «раундах». Таким образом, мы собираемся обновить временную таблицу, пока все состояния не установлены правильно, и построить результат итеративным способом. Мы можем контролировать количество итераций, используя просто локальную переменную.

Таким образом, мы разделили каждую итерацию на два этапа.

  1. Установите все значения отслеживания, близкие к новым записям. Это довольно легко сделать, используя правильный фильтр.
  2. Для остальных записей, которые не имеют установленного статуса, мы можем выбрать сначала в группе с тем же PatientID. И говорят, что они новые, так как они не обработаны на первом этапе.

Так

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

Обновить. Прочитайте комментарий, предоставленный Лукаш. Это намного умнее. Я оставляю свой ответ просто как идею.


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
Ну, вопрос немного другой, и это упрощение. Но это хороший пример того, как реализовать акробатическое окно
Лукаш Шозда

Это касается и производительности тоже. Я считаю, что рекурсивный должен быть медленнее.
получил

3

С уважением ко всем и в ИМХО

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

При использовании не так много прироста Recursive CTEи Window Partition functionвсе в одном.

Appidдолжно быть int identity(1,1), или это должно быть постоянно увеличивается clustered index.

Помимо других преимуществ это также гарантирует, что все последующие ряды APPDateэтого пациента должны быть больше.

Таким образом, вы можете легко поиграть APPIDв своем запросе, что будет более эффективно, чем помещать inequalityоператоры типа>, <в APPDate. Помещение inequalityоператора как>, <в APPID поможет Sql Optimizer.

Также в таблице должно быть два столбца даты

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-дневным группам. Это не имеет никакого делового смысла. И вы также не можете использовать идентификатор приложения. Сегодня можно назначить новую встречу для2020-09-06, Вот как я решаю эту проблему. Сначала получите первую встречу, затем вычислите разницу в дате между каждой встречей и первой квартирой. Если это 0, установите «Новый». Если <= 30, «Продолжение». Если> 30, установите значение «Не определено» и выполняйте проверку следующего раунда, пока не останется «Не определено». И для этого вам действительно нужен цикл while, но он не перебирает каждую дату встречи, а только несколько наборов данных. Я проверил план выполнения. Несмотря на то, что строк всего 10, стоимость запроса значительно ниже, чем при использовании рекурсивного CTE, но не так низка, как метод добавления Лукаша Шозды.

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 за редактирование кода в удобочитаемом формате, я использую свой мобильный телефон для отправки ответов, поэтому не смог дать правильные отступы.
Абхиджит Хандагале

Я думаю, что это по сути правильный ответ. Но это некачественный ответ в том смысле, что он не объяснен, и у кода есть ненужный внешний запрос, когда модификация внутренней части будет работать нормально. Если вы сможете решить эти проблемы, я буду рад проголосовать за вас.
Pwilcox

1
@pwilcox, спасибо за ценное предложение, я отредактировал ответ и опубликовал его на данный момент. Поскольку я путешествую и у меня нет ноутбука, я буду публиковать объяснения через день или два.
Абхиджит Хандагале

1
@AbhijeetKhandagale Это не полностью соответствует бизнес-требованиям. Я добавил неудачный сценарий в вопрос. Для пациента 303, 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

Вопрос в том, должна ли эта категория назначаться на основе первоначальной или предыдущей? То есть, если у пациента было три приема, следует ли сравнивать третье посещение с первым или вторым?

У вас первая проблема, о которой я и ответил. Если это не так, вы захотите использовать lag.

Кроме того, имейте в виду, что DateDiff не исключение для выходных. Если это будут только будние дни, вам нужно будет создать свою собственную функцию Scalar-Valued.


1
Это не связывает две последовательные встречи, это связывает appt 1 со всеми последующими встречами и вычисляет промежуточные дни для всех из них. Вы бы вернули слишком много записей таким образом, так как appt 1 теперь имеет отношение с 2, 3, 4, appt 2 имеет отношение с 3, 4 ...
steenbergh

Хорошая точка зрения. Я обновил свой ответ, чтобы сделать отбор для А1.
пользователь

1
Это не дает ожидаемого результата. Назначение на 20 февраля должно быть "Followup"
LCJ

Вопрос неясен ... Описание постера таково: «Любое посещение (для пациента) в течение 30 дней после первого посещения (для этого пациента) является повторным наблюдением. Через 30 дней назначение снова« новое ». Любое посещение в течение 30 дней». стать "Продолжение". " 5 января, безусловно, более чем на 30 дней от 20 февраля, то есть Нового. Это не 30 дней от 12 февраля, однако. Я предлагаю решение того, что он написал, а не таблицу, поставляемую. Если пользователь хочет согласовать с тем, что поставляет таблица, он должен использовать лаг. Они также должны уточнить ...
пользователь

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
Это работает с текущими данными выборки, но может привести к неверным результатам, учитывая другие данные выборки. Даже если вы используете apptDateв качестве order byстолбца lagфункции (что на самом деле должно быть, поскольку идентификатор ничего не гарантирует), его все равно можно легко сломать, введя дополнительные последующие встречи. Посмотрите эту демонстрацию Rextester, например. Хорошая попытка, хотя ...
Зохар Пелед

Спасибо. Должен был использовать дату вместо ID. Но почему это неправильно для apptID = 6 25.01.2020 - 12.02.2020 -> 18 дней -> последующее наблюдение.
Digvijay S

2
Потому что это должен быть Newа не а FollowUp. Прошло более 30 дней с момента первого приема этого пациента ... Вы должны сосчитать 30 дней после каждого Newпосещения, а затем Newснова использовать ...
Зохар Пелед

Да. Спасибо. :( Необходимо создать новый, чтобы проверить действительный период даты.
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

Мой правильный. Авторы были неверны, см. Истекшие

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.