SQL 查询 - 根据日期范围收集数据 - 可能的列数可变

SQL Query - gather data based on date range - possible variable number of columns

请原谅我的经验不足,我希望这不是一个太愚蠢的问题,我被困住了,无处可去。我会直截了当:

我正在尝试收集工资单数据,结果如下:

我遇到的问题是列数可变。我将获得一个日期范围,并且需要 return 给定范围内每一天的出勤记录,如果没有数据则为空值。我使用 WebAPI 作为中间层,因此我能够执行进一步的数据操作来实现此结果。

我的表格如下:

我不可能是第一个需要完成此操作的人,任何 articles/posts 或任何可以帮助我完成此操作的东西?即使是伪代码也会有所帮助;任何东西!

感谢一百万的提前!

这是我能够想到的,但我什至不确定它是否可行:

-- convert date range into days of month 
-- to ensure null values are included in data??
DECLARE @intFlag INT = 0;
DECLARE @numberOfDays INT = DATEDIFF(DAY, @startDate, @endDate);
DECLARE @TMP TABLE (DaysOfMonth date)

WHILE (@intFlag <= @numberOfDays)
BEGIN
    INSERT INTO @TMP VALUES (DATEADD(DAY, @intFlag, @startDate));
    SET @intFlag = @intFlag + 1
END

-- select days in given data range so c# app can build header row
-- would it help if I pivot this data?
SELECT
    DaysOfMonth
FROM
    @TMP
ORDER BY
    DaysOfMonth

-- get a count for number of people
DECLARE @count INT = 0;
DECLARE @TMPPPL TABLE (Id int identity(1,0), PId Int)

INSERT INTO 
    @TMPPPL
SELECT 
    p.PersonId
FROM 
    dbo.People p
JOIN 
    dbo.UserTypes ut on p.UserType_UserTypeId = ut.UserTypeId and (ut.Code = 'caregiver' or ut.Code = 'director')

DECLARE @numberOfPeople INT = (SELECT COUNT(1) FROM @TMPPPL)

-- create and execute sproc to return row of data for each person
WHILE (@count <= @numberOfPeople)
BEGIN

    -- STUCK HERE, This obviously won't work but what else can I do?
    EXEC GetPersonAttendanceHours @personId, @startDate, @endDate;

    SET @count = @count + 1
END

这是我为显示日程安排或出勤情况所做的主题的变体。我希望类似的东西应该适用于您的报告。这是您的存储过程的开头:

DECLARE @iDay INT = 0;
DECLARE @countDays INT = DATEDIFF(DAY, @startDate, @endDate);
DECLARE @tempDates TABLE ([tempDate] DATE);
DECLARE @filterDates NVARCHAR;
WHILE (@iDay <= @countDays)
BEGIN
  INSERT INTO @tempDates VALUES (DATEADD(DAY, @iDay, @startDate));
  SET @iDay = @iDay + 1;
END;
SELECT @filterDates = STUFF(
  (SELECT N''',''' + CONVERT(NVARCHAR, [tempDate], 103) FROM @tempDates FOR XML PATH('')),
  1,
  2,
  ''  
);

你的建议是正确的。下一个查询在您 PIVOT 之前获取您的数据。

SELECT [People].[Person_PersonID], [tempDates].[tempDate], [Attendances].[SignIn], [Attendances].[SignOut],
  MIN([Attendances].[SignOut], DATEADD(DAY, 1, [tempDates].[tempDate]))
  - MAX([Attendances].[SignIn], [tempDates].[tempDate]) * 24 AS [numHours]
FROM [People]
CROSS JOIN @tempDates [tempDates]
LEFT JOIN [Attendances]
  ON (
    ([Attendances].[SignIn] < DATEADD(DAY, 1, [tempDates].[tempDate]))
    AND ([Attendances].[SignOut] > [tempDates].[tempDate])
  );

一旦我们对上一个查询的结果感到满意,我们就会将其替换为使用 PIVOT 的查询,它应该看起来像这样。

SELECT *
FROM (
  SELECT [People].[PersonID], [tempDates].[tempDate], [Attendances].[SignIn], [Attendances].[SignOut],
    MIN([Attendances].[SignOut], DATEADD(DAY, 1, [tempDates].[tempDate]))
    - MAX([Attendances].[SignIn], [tempDates].[tempDate]) * 24 AS [numHours]
  FROM [People]
  CROSS JOIN @tempDates [tempDates]
  LEFT JOIN [Attendances]
    ON (
      ([Attendances].[SignIn] < DATEADD(DAY, 1, [tempDates].[tempDate]))
      AND ([Attendances].[SignOut] > [tempDates].[tempDate])
    )
) AS [DatedAttendance]
PIVOT (
  SUM([numHours]) FOR ([tempDate] IN (@filterDates))
) AS [PivotAttendance]
ORDER BY [PersonID]

这很有趣。我认为这会做你正在寻找的东西。第一个测试数据:

CREATE TABLE people (PersonID int, Name varchar(30))

INSERT INTO people (PersonID, Name)
SELECT 1, 'Kelly'
UNION ALL SELECT 2, 'Dave'
UNION ALL SELECT 3, 'Mike'

CREATE TABLE attendances (PersonID int, SignIn datetime, SignOut datetime)

INSERT INTO attendances (PersonID, SignIn, SignOut)
SELECT 1, '1-Feb-2015 08:00', '1-Feb-2015 09:00'
UNION ALL SELECT 1, '1-Feb-2015 12:00', '1-Feb-2015 12:30'
UNION ALL SELECT 2, '2-Feb-2015 08:00', '2-Feb-2015 08:15'
UNION ALL SELECT 1, '3-Feb-2015 08:00', '3-Feb-2015 09:00'
UNION ALL SELECT 1, '4-Feb-2015 08:00', '4-Feb-2015 08:30'
UNION ALL SELECT 2, '4-Feb-2015 08:00', '4-Feb-2015 10:00'
UNION ALL SELECT 2, '6-Feb-2015 12:00', '6-Feb-2015 15:00'
UNION ALL SELECT 3, '6-Feb-2015 15:00', '6-Feb-2015 17:00'
UNION ALL SELECT 3, '8-Feb-2015 10:00', '8-Feb-2015 12:00'

然后动态查询:

DECLARE @startDate DATETIME='1-Feb-2015'
DECLARE @endDate DATETIME='9-Feb-2015'
DECLARE @numberOfDays INT = DATEDIFF(DAY, @startDate, @endDate)

declare @dayColumns TABLE (delta int, colName varchar(12))

-- Produce 1 row for each day in the report. Note that this is limited by the 
-- number of objects in sysobjects (which is about 2000 so it's a high limit)
-- Each row contains a delta date offset, @startDate+delta gives each date to report 
-- which is converted to a valid SQL column name in the format colYYYYMMDD
INSERT INTO @dayColumns (delta, colName)
SELECT delta, 'col'+CONVERT(varchar(12),DATEADD(day,delta,@startDate),112) as colName from (
  select (ROW_NUMBER() OVER (ORDER BY sysobjects.id))-1 as delta FROM sysobjects 
) daysAhead
WHERE delta<=@numberOfDays

-- Create a comma seperated list of columns to report
DECLARE @cols AS NVARCHAR(MAX)= ''
SELECT @cols=CASE WHEN @cols='' THEN @cols ELSE @cols+',' END + colName FROM @dayColumns ORDER BY delta
DECLARE @totalHours AS NVARCHAR(MAX)= ''
SELECT @totalHours=CASE WHEN @totalHours='' THEN '' ELSE @totalHours+' + ' END + 'ISNULL(' + colName +',0)' FROM @dayColumns ORDER BY delta

-- Produce a SQL statement which outputs a variable number of pivoted columns
DECLARE @query AS NVARCHAR(MAX)
SELECT @query=
'declare @days TABLE (reportDay date, colName varchar(12))

INSERT INTO @days (reportDay, colName)
SELECT DATEADD(day,Delta,'''+CONVERT(varchar(22),@startDate,121)+'''), ''col''+CONVERT(varchar(12),DATEADD(day,delta,'''+CONVERT(varchar(22),@startDate,121)+'''),112) as colName from (
  select (ROW_NUMBER() OVER (ORDER BY sysobjects.id))-1 as Delta FROM sysobjects 
) daysAhead
WHERE Delta<='+CAST(@numberOfDays as varchar(10))+'

SELECT p.Name, pivotedAttendance.*,'+@totalHours+' as totalHours FROM (
  SELECT * FROM (
    select p.PersonID, d.colName, CAST(DATEDIFF(MINUTE, a.SignIn, a.SignOut)/60.0 as decimal(5,1)) as hrsAttendance 
    from @days d
    CROSS JOIN people p 
    LEFT OUTER JOIN attendances a ON a.PersonID=p.PersonID AND CAST(a.SignOut as DATE)=d.reportDay
  ) as s
  PIVOT (
    SUM(hrsAttendance) FOR colName in ('+@cols+')
  ) as pa
) as pivotedAttendance
INNER JOIN people p on p.PersonID=pivotedAttendance.PersonID'

-- Run the query
EXEC (@query)

它生成的数据格式与您的示例类似,报告范围内的所有天数和每个人的一行。从上面我看到:

出于演示目的,您应该能够将列名转换为可显示的日期(只需从列名中解析 YYYYMMDD)。日期不能直接用作列名,因为它会产生无效的列名。

SQL Fiddle 示例 here.