Consulta sem WHILE Loop

18

Temos tabela de compromissos, como mostrado abaixo. Cada compromisso precisa ser categorizado como "Novo" ou "Acompanhamento". Qualquer consulta (para um paciente) dentro de 30 dias da primeira consulta (desse paciente) é Acompanhamento. Após 30 dias, a nomeação é novamente "Nova". Qualquer compromisso dentro de 30 dias se torna "Acompanhamento".

Atualmente, estou fazendo isso digitando while loop.
Como conseguir isso sem o loop WHILE?

insira a descrição da imagem aqui

Mesa

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' 
LCJ
fonte
Não consigo ver sua imagem, mas quero confirmar que, se houver três compromissos, a cada 20 dias um do outro, o último ainda será 'acompanhamento' corretamente, porque, mesmo sendo mais de 30 dias desde o primeiro, ainda está a menos de 20 dias do meio. Isso é verdade?
pwilcox 28/02
@pwilcox Não. O terceiro será o novo compromisso, como mostra a imagem
LCJ
Embora o loop sobre o fast_forwardcursor provavelmente seja sua melhor opção, em termos de desempenho.
David Markovitz 4/03

Respostas:

14

Você precisa usar a consulta recursiva.

O período de 30 dias é contado a partir de anterior (e não, não é possível fazê-lo sem recursão / atualização / loop peculiar). É por isso que toda a resposta existente usando apenas ROW_NUMBERfalhou.

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 <> demo de violino

Resultado:

+---------+------------+-------------+----------+
| 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 |
+---------+------------+-------------+----------+

Como funciona:

  1. f - obter ponto de partida (âncora - por cada PatientId)
  2. rec - recursibe parte, se a diferença entre o valor atual e o anterior for> 30, altere a categoria e o ponto inicial, no contexto de PatientId
  3. Principal - exibe o conjunto de resultados classificados

Classe semelhante:

Soma Condicional no Oracle - Limpando uma Função de Janela

Janela de sessão (Azure Stream Analytics)

Total em execução até que a condição específica seja verdadeira - Atualização peculiar


Termo aditivo

Nunca use esse código na produção!

Mas outra opção, que vale a pena mencionar, além de usar cte, é usar a tabela temporária e atualizar em "rodadas"

Isso poderia ser feito na rodada "única" (atualização peculiar):

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

Inquerir:

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;

atualização quirky do violino do db <>

Lukasz Szozda
fonte
11
sua lógica é muito parecida com a minha. Você pode descrever diferenças significativas?
pwilcox 02/03
@pwilcox Quando escrevi esta resposta, todas as existentes estavam usando um simples número de linha que não estava funcionando, por isso forneci minha própria versão
Lukasz Szozda
Sim, eu fui rápido demais com a resposta. Agradecemos por comentar sobre isso.
Irdis 2/03
2
Acredito que o rcte é a única solução para isso até que o SQL Server implemente corretamente a RANGE x PRECEDINGcláusula.
Salman A
11
A atualização do @LCJ Quirky é baseada no comportamento "não documentado" e pode mudar a qualquer momento sem aviso prévio ( red-gate.com/simple-talk/sql/learn-sql-server/… )
Lukasz Szozda
5

Você poderia fazer isso com um cte recursivo. Você deve primeiro solicitar por apptDate em cada paciente. Isso pode ser conseguido por meio de um CTE comum.

Em seguida, na parte âncora do seu recursivo, selecione a primeira ordem para cada paciente, marque o status como 'novo' e marque apptDate como a data do 'novo' registro mais recente.

Na parte recursiva do cte recursivo, aumente para o próximo compromisso, calcule a diferença de dias entre o presente e a mais recente data do novo compromisso. Se for superior a 30 dias, marque-o como 'novo' e redefina a data do novo compromisso mais recente. Caso contrário, marque-o como "acompanhamento" e passe apenas os dias existentes desde a nova data do compromisso.

Por fim, na consulta base, basta selecionar as colunas que deseja.

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;

Devo mencionar que, inicialmente, excluí esta resposta porque a resposta de Abhijeet Khandagale parecia atender às suas necessidades com uma consulta mais simples (depois de reformulá-la um pouco). Mas, com seu comentário sobre seus requisitos de negócios e seus dados de amostra adicionados, cancelei a exclusão dos meus porque acredito que este atenda às suas necessidades.

pwilcox
fonte
4

Não tenho certeza de que é exatamente o que você implementou. Mas outra opção, que vale a pena mencionar, além de usar o cte, é usar a tabela temporária e atualizar em "rodadas". Então, vamos atualizar a tabela temporária enquanto todos os status não estiverem definidos corretamente e criar o resultado de maneira iterativa. Podemos controlar o número de iterações usando simplesmente variáveis ​​locais.

Então, dividimos cada iteração em dois estágios.

  1. Defina todos os valores de acompanhamento próximos a novos registros. É muito fácil fazer isso usando o filtro certo.
  2. Nos demais registros que não possuem status definido, podemos selecionar primeiro o grupo com o mesmo PatientID. E diga que eles são novos, pois não foram processados ​​no primeiro estágio.

assim

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

Atualizar. Leia o comentário fornecido por Lukasz. É de maneira muito mais inteligente. Deixo minha resposta apenas como uma ideia.

Irdis
fonte
4

Acredito que a expressão comum recursiva é uma ótima maneira de otimizar consultas, evitando loops, mas em alguns casos pode levar a um desempenho ruim e deve ser evitada, se possível.

Uso o código abaixo para resolver o problema e testá-lo com mais valores, mas encorajo você a testá-lo com seus dados reais também.

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

insira a descrição da imagem aqui

A ideia é bem simples - quero separar os registros em grupo (30 dias), em qual grupo o menor é o registro newe os outros follow ups. Verifique como a declaração é criada:

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

insira a descrição da imagem aqui

Assim:

  1. primeiro, estamos obtendo a primeira data, para cada grupo e calculando as diferenças em dias com a atual
  2. então, queremos obter grupos - * 1.0 / 30é adicionado
  3. quanto a 30, 60, 90, etc dias, estamos obtendo um número inteiro e queríamos iniciar um novo período, acrescentei + 0.000001; Além disso, estamos usando a função de teto para obter osmallest integer greater than, or equal to, the specified numeric expression

É isso aí. Tendo esse grupo, simplesmente usamos ROW_NUMBERpara encontrar nossa data de início e torná-la como newe deixando o resto como follow ups.

gotqn
fonte
2
Bem, a questão é um pouco diferente e essa abordagem é simplificada demais. Mas é um bom exemplo de como implementar janela de queda
Lukasz Szozda
É sobre desempenho também. Eu acredito que recursivo deve ser mais lento.
gotqn 6/03
3

Com o devido respeito a todos e no IMHO,

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

Não há muito ganho de desempenho ao usar Recursive CTEe Window Partition functiontudo em um.

Appiddeveria ser int identity(1,1), ou deveria estar sempre aumentando clustered index.

Além de outros benefícios, também garante que todas as filas sucessivas APPDatedesse paciente sejam maiores.

Dessa forma, você pode facilmente brincar com APPIDsua consulta, o que será mais eficiente do que colocar o inequalityoperador como>, <em APPDate. Colocar o inequalityoperador como>, <no APPID ajudará o Sql Optimizer.

Também deve haver duas colunas de data na tabela como

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

Como essas são as colunas mais importantes da tabela mais importante, então não muito elenco, converta.

Então Non clustered indexpode ser criado no Appdate

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

Teste meu script com outros dados de amostra e deixe-me saber para quais dados de amostra ele não está funcionando. Mesmo que não funcione, tenho certeza de que pode ser corrigido na minha própria lógica de script.

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
KumarHarsh
fonte
3

Embora não esteja claramente abordado na pergunta, é fácil descobrir que as datas dos compromissos não podem ser simplesmente categorizadas por grupos de 30 dias. Não faz sentido nos negócios. E você também não pode usar o ID do agendamento. Pode-se marcar uma nova consulta hoje para2020-09-06. Aqui está como eu resolvo esse problema. Primeiro, obtenha o primeiro compromisso e calcule a diferença de data entre cada compromisso e o primeiro agendamento. Se for 0, defina como 'Novo'. Se <= 30 'Acompanhamento'. Se> 30, defina como 'Indeciso' e faça a próxima rodada até que não haja mais 'Indeciso'. E para isso, você realmente precisa de um loop while, mas ele não percorre a data de cada compromisso, apenas alguns conjuntos de dados. Eu verifiquei o plano de execução. Embora existam apenas 10 linhas, o custo da consulta é significativamente menor que o do CTE recursivo, mas não tão baixo quanto o método de adendo de 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
Weihui Guo
fonte
2

Eu espero que isso te ajude.

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
Abhijeet Khandagale
fonte
Obrigado @ x00 por editar o código em formato legível, estou usando meu celular para postar respostas, de modo que não foi possível fornecer recuos adequados.
Abhijeet Khandagale
Eu acho que essa é, em essência, a resposta certa. Mas é uma resposta de baixa qualidade, uma vez que não é explicada e o código possui uma consulta externa desnecessária quando uma modificação da parte interna funcionará bem. Se você puder resolver esses problemas, ficarei feliz em votar em você.
pwilcox 02/03
11
@pwilcox, obrigado pela sugestão valiosa, editei a resposta e a publiquei a partir de agora. Enquanto viajo e não tenho laptop comigo, postarei explicações em um dia ou dois.
Abhijeet Khandagale
11
@AbhijeetKhandagale Isso não atende completamente aos requisitos de negócios. Eu adicionei um cenário com falha na pergunta. Para o paciente 303, a consulta de 2 de fevereiro deve ser Acompanhamento; mas sua consulta diz que é "Novo"
LCJ
1

Você poderia usar uma Casedeclaração .

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

A questão é: essa categoria deve ser atribuída com base no compromisso inicial ou no anterior? Ou seja, se um paciente teve três consultas, devemos comparar a terceira consulta com a primeira ou a segunda?

Seu problema declara o primeiro, e foi assim que eu respondi. Se não for esse o caso, convém usar lag.

Além disso, lembre-se de que isso DateDiffnão é exceção nos finais de semana. Se for apenas durante a semana, você precisará criar sua própria função com valor escalar.

do utilizador
fonte
11
Isso não vincula dois compromissos seqüenciais, vincula o appt 1 a todos os compromissos seguintes e calcula os dias intermediários para todos eles. Você retornaria muitos registros dessa maneira, pois o apêndice 1 agora tem uma relação com 2, 3, 4, o apêndice 2 tem uma relação com 3, 4 ...
steenbergh
Bom ponto. Atualizei minha resposta para fazer uma subseleção para A1.
usuário
11
Não dá o resultado esperado. A consulta no dia 20 de fevereiro deve ser "Acompanhamento"
LCJ
A pergunta não está clara ... A descrição do pôster é esta: "Qualquer consulta (para um paciente) dentro de 30 dias da primeira consulta (desse paciente) é Acompanhamento. Após 30 dias, a consulta é novamente" Nova ". Qualquer consulta dentro de 30 dias torne-se "Acompanhamento". " O dia 5 de janeiro certamente está a mais de 30 dias do dia 20 de fevereiro, ou seja, Novo. No entanto, NÃO são 30 dias a partir de 12 de fevereiro. Eu ofereço uma solução para o que ele escreveu, não para a tabela fornecida. Se o usuário quiser se alinhar com o que a tabela fornece, ele deve usar lag. Eles também devem esclarecer ...
user
1

usando a função Lag


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

Demonstração -> https://rextester.com/TNW43808

Digvijay S
fonte
2
Isso funciona nos dados de amostra atuais, mas pode gerar resultados incorretos, dados diferentes. Mesmo se você usar apptDatecomo a order bycoluna da lagfunção (que você realmente deve como id não é garantia de nada), ela ainda poderá ser quebrada facilmente, introduzindo mais compromissos de acompanhamento. Veja esta demonstração do Rextester, por exemplo. Boa tentativa, embora ...
Zohar Peled 03/03
Obrigado. Deveria ter usado data em vez de ID. Mas por que está errado o apptID = 6 25.01.2020 - 12.02.2020 -> 18 dias -> acompanhamento.
Digvijay S 03/03
2
Porque deveria ser um Newe não um FollowUp. Faz mais de 30 dias desde a primeira consulta desse paciente ... Você deve contar 30 dias desde cada Newconsulta e depois usar Newnovamente ...
Zohar Peled 03/03
Sim. Obrigado. :( Precisa criar um novo para verificar o período válido de data.
Digvijay S 03/03
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

O meu está correto. Os autores estavam incorretos.

Leão dourado
fonte