Este é um tipo de tarefa trivial em meu mundo natal C #, mas ainda não o faço em SQL e prefiro resolvê-lo com base em conjuntos (sem cursores). Um conjunto de resultados deve vir de uma consulta como esta.
SELECT SomeId, MyDate,
dbo.udfLastHitRecursive(param1, param2, MyDate) as 'Qualifying'
FROM T
Como deve funcionar
Envio esses três parâmetros para um UDF.
A UDF usa internamente parâmetros para buscar linhas relacionadas <= 90 dias anteriores, de uma exibição.
O UDF percorre 'MyDate' e retorna 1 se for incluído em um cálculo total.
Caso contrário, retornará 0. Nomeado aqui como "qualificação".
o que a udf vai fazer
Liste as linhas em ordem de data. Calcule os dias entre as linhas. O padrão da primeira linha no conjunto de resultados é Hit = 1. Se a diferença for de até 90, então passe para a próxima linha até que a soma das lacunas seja de 90 dias (o 90º dia deve passar) Quando atingido, defina Hit como 1 e redefina a lacuna como 0 Também funcionaria omitir a linha do resultado.
|(column by udf, which not work yet)
Date Calc_date MaxDiff | Qualifying
2014-01-01 11:00 2014-01-01 0 | 1
2014-01-03 10:00 2014-01-01 2 | 0
2014-01-04 09:30 2014-01-03 1 | 0
2014-04-01 10:00 2014-01-04 87 | 0
2014-05-01 11:00 2014-04-01 30 | 1
Na tabela acima, a coluna MaxDiff é a diferença da data na linha anterior. O problema com minhas tentativas até agora é que não posso ignorar a penúltima linha no exemplo acima.
[EDIT]
De acordo com o comentário, adiciono uma tag e também colo o udf que compilei agora. No entanto, é apenas um espaço reservado e não fornecerá resultados úteis.
;WITH cte (someid, otherkey, mydate, cost) AS
(
SELECT someid, otherkey, mydate, cost
FROM dbo.vGetVisits
WHERE someid = @someid AND VisitCode = 3 AND otherkey = @otherkey
AND CONVERT(Date,mydate) = @VisitDate
UNION ALL
SELECT top 1 e.someid, e.otherkey, e.mydate, e.cost
FROM dbo.vGetVisits AS E
WHERE CONVERT(date, e.mydate)
BETWEEN DateAdd(dd,-90,CONVERT(Date,@VisitDate)) AND CONVERT(Date,@VisitDate)
AND e.someid = @someid AND e.VisitCode = 3 AND e.otherkey = @otherkey
AND CONVERT(Date,e.mydate) = @VisitDate
order by e.mydate
)
Tenho outra consulta que defino separadamente que está mais próxima do que preciso, mas bloqueada pelo fato de não poder calcular em colunas com janelas. Eu também tentei um similar que dá mais ou menos a mesma saída apenas com um LAG () sobre MyDate, cercado por um datediff.
SELECT
t.Mydate, t.VisitCode, t.Cost, t.SomeId, t.otherkey, t.MaxDiff, t.DateDiff
FROM
(
SELECT *,
MaxDiff = LAST_VALUE(Diff.Diff) OVER (
ORDER BY Diff.Mydate ASC
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)
FROM
(
SELECT *,
Diff = ISNULL(DATEDIFF(DAY, LAST_VALUE(r.Mydate) OVER (
ORDER BY r.Mydate ASC
ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING),
r.Mydate),0),
DateDiff = ISNULL(LAST_VALUE(r.Mydate) OVER (
ORDER BY r.Mydate ASC
ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING),
r.Mydate)
FROM dbo.vGetVisits AS r
WHERE r.VisitCode = 3 AND r.SomeId = @SomeID AND r.otherkey = @otherkey
) AS Diff
) AS t
WHERE t.VisitCode = 3 AND t.SomeId = @SomeId AND t.otherkey = @otherkey
AND t.Diff <= 90
ORDER BY
t.Mydate ASC;
Enquanto leio a pergunta, o algoritmo recursivo básico necessário é:
Isso é relativamente fácil de implementar com uma expressão de tabela comum recursiva.
For example, using the following sample data (based on the question):
The recursive code is:
The results are:
With an index having
TheDate
as a leading key, the execution plan is very efficient:You could choose to wrap this in a function and execute it directly against the view mentioned in the question, but my instincts are against it. Usually, performance is better when you select rows from a view into a temporary table, provide the appropriate index on the temporary table, then apply the logic above. The details depend on the details of the view, but this is my general experience.
For completeness (and prompted by ypercube's answer) I should mention that my other go-to solution for this type of problem (until T-SQL gets proper ordered set functions) is a SQLCLR cursor (see my answer here for an example of the technique). This performs much better than a T-SQL cursor, and is convenient for those with skills in .NET languages and the ability to run SQLCLR in their production environment. It may not offer much in this scenario over the recursive solution because the majority of the cost is the sort, but it is worth mentioning.
Since this is a SQL Server 2014 question I might as well add a natively compiled stored procedure version of a "cursor".
Source table with some data:
A table type that is the parameter to the stored procedure. Adjust the
bucket_count
appropriately.And a stored procedure that loops through the table valued parameter and collects the rows in
@R
.Code to fill a memory optimized table variable that is used as a parameter to the natively compiled stored procedure and call the procedure.
Result:
Update:
If you for some reason don't need to visit every row in the table you can do the equivalent of the "jump to next date" version that is implemented in the recursive CTE by Paul White.
O tipo de dados não precisa da coluna ID e você não deve usar um índice de hash.
E o procedimento armazenado usa a
select top(1) ..
para localizar o próximo valor.A solution that uses a cursor.
(first, some needed tables and variables):
The actual cursor:
And getting the results:
Tested at SQLFiddle
Resultado
Também dê uma olhada em Como calcular o total em execução no SQL Server
atualização: veja abaixo os resultados dos testes de desempenho.
Por causa da lógica diferente usada para encontrar "intervalo de 90 dias", as soluções do ypercube e minhas, se deixadas intactas, podem retornar resultados diferentes para a solução de Paul White. Isso se deve ao uso das funções DATEDIFF e DATEADD , respectivamente.
Por exemplo:
retorna '2014-04-01 00:00:00.000', o que significa que '2014-04-01 01:00:00.000' está além do intervalo de 90 dias
mas
Retorna '90', o que significa que ainda está dentro do intervalo.
Considere um exemplo de um varejista. Neste caso, vender um produto perecível com data de validade '2014-01-01' em '2014-01-01 23:59:59:999' é aceitável. Portanto, o valor DATEDIFF(DAY, ...) neste caso está OK.
Outro exemplo é um paciente esperando para ser atendido. Para alguém que chega em '2014-01-01 00:00:00:000' e sai em '2014-01-01 23:59:59:999', é 0 (zero) dias se DATEDIFF for usado, mesmo que o a espera real foi de quase 24 horas. Novamente, o paciente que chega em '2014-01-01 23:59:59' e sai em '2014-01-02 00:00:01' esperou um dia se DATEDIFF for usado.
Mas eu discordo.
Deixei as soluções DATEDIFF e até testei o desempenho delas, mas elas realmente deveriam estar em sua própria liga.
Também foi notado que para os grandes conjuntos de dados é impossível evitar valores no mesmo dia. Portanto, se tivermos 13 milhões de registros abrangendo 2 anos de dados, acabaremos tendo mais de um registro por alguns dias. Esses registros estão sendo filtrados na primeira oportunidade nas soluções DATEDIFF minhas e do ypercube. Espero que o ypercube não se importe com isso.
As soluções foram testadas na tabela a seguir
com dois índices clusterizados diferentes (mydate neste caso):
A tabela foi preenchida da seguinte maneira
Para um caso de vários milhões de linhas, INSERT foi alterado de forma que entradas de 0 a 20 minutos fossem adicionadas aleatoriamente.
Todas as soluções foram cuidadosamente agrupadas no seguinte código
Códigos reais testados (em nenhuma ordem específica):
Solução DATEDIFF da Ypercube ( YPC, DATEDIFF )
Solução DATEADD da Ypercube ( YPC, DATEADD )
A solução de Paul White ( PW )
Minha solução DATEADD ( PN, DATEADD )
Minha solução DATEDIFF ( PN, DATEDIFF )
Estou usando o SQL Server 2012, então peço desculpas ao Mikael Eriksson, mas o código dele não será testado aqui. Eu ainda esperaria que suas soluções com DATADIFF e DATEADD retornassem valores diferentes em alguns conjuntos de dados.
E os resultados reais são:
Ok, eu perdi alguma coisa ou por que você simplesmente não pularia a recursão e voltaria para si mesmo? Se a data for a chave primária, ela deve ser única e em ordem cronológica se você planeja calcular o deslocamento para a próxima linha
Rendimentos
A menos que eu tenha perdido totalmente algo importante....