Eu adicionei uma solução sem usar funções de janela e um benchmark com um grande conjunto de dados abaixo da resposta de Martin
Este é um thread de acompanhamento para GROUP BY usando colunas que não estão na lista SELECT - quando isso é prático, elegante ou poderoso?
Na minha solução para esse desafio, utilizo uma consulta que agrupa por uma expressão que não faz parte da lista de seleção. Isso é frequentemente usado com funções de janela, quando o elemento de agrupamento lógico envolve dados de outras linhas.
Talvez isso seja um exagero como exemplo, mas achei que você poderia achar o desafio interessante por si só. Vou esperar postando minha solução, talvez alguns de vocês possam criar melhores.
Desafio
Temos uma tabela de sensores que registra periodicamente os valores de leitura. Não há garantia de que os tempos de amostragem estejam em intervalos monótonos.
Você precisa escrever uma consulta que informará as 'exceções', ou seja, as vezes em que os sensores relataram leitura fora do limite, baixa ou alta. Cada período de tempo que o sensor estava relatando acima ou abaixo dos valores limite é considerado uma 'exceção'. Uma vez que a leitura voltou ao normal, a exceção termina.
Tabelas e dados de amostra
O script está em T-SQL e faz parte dos meus materiais de treinamento.
Aqui está um link para o SQLFiddle .
------------------------------------------
-- Sensor Thresholds - 1 - Setup Example --
------------------------------------------
CREATE TABLE [Sensors]
(
[Sensor] NVARCHAR(10) NOT NULL,
[Lower Threshold] DECIMAL(7,2) NOT NULL,
[Upper Threshold] DECIMAL(7,2) NOT NULL,
CONSTRAINT [PK Sensors]
PRIMARY KEY CLUSTERED ([Sensor]),
CONSTRAINT [CK Value Range]
CHECK ([Upper Threshold] > [Lower Threshold])
);
GO
INSERT INTO [Sensors]
(
[Sensor] ,
[Lower Threshold] ,
[Upper Threshold]
)
VALUES (N'Sensor A', -50, 50 ),
(N'Sensor B', 40, 80),
(N'Sensor C', 0, 100);
GO
CREATE TABLE [Measurements]
(
[Sensor] NVARCHAR(10) NOT NULL,
[Measure Time] DATETIME2(0) NOT NULL,
[Measurement] DECIMAL(7,2) NOT NULL,
CONSTRAINT [PK Measurements]
PRIMARY KEY CLUSTERED ([Sensor], [Measure Time]),
CONSTRAINT [FK Measurements Sensors]
FOREIGN KEY ([Sensor])
REFERENCES [Sensors]([Sensor])
);
GO
INSERT INTO [Measurements]
(
[Sensor] ,
[Measure Time] ,
[Measurement]
)
VALUES ( N'Sensor A', N'20160101 08:00', -9),
( N'Sensor A', N'20160101 09:00', 30),
( N'Sensor A', N'20160101 10:30', 59),
( N'Sensor A', N'20160101 23:00', 66),
( N'Sensor A', N'20160102 08:00', 48),
( N'Sensor A', N'20160102 11:30', 08),
( N'Sensor B', N'20160101 08:00', 39), -- Note that this exception range has both over and under....
( N'Sensor B', N'20160101 10:30', 88),
( N'Sensor B', N'20160101 13:00', 75),
( N'Sensor B', N'20160102 08:00', 95),
( N'Sensor B', N'20160102 17:00', 75),
( N'Sensor C', N'20160101 09:00', 01),
( N'Sensor C', N'20160101 10:00', -1),
( N'Sensor C', N'20160101 18:00', -2),
( N'Sensor C', N'20160101 22:00', -2),
( N'Sensor C', N'20160101 23:30', -1);
GO
resultado esperado
Sensor Exception Start Time Exception End Time Exception Duration (minutes) Min Measurement Max Measurement Lower Threshold Upper Threshold Maximal Delta From Thresholds
------ -------------------- ------------------ ---------------------------- --------------- --------------- --------------- --------------- -----------------------------
Sensor A 2016-01-01 10:30:00 2016-01-02 08:00:00 1290 59.00 66.00 -50.00 50.00 16.00
Sensor B 2016-01-01 08:00:00 2016-01-01 13:00:00 300 39.00 88.00 40.00 80.00 8.00
Sensor B 2016-01-02 08:00:00 2016-01-02 17:00:00 540 95.00 95.00 40.00 80.00 15.00
Sensor C 2016-01-01 10:00:00 2016-01-01 23:30:00 810 -2.00 -1.00 0.00 100.00 -2.00
*/
Eu provavelmente usaria algo como o abaixo.
Ele é capaz de usar a ordem do índice e evitar uma classificação até chegar ao final
GROUP BY
(para o qual usa um agregado de fluxo, para mim)Em princípio, esta operação de agrupamento final não é realmente necessária. Deve ser possível ler um fluxo de entrada ordenado por
Sensor, MeasureTime
e produzir os resultados desejados em um modo de streaming, mas acho que você precisaria escrever um procedimento SQLCLR para isso.Uma implementação de função CLR SQL de streaming lendo linhas na
Sensor, [Measure Time]
ordem:Fonte
Script de implantação
Crie bits de montagem (um pouco longos demais para postar em linha)
Nota: devido a uma limitação, este assembly requer
EXTERNAL_ACCESS
permissão, embora apenas leia do mesmo banco de dados. Para fins de teste, é suficiente tornar o banco de dadosTRUSTWORTHY
, embora existam boas razões para não fazer isso em produção - assine o assembly.Consulta
Os parâmetros são necessários para que a função saiba como se conectar aos dados de origem.
Plano de execução
Resultados
Escrevi minha tentativa de solução sem olhar para outras respostas, mas não estou surpreso ao ver que minha consulta é muito semelhante à de Martin. Parece que consigo os resultados certos com uma função de janela a menos, mas duvido que haja muita diferença no desempenho. Segue o código completo:
Aqui está uma captura de tela do plano:
É difícil ver os detalhes, então também enviei para Paste The Plan .
Para ver como parte dele funciona, considere as linhas do Sensor B na
q2
tabela derivada:For that sensor there are two exception periods. The first row of an exception period must be an exception by definition. If the exception period contains a row that isn't an exception then it must be after all of the exception rows. Therefore, I can get the minimum exception time by taking the minimum time value and I can get the maximum exception time by taking the minimum time value for a row that isn't an exception, or if that doesn't exist, taking the maximum time value.
Better late than never...
I promised to provide my solution to this challenge a few months ago, but since both Martin and Joe came up with very similar solutions to my original one, I decided to look for another. :-) For extra challenge, I decided to try and find one without window functions, so that it will be valid for other RDBMS as well that don't yet support window functions.
Time went by, and I honestly just forgot about this challenge, but this morning I had an hour to spare, and I happened to recall this challenge, so here is an alternative solution, without using window functions. The general idea is to find the 'nearest next normal measurements' for each exception row, and use that as a grouping expression for the GROUP BY in the outer query. More details in the code comments.
While Paul's CLR solution is unique and might be very efficient (I didn't test it), I was really looking for SQL solutions. Still, Kudos to Paul, and if you have a case where the benefit of using CLR outweighs the challenges it introduces, you should consider it. I usually advise avoiding CLR in SQL Server like the plague, and only use as a last resort... Also, it is not portable to other platforms.
Both Martin's and Joe's solutions come up with nearly identical execution plans, only minor operation order difference. I also find them similar in terms of clarity, so I'm granting the correct answer to Martin, but only because he published his first.
Both Martin and Joe's solutions seem to be more efficient than mine when looking at the estimated query cost. For the small sample data, the optimizer came up with this plan for my solution:
You can see that there are 5 table access operators vs. only 2 for Joe's and Martin's solutions, but on the other hand, no spooling vs. the 2 spools...
The optimizer estimated that my solution will be about twice as expensive as Joe's and Martin's; 0.056 vs. 0.032 total estimated plan cost.
So, being curious, I decided to test all solutions with a larger set:
This resulted in ~160,000 rows in the table. The estimated plan cost now increased to 30501 for my solution, vs only 3.8 for Joe's and Martin's... Here is the plan for my solution, with the large data set:
Eu decidi executar um benchmark real. Limpei o pool de buffers antes de cada execução e aqui estão os resultados no meu laptop:
Agora esse é um resultado decisivo... Viva as funções da janela!!
Brinquei um pouco para tentar otimizá-lo ainda mais, mas adicionei essa solução principalmente para fins educacionais. Parece que o otimizador está longe de suas estimativas.
Você pode encontrar uma otimização para isso sem usar o Window Functions?, Isso seria interessante de ver.
Obrigado novamente por todas as suas soluções, aprendi com isso. E... desculpe novamente pela resposta (muito) atrasada.
Tenham todos um ótimo final de semana!