Agregados apenas os registos adjacentes com T-SQL
-
04-07-2019 - |
Pergunta
Eu tenho (simplificado para o exemplo) uma tabela com os seguintes dados
Row Start Finish ID Amount
--- --------- ---------- -- ------
1 2008-10-01 2008-10-02 01 10
2 2008-10-02 2008-10-03 02 20
3 2008-10-03 2008-10-04 01 38
4 2008-10-04 2008-10-05 01 23
5 2008-10-05 2008-10-06 03 14
6 2008-10-06 2008-10-07 02 3
7 2008-10-07 2008-10-08 02 8
8 2008-10-08 2008-11-08 03 19
As datas representam um período de tempo, o ID é o estado de um sistema estava durante esse período e o montante é um valor relacionado a esse estado.
O que eu quero fazer é agregar os montantes para adjacentes linhas com o mesma número de identificação, mas manter a mesma seqüência geral de modo que contíguas execuções podem ser combinados. Assim, eu quero acabar com dados como:
Row Start Finish ID Amount
--- --------- ---------- -- ------
1 2008-10-01 2008-10-02 01 10
2 2008-10-02 2008-10-03 02 20
3 2008-10-03 2008-10-05 01 61
4 2008-10-05 2008-10-06 03 14
5 2008-10-06 2008-10-08 02 11
6 2008-10-08 2008-11-08 03 19
Eu sou depois uma solução de T-SQL que pode ser colocado em um SP, no entanto eu não posso ver como fazer isso com consultas simples. Eu suspeito que ele pode exigir iteração de algum tipo, mas eu não quero ir por esse caminho.
A razão que eu quero fazer essa agregação é que o próximo passo no processo é fazer um SUM () e Contagem () agrupados pelos de identificação única que ocorrem dentro da seqüência, de modo que os meus dados finais será algo parecido :
ID Counts Total
-- ------ -----
01 2 71
02 2 31
03 2 33
No entanto, se eu faço uma simples
SELECT COUNT(ID), SUM(Amount) FROM data GROUP BY ID
Na tabela original eu obter algo como
ID Counts Total
-- ------ -----
01 3 71
02 3 31
03 2 33
O que não é o que eu quero.
Nenhuma solução correta
Outras dicas
Se você ler o livro "Desenvolvendo Time-Oriented Applications banco de dados SQL" por RT Snodgrass (o pdf do que está disponível no seu web site sob publicações), e chegar tão longe como Figura 6.25 em p165-166, você vai encontrar o SQL não-trivial que pode ser usado no exemplo atual para agrupar os diversos linhas com o mesmo valor de ID e os intervalos de tempo contínuo.
O desenvolvimento consulta abaixo está perto de correto, mas há um problema visto à direita no final, que tem a sua fonte na primeira instrução SELECT. Eu ainda não rastreou por isso que a resposta incorreta está sendo dada. [Se alguém pode testar o SQL em seus DBMS e me diga se a primeira consulta funciona corretamente lá, seria uma grande ajuda!]
Parece algo como:
-- Derived from Figure 6.25 from Snodgrass "Developing Time-Oriented
-- Database Applications in SQL"
CREATE TABLE Data
(
Start DATE,
Finish DATE,
ID CHAR(2),
Amount INT
);
INSERT INTO Data VALUES('2008-10-01', '2008-10-02', '01', 10);
INSERT INTO Data VALUES('2008-10-02', '2008-10-03', '02', 20);
INSERT INTO Data VALUES('2008-10-03', '2008-10-04', '01', 38);
INSERT INTO Data VALUES('2008-10-04', '2008-10-05', '01', 23);
INSERT INTO Data VALUES('2008-10-05', '2008-10-06', '03', 14);
INSERT INTO Data VALUES('2008-10-06', '2008-10-07', '02', 3);
INSERT INTO Data VALUES('2008-10-07', '2008-10-08', '02', 8);
INSERT INTO Data VALUES('2008-10-08', '2008-11-08', '03', 19);
SELECT DISTINCT F.ID, F.Start, L.Finish
FROM Data AS F, Data AS L
WHERE F.Start < L.Finish
AND F.ID = L.ID
-- There are no gaps between F.Finish and L.Start
AND NOT EXISTS (SELECT *
FROM Data AS M
WHERE M.ID = F.ID
AND F.Finish < M.Start
AND M.Start < L.Start
AND NOT EXISTS (SELECT *
FROM Data AS T1
WHERE T1.ID = F.ID
AND T1.Start < M.Start
AND M.Start <= T1.Finish))
-- Cannot be extended further
AND NOT EXISTS (SELECT *
FROM Data AS T2
WHERE T2.ID = F.ID
AND ((T2.Start < F.Start AND F.Start <= T2.Finish)
OR (T2.Start <= L.Finish AND L.Finish < T2.Finish)));
A saída dessa consulta é:
01 2008-10-01 2008-10-02
01 2008-10-03 2008-10-05
02 2008-10-02 2008-10-03
02 2008-10-06 2008-10-08
03 2008-10-05 2008-10-06
03 2008-10-05 2008-11-08
03 2008-10-08 2008-11-08
Editado : Há um problema com a penúltima linha - não deveria estar lá. E eu não sou claro (ainda) onde ele está vindo.
Agora, precisamos tratar essa expressão complexa como uma expressão de consulta na cláusula FROM de outra instrução SELECT, que irá somar os valores de quantidade para um determinado ID sobre as entradas que se sobrepõem com a máxima margens indicadas acima.
SELECT M.ID, M.Start, M.Finish, SUM(D.Amount)
FROM Data AS D,
(SELECT DISTINCT F.ID, F.Start, L.Finish
FROM Data AS F, Data AS L
WHERE F.Start < L.Finish
AND F.ID = L.ID
-- There are no gaps between F.Finish and L.Start
AND NOT EXISTS (SELECT *
FROM Data AS M
WHERE M.ID = F.ID
AND F.Finish < M.Start
AND M.Start < L.Start
AND NOT EXISTS (SELECT *
FROM Data AS T1
WHERE T1.ID = F.ID
AND T1.Start < M.Start
AND M.Start <= T1.Finish))
-- Cannot be extended further
AND NOT EXISTS (SELECT *
FROM Data AS T2
WHERE T2.ID = F.ID
AND ((T2.Start < F.Start AND F.Start <= T2.Finish)
OR (T2.Start <= L.Finish AND L.Finish < T2.Finish)))) AS M
WHERE D.ID = M.ID
AND M.Start <= D.Start
AND M.Finish >= D.Finish
GROUP BY M.ID, M.Start, M.Finish
ORDER BY M.ID, M.Start;
Isto dá:
ID Start Finish Amount
01 2008-10-01 2008-10-02 10
01 2008-10-03 2008-10-05 61
02 2008-10-02 2008-10-03 20
02 2008-10-06 2008-10-08 11
03 2008-10-05 2008-10-06 14
03 2008-10-05 2008-11-08 33 -- Here be trouble!
03 2008-10-08 2008-11-08 19
Editado : Este é quase do conjunto de dados correta em que para fazer a contagem e agregação SUM solicitado com a pergunta original, então a resposta final é:
SELECT I.ID, COUNT(*) AS Number, SUM(I.Amount) AS Amount
FROM (SELECT M.ID, M.Start, M.Finish, SUM(D.Amount) AS Amount
FROM Data AS D,
(SELECT DISTINCT F.ID, F.Start, L.Finish
FROM Data AS F, Data AS L
WHERE F.Start < L.Finish
AND F.ID = L.ID
-- There are no gaps between F.Finish and L.Start
AND NOT EXISTS
(SELECT *
FROM Data AS M
WHERE M.ID = F.ID
AND F.Finish < M.Start
AND M.Start < L.Start
AND NOT EXISTS
(SELECT *
FROM Data AS T1
WHERE T1.ID = F.ID
AND T1.Start < M.Start
AND M.Start <= T1.Finish))
-- Cannot be extended further
AND NOT EXISTS
(SELECT *
FROM Data AS T2
WHERE T2.ID = F.ID
AND ((T2.Start < F.Start AND F.Start <= T2.Finish) OR
(T2.Start <= L.Finish AND L.Finish < T2.Finish)))
) AS M
WHERE D.ID = M.ID
AND M.Start <= D.Start
AND M.Finish >= D.Finish
GROUP BY M.ID, M.Start, M.Finish
) AS I
GROUP BY I.ID
ORDER BY I.ID;
id number amount
01 2 71
02 2 31
03 3 66
revisão : Oh! Drat ... a entrada para 3 tem duas vezes o 'valor' que deveria ter. partes anteriores 'editada' indicam onde as coisas começaram a dar errado. Parece que quer a primeira consulta é sutilmente errado (talvez que se destine a uma pergunta diferente), ou o otimizador Eu estou trabalhando com está se comportando mal. No entanto, deve haver uma resposta estreitamente relacionado com esse que vai dar os valores corretos.
Para o registro:. Testado em IBM Informix Dynamic Server 11.50 no Solaris 10. No entanto, deve funcionar bem em qualquer outro SQL DBMS moderadamente padrão-conformant
Provavelmente precisa criar um cursor e percorrer os resultados, mantendo o controle de qual id você está trabalhando e acumulando os dados ao longo do caminho. Quando o ID de mudanças que você pode inserir os dados acumulados em uma tabela temporária e retornar a tabela no final do procedimento (selecionar tudo a partir dele). Uma função baseada em tabela poderia ser melhor como você pode em seguida, basta inserir na tabela o retorno que você vá junto.
Eu suspeito que ele pode exigir iteração de algum tipo, mas eu não quero ir por esse caminho.
Eu acho que é a rota que você terá que tomar, usar um cursor para preencher uma variável de tabela. Se você tem um grande número de registros que você pode usar uma tabela permanente para armazenar os resultados, em seguida, quando você precisa para recuperar os dados que você poderia processar apenas os novos dados.
Gostaria de acrescentar um campo bit com um padrão de 0 a tabela de origem para manter o controle de quais registros foram processados. Supondo que ninguém está usando select * sobre a mesa, adicionando uma coluna com um valor padrão não afetará o resto de sua aplicação.
Adicionar um comentário a este post se você quiser ajudar a codificação da solução.
Bem, eu decidi ir abaixo da rota iteração usando uma mistura de junta e cursores. Juntando-se a tabela de dados contra si mesmo posso criar uma lista de links de apenas os registos que são consecutivos.
INSERT INTO #CONSEC
SELECT a.ID, a.Start, b.Finish, b.Amount
FROM Data a JOIN Data b
ON (a.Finish = b.Start) AND (a.ID = b.ID)
Então eu posso descansar a lista por iteração sobre ele com um cursor, e fazendo atualizações de volta para a tabela de dados para ajustar (e excluir os registros agora estranhos da tabela de dados a)
DECLARE CCursor CURSOR FOR
SELECT ID, Start, Finish, Amount FROM #CONSEC ORDER BY Start DESC
@Total = 0
OPEN CCursor
FETCH NEXT FROM CCursor INTO @ID, @START, @FINISH, @AMOUNT
WHILE @FETCH_STATUS = 0
BEGIN
@Total = @Total + @Amount
@Start_Last = @Start
@Finish_Last = @Finish
@ID_Last = @ID
DELETE FROM Data WHERE Start = @Finish
FETCH NEXT FROM CCursor INTO @ID, @START, @FINISH, @AMOUNT
IF (@ID_Last<> @ID) OR (@Finish<>@Start_Last)
BEGIN
UPDATE Data
SET Amount = Amount + @Total
WHERE Start = @Start_Last
@Total = 0
END
END
CLOSE CCursor
DEALLOCATE CCursor
Esta todas as obras e tem um desempenho aceitável para os dados típicos que estou usando.
Eu fiz encontrar um pequeno problema com o código acima. Inicialmente eu estava atualizando a tabela de dados do em cada loop através do cursor. Mas isso não funcionou. Parece que você só pode fazer uma atualização em um registro, e que várias atualizações (em ordem para continuar a acrescentar dados) reverter para a leitura dos conteúdos originais do registro.