O problema real envolve muito mais dados e junções, mas criei uma pequena amostra para demonstrar o problema:
-- create example table
DROP TABLE dbo.EventRecords
GO
CREATE TABLE dbo.EventRecords
(
EventDate datetime NOT NULL,
EventCount int NOT NULL
) ON [PRIMARY]
GO
ALTER TABLE dbo.EventRecords ADD CONSTRAINT
PK_EventRecords PRIMARY KEY CLUSTERED
(
EventDate,
EventCount
) WITH( STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
GO
-- put in some random data for example
DECLARE @Counter INT=0
WHILE (@Counter<1000)
BEGIN
DECLARE @SemiRandomCount1 INT=@Counter*589043%23
DECLARE @SemiRandomCount2 INT=@Counter*85907%7
IF @SemiRandomCount1>0 AND @Counter%7<>0 -- leave some dates empty
BEGIN
INSERT INTO dbo.EventRecords(EventDate,EventCount)
VALUES (DATEADD(day,@Counter,'2013-01-01'),@SemiRandomCount1)
PRINT CAST(@SemiRandomCount2 AS VARCHAR(MAX))
IF @SemiRandomCount2>0 AND @Counter%2=0 -- some dates have multiple entries
INSERT INTO dbo.EventRecords(EventDate,EventCount)
VALUES (DATEADD(day,@Counter,'2013-01-01'),@SemiRandomCount2)
END
SET @Counter=@Counter+1
END
--SELECT * FROM dbo.EventRecords
Portanto, algumas datas têm várias entradas, outras não. Preciso obter resultados para relatórios que contenham todas as datas em um intervalo especificado, com as contagens totais para essa data (zero se não houver contagens para essa data). Depois de muito pesquisar no Google e experimentar, descobri uma maneira muito inteligente de gerar sequências em tempo real e construí essa função a partir dela. Essas sequências podem ser usadas para criar uma tabela de sequência de datas em tempo real, que pode ser usada para ingressar na tabela EventRecords e agrupar por data sem lacunas:
IF NOT EXISTS (SELECT * FROM dbo.sysobjects WHERE name='GetSequence')
EXECUTE sp_executesql N'CREATE FUNCTION GetSequence() RETURNS @Table TABLE (Value SMALLINT NOT NULL) AS BEGIN RETURN END'
GO
ALTER FUNCTION [dbo].[GetSequence](@StartInclusive INT, @EndExclusive INT)
RETURNS @Sequence TABLE
(
Value BIGINT NOT NULL
)
AS
BEGIN
INSERT @Sequence
SELECT Value=@StartInclusive+n-1
FROM (SELECT ROW_NUMBER() OVER (ORDER BY o1.n)
FROM (SELECT n=ROW_NUMBER() OVER (ORDER BY object_id) FROM sys.objects WITH (NOLOCK)) o1
CROSS JOIN (SELECT n=ROW_NUMBER() OVER (ORDER BY object_id) FROM sys.objects WITH (NOLOCK)) o2
CROSS JOIN (SELECT n=ROW_NUMBER() OVER (ORDER BY object_id) FROM sys.objects WITH (NOLOCK)) o3
CROSS JOIN (SELECT n=ROW_NUMBER() OVER (ORDER BY object_id) FROM sys.objects WITH (NOLOCK)) o4
CROSS JOIN (SELECT n=ROW_NUMBER() OVER (ORDER BY object_id) FROM sys.objects WITH (NOLOCK)) o5
CROSS JOIN (SELECT n=ROW_NUMBER() OVER (ORDER BY object_id) FROM sys.objects WITH (NOLOCK)) o6
) D (n)
WHERE n<=@EndExclusive-@StartInclusive
RETURN
END
GO
Aqui estão exemplos de consultas:
DECLARE @StartDate DATE='2013-01-01'
DECLARE @EndDate DATE='2015-01-01'
-- query with holes: not what I need
SELECT [EventDate], [TotalEventCount]=ISNULL(SUM(EventCount),0)
FROM dbo.EventRecords
GROUP BY [EventDate]
-- query with date holes filled in: this is what I need
SELECT
[EventDate]=DATEADD(day,s.Value,@StartDate),
[TotalEventCount]=ISNULL(SUM(EventCount),0)
FROM [dbo].[GetSequence](0,DATEDIFF(day,@StartDate,@EndDate)) s
LEFT JOIN dbo.EventRecords c
ON DATEDIFF(day,@StartDate, EventDate)=s.Value
GROUP BY s.Value
Então, minha pergunta é: existe uma maneira melhor (mais simples ou mais rápida) de obter sequências ou uma maneira melhor de resolver esse problema no SQL?
Eu recomendo usar uma tabela de calendário ou dimensão de data (qualquer nome que você preferir). Aqui está uma resposta com o uso de um CTE rápido.
Alguns links sobre tabelas de calendário:
Bem, você deve ter uma tabela de números ou uma tabela de calendário. Vou começar com uma tabela do Numbers:
Então:
Uma tabela de calendário, conforme descrito na outra resposta , é ainda melhor, mas deve ter um desempenho muito melhor do que sua função.
Se você não tem uma tabela do Numbers ou do Calendar e também não deseja criar (leia os links na parte inferior desta resposta e deste artigo primeiro), então você pode usar coisas internas, como
spt_values
se o seu máximo intervalo de datas é inferior a cerca de 2.000 dias:Se você precisar de mais de 2.000 dias (quem vai consumir um relatório que abrange dois anos de dias individuais, afinal?), você pode usar uma única
ROW_NUMBER() OVER (ORDER BY [object_id]) FROM sys.all_columns
ou uma junção cruzada de diferentes exibições de catálogo para atender ao seu intervalo máximo, em vez de um junção cruzada desleixada de muitas consultas individuais. Mas, na verdade, a tabela Calendário é sua melhor aposta.