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

时间:2015-02-23 16:26:51

标签: sql select sql-server-2012 date-range

请原谅我的经验不足,我希望这不是一个愚蠢的问题,我被困住了,没有别的地方可以转。我会坚持到这一点:

我正在尝试使用结果收集工资单数据,如下所示: enter image description here

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

我的表格如下:

enter image description here

我不能成为第一个需要这样做的人,任何文章/帖子或任何可以帮助我实现这一目标的东西?即使伪代码也会有所帮助;什么!

万分感谢!

这是我能够提出的,但我甚至不确定它是否可行:

-- 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

2 个答案:

答案 0 :(得分:2)

这很有意思。我认为这会做你想要的。第一个测试数据:

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)

以与您的示例类似的格式生成数据,包含报告范围中的所有日期以及每个人的一行。从上面我看到:

Example output

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

SQL小提琴示例here

答案 1 :(得分:1)

这是我为了显示日程安排或出勤而完成的主题变体。我希望类似的内容可以与您的报告一起使用。这是存储过程的开始:

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]