Запрос без цикла WHILE - PullRequest
       16

Запрос без цикла WHILE

18 голосов
/ 28 февраля 2020

У нас есть таблица встреч, как показано ниже. Каждое назначение должно быть отнесено к категории «Новое» или «Последующее наблюдение». Любое посещение (для пациента) в течение 30 дней после первого посещения (для этого пациента) является последующим наблюдением. Через 30 дней назначение снова «Новое». Любая встреча в течение 30 дней становится «Последованием».

В настоящее время я делаю это, набирая в то время как l oop.
Как этого добиться без WHILE l oop?

enter image description here

Таблица

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' 

Ответы [ 10 ]

14 голосов
/ 02 марта 2020

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

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

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

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

Окно сеанса (Azure Stream Analytics)

Промежуточный итог до конкретное c условие истинно - необычное обновление


Приложение

Никогда не используйте этот код на производстве!

Но другой вариант, о котором стоит упомянуть помимо использования cte, - использовать временную таблицу и обновлять ее в «раундах»

Это можно сделать в «одиночном» раунде. d (необычное обновление):

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

5 голосов
/ 02 марта 2020

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

1002 * Тогда, в анкерной части вашего рекурсивного 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;

I следует упомянуть, что я сначала удалил этот ответ, потому что ответ Абхиджита Кхандагале, казалось, отвечал вашим потребностям с помощью более простого запроса (после его доработки). Но с вашим комментарием к нему о ваших бизнес-требованиях и добавленными образцами данных я восстановил мои, потому что считаю, что это отвечает вашим потребностям.

4 голосов
/ 03 марта 2020

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

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

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];

enter image description here

Идея довольно проста - я хочу разделить записи в группе (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];

enter image description here

Итак:

  1. сначала мы получаем первое число для каждой группы и расчета различий в днях с текущей
  2. затем мы хотим получить группы - * 1.0 / 30 добавляется
  3. как для 30, 60, 90 и т. д. c дней мы получаем целое число, и мы хотели начать новый период, я добавил + 0.000001; Кроме того, мы используем функцию потолка, чтобы получить smallest integer greater than, or equal to, the specified numeric expression

Вот и все. Имея такую ​​группу, мы просто используем ROW_NUMBER, чтобы найти дату начала и сделать ее new, а оставшуюся - follow ups.

.
4 голосов
/ 02 марта 2020

Я не уверен, что это именно то, что вы реализовали. Но другой вариант, о котором стоит упомянуть помимо использования 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

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

3 голосов
/ 05 марта 2020

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

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. Помещение <code>inequality оператора как>, <в APPID поможет Sql Оптимизатору. </p>

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

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)

Протестируйте мой скрипт с другими примерами данных и узнайте, для каких образцов данных это не работает. Даже если это не сработает, я уверен, что это можно исправить в самом скрипте logi c.

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 голосов
/ 04 марта 2020

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

Надеюсь, это поможет вам.

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
1 голос
/ 06 марта 2020
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

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

1 голос
/ 03 марта 2020

с использованием функции задержки


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

1 голос
/ 28 февраля 2020

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

...