SQL Server:按8个生成小时分组数据

问题描述 投票:2回答:1

这是我的问题,我有一个票证表,其中存储了已读的票证,用户轮班工作8小时。我需要将门票分为8组阅读。

基本上我需要类似的东西

如果HourStart是15:20

Group      Hour     Quantity
  1      15:20:00     20
  2      16:20:00     20
  3      17:20:00     40
  4      18:20:00     0
  5      19:20:00     0
  6      20:20:00     0
  7      21:20:00     0
  8      22:20:00     0

所以,因为我一直都需要8行,所以我认为创建一个临时表将是最好的,因此我可以进行联接,并且如果在那些小时内未输入任何记录,则即使使用空数据,行仍会显示。

问题是,这在性能上有点慢,有点脏,我在寻找是否有更好的方法可以按某些生成的行对数据进行分组,而不必创建临时表

CREATE TABLE Production.hProductionRecods
(
    ID INT IDENTITY PRIMARY KEY,
    HourStart TIME
)

CREATE TABLE Production.hTickets
(
    ID INT IDENTITY PRIMARY KEY,
    DateRead DATETIME,
    ProductionRecordId INT
)

CREATE TABLE #TickersPerHour
(
    Group INT,
    Hour TIME       
)

DECLARE @HourStart TIME = (SELECT HourStart 
                           FROM Production.hProductionRecords 
                           WHERE Id = 1)

INSERT INTO #TickersPerHour (Group, Hour)
VALUES (1, @HourStart),
       (2, DATEADD(hh, 1, @HourStart)),
       (3, DATEADD(hh, 2, @HourStart)),
       (4, DATEADD(hh, 3, @HourStart)),
       (5, DATEADD(hh, 4, @HourStart)),
       (6, DATEADD(hh, 5, @HourStart)),
       (7, DATEADD(hh, 6, @HourStart)),
       (8, DATEADD(hh, 7, @HourStart))

SELECT 
    TEMP.Group,
    TEMP.Hour,
    ISNULL(SUM(E.Quantity),0) Quantity
FROM 
    Production.hProductionRecords P
LEFT JOIN 
    Production.hTickets E ON E.ProductionRecordId = P.Id
RIGHT JOIN 
    #TickersPerHour TEMP 
       ON TEMP.Hour = CASE
                         WHEN CAST(E.DateRead AS TIME) >= P.HourStart 
                              AND CAST(E.DateRead AS TIME) < DATEADD(hour, 1, P.HourStart) 
                            THEN DATEADD(hour, 1, P.HourStart)
                         WHEN CAST(E.DateRead AS TIME) >= P.HourStart 
                              AND CAST(E.DateRead AS TIME) < DATEADD(hour, 2, P.HourStart) 
                            THEN DATEADD(hour, 2, P.HourStart)
                         WHEN CAST(E.DateRead AS TIME) >= P.HourStart 
                              AND CAST(E.DateRead AS TIME) < DATEADD(hour, 3, P.HourStart) 
                            THEN DATEADD(hour, 3, P.HourStart)
                         WHEN CAST(E.DateRead AS TIME) >= P.HourStart 
                              AND CAST(E.DateRead AS TIME) < DATEADD(hour, 4, P.HourStart) 
                            THEN DATEADD(hour, 4, P.HourStart)
                         WHEN CAST(E.DateRead AS TIME) >= P.HourStart 
                              AND CAST(E.DateRead AS TIME) < DATEADD(hour, 5, P.HourStart) 
                            THEN DATEADD(hour,5, P.HourStart)
                         WHEN CAST(E.DateRead AS TIME) >= P.HourStart 
                              AND CAST(E.DateRead AS TIME) < DATEADD(hour, 6, P.HourStart) 
                            THEN DATEADD(hour, 6, P.HourStart)
                         WHEN CAST(E.DateRead AS TIME) >= P.HourStart 
                              AND CAST(E.DateRead AS TIME) < DATEADD(hour, 7, P.HourStart) 
                            THEN DATEADD(hour,7, P.HourStart)
                         WHEN CAST(E.DateRead AS TIME) >= P.HourStart 
                              AND CAST(E.DateRead AS TIME) < DATEADD(hour, 8, P.HourStart)  
                            THEN DATEADD(hour, 8, P.HourStart)
                      END
GROUP BY        
    TEMP.Group, TEMP.Hour
ORDER BY 
    Group

DROP TABLE #TickersPerHour
sql-server
1个回答
0
投票

我正在寻找是否有更好的方法可以将生成的数据分组行而不必创建临时表

代替临时表,您可以构建一个惰性序列,因为这需要rangeAB(本文结尾的代码)。惰性序列(又名Tally表或数字虚拟辅助表)非常讨厌。注意这个例子:

DECLARE @HourStart TIME = '15:20:00';

SELECT 
 r.RN,
 r.OP,
 TimeAsc  = DATEADD(HOUR,r.RN,@HourStart),
 TimeDesc = DATEADD(HOUR,r.OP,@HourStart)
FROM dbo.rangeAB(0,7,1,0) AS r
ORDER BY r.RN;

结果:

RN   OP   TimeAsc          TimeDesc
---- ---- ---------------- ----------------
0    7    15:20:00.0000000 22:20:00.0000000
1    6    16:20:00.0000000 21:20:00.0000000
2    5    17:20:00.0000000 20:20:00.0000000
3    4    18:20:00.0000000 19:20:00.0000000
4    3    19:20:00.0000000 18:20:00.0000000
5    2    20:20:00.0000000 17:20:00.0000000
6    1    21:20:00.0000000 16:20:00.0000000
7    0    22:20:00.0000000 15:20:00.0000000

请注意,我能够在执行计划中的ASCending 和/或 DESCending 无排序的订单中生成这些日期。这是因为rangeAB利用了我所说的Virtual Index。您可以排序,分组等等,甚至可以在RN列上加入而无需排序。注意执行计划-不排序,那是巨大的!

enter image description here

现在使用RangeAB解决您的问题:

-- Create Some Sample Data
DECLARE @things TABLE (Something CHAR(1), SomeTime TIME);

WITH Something(x) AS (SELECT 1)
INSERT @things 
SELECT x, xx
FROM   Something
CROSS APPLY (VALUES('15:30:00'),('20:19:00'),('16:30:00'),('16:33:00'),
                   ('17:10:00'),('18:13:00'),('19:01:00'),('21:35:00'),
                   ('15:13:00'),('21:55:00'),('19:22:00'),('16:39:00')) AS f(xx);

-- Solution:
DECLARE @HourStart TIME = '15:20:00'; -- you get via a subquery

SELECT 
 GroupNumber = r.RN+1,
 HourStart   = MAX(f.HStart),
 Quantity    = COUNT(t.SomeThing)
FROM        dbo.rangeAB(0,7,1,0) AS r
CROSS APPLY (VALUES(DATEADD(HOUR,r.RN,@HourStart))) AS f(HStart)
CROSS APPLY (VALUES(DATEADD(SECOND,3599,f.HStart))) AS f2(HEnd)
LEFT JOIN   @things              AS t 
       ON   t.SomeTime BETWEEN f.HStart AND f2.HEnd
GROUP BY    r.RN;

结果:

GroupNumber   HourStart         Quantity
------------- ----------------- -----------
1             15:20:00.0000000  1
2             16:20:00.0000000  4
3             17:20:00.0000000  1
4             18:20:00.0000000  1
5             19:20:00.0000000  2
6             20:20:00.0000000  0
7             21:20:00.0000000  2
8             22:20:00.0000000  0

执行计划:

enter image description here

让我知道您是否有疑问。 dbo.rangeAB。

CREATE FUNCTION dbo.rangeAB
(
  @low  bigint, 
  @high bigint, 
  @gap  bigint,
  @row1 bit
)
/****************************************************************************************
[Purpose]:
 Creates up to 531,441,000,000 sequentia1 integers numbers beginning with @low and ending 
 with @high. Used to replace iterative methods such as loops, cursors and recursive CTEs 
 to solve SQL problems. Based on Itzik Ben-Gan's getnums function with some tweeks and 
 enhancements and added functionality. The logic for getting rn to begin at 0 or 1 is 
 based comes from Jeff Moden's fnTally function. 

 The name range because it's similar to clojure's range function. The name "rangeAB" as 
 used because "range" is a reserved SQL keyword.

[Author]: Alan Burstein

[Compatibility]: 
 SQL Server 2008+ and Azure SQL Database

[Syntax]:
 SELECT r.RN, r.OP, r.N1, r.N2
 FROM dbo.rangeAB(@low,@high,@gap,@row1) AS r;

[Parameters]:
 @low  = a bigint that represents the lowest value for n1.
 @high = a bigint that represents the highest value for n1.
 @gap  = a bigint that represents how much n1 and n2 will increase each row; @gap also
         represents the difference between n1 and n2.
 @row1 = a bit that represents the first value of rn. When @row = 0 then rn begins
         at 0, when @row = 1 then rn will begin at 1.

[Returns]:
 Inline Table Valued Function returns:
 rn = bigint; a row number that works just like T-SQL ROW_NUMBER() except that it can 
      start at 0 or 1 which is dictated by @row1.
 op = bigint; returns the "opposite number that relates to rn. When rn begins with 0 and
      ends with 10 then 10 is the opposite of 0, 9 the opposite of 1, etc. When rn begins
      with 1 and ends with 5 then 1 is the opposite of 5, 2 the opposite of 4, etc...
 n1 = bigint; a sequential number starting at the value of @low and incrimentingby the
      value of @gap until it is less than or equal to the value of @high.
 n2 = bigint; a sequential number starting at the value of @low+@gap and  incrimenting 
      by the value of @gap.

[Dependencies]:
N/A

[Developer Notes]:

 1. The lowest and highest possible numbers returned are whatever is allowable by a 
    bigint. The function, however, returns no more than 531,441,000,000 rows (8100^3). 
 2. @gap does not affect rn, rn will begin at @row1 and increase by 1 until the last row
    unless its used in a query where a filter is applied to rn.
 3. @gap must be greater than 0 or the function will not return any rows.
 4. Keep in mind that when @row1 is 0 then the highest row-number will be the number of
    rows returned minus 1
 5. If you only need is a sequential set beginning at 0 or 1 then, for best performance
    use the RN column. Use N1 and/or N2 when you need to begin your sequence at any 
    number other than 0 or 1 or if you need a gap between your sequence of numbers. 
 6. Although @gap is a bigint it must be a positive integer or the function will
    not return any rows.
 7. The function will not return any rows when one of the following conditions are true:
      * any of the input parameters are NULL
      * @high is less than @low 
      * @gap is not greater than 0
    To force the function to return all NULLs instead of not returning anything you can
    add the following code to the end of the query:

      UNION ALL 
      SELECT NULL, NULL, NULL, NULL
      WHERE NOT (@high&@low&@gap&@row1 IS NOT NULL AND @high >= @low AND @gap > 0)

    This code was excluded as it adds a ~5% performance penalty.
 8. There is no performance penalty for sorting by rn ASC; there is a large performance 
    penalty for sorting in descending order WHEN @row1 = 1; WHEN @row1 = 0
    If you need a descending sort the use op in place of rn then sort by rn ASC. 

Best Practices:
--===== 1. Using RN (rownumber)
 -- (1.1) The best way to get the numbers 1,2,3...@high (e.g. 1 to 5):
 SELECT RN FROM dbo.rangeAB(1,5,1,1);
 -- (1.2) The best way to get the numbers 0,1,2...@high-1 (e.g. 0 to 5):
 SELECT RN FROM dbo.rangeAB(0,5,1,0);

--===== 2. Using OP for descending sorts without a performance penalty
 -- (2.1) The best way to get the numbers 5,4,3...@high (e.g. 5 to 1):
 SELECT op FROM dbo.rangeAB(1,5,1,1) ORDER BY rn ASC;
 -- (2.2) The best way to get the numbers 0,1,2...@high-1 (e.g. 5 to 0):
 SELECT op FROM dbo.rangeAB(1,6,1,0) ORDER BY rn ASC;

--===== 3. Using N1
 -- (3.1) To begin with numbers other than 0 or 1 use N1 (e.g. -3 to 3):
 SELECT N1 FROM dbo.rangeAB(-3,3,1,1);
 -- (3.2) ROW_NUMBER() is built in. If you want a ROW_NUMBER() include RN:
 SELECT RN, N1 FROM dbo.rangeAB(-3,3,1,1);
 -- (3.3) If you wanted a ROW_NUMBER() that started at 0 you would do this:
 SELECT RN, N1 FROM dbo.rangeAB(-3,3,1,0);

--===== 4. Using N2 and @gap
 -- (4.1) To get 0,10,20,30...100, set @low to 0, @high to 100 and @gap to 10:
 SELECT N1 FROM dbo.rangeAB(0,100,10,1);
 -- (4.2) Note that N2=N1+@gap; this allows you to create a sequence of ranges.
 --       For example, to get (0,10),(10,20),(20,30).... (90,100):
 SELECT N1, N2 FROM dbo.rangeAB(0,90,10,1);
 -- (4.3) Remember that a rownumber is included and it can begin at 0 or 1:
 SELECT RN, N1, N2 FROM dbo.rangeAB(0,90,10,1);

[Examples]:
--===== 1. Generating Sample data (using rangeAB to create "dummy rows")
 -- The query below will generate 10,000 ids and random numbers between 50,000 and 500,000
 SELECT
   someId    = r.rn,
   someNumer = ABS(CHECKSUM(NEWID())%450000)+50001 
 FROM rangeAB(1,10000,1,1) r;

--===== 2. Create a series of dates; rn is 0 to include the first date in the series
 DECLARE @startdate DATE = '20180101', @enddate DATE = '20180131';

 SELECT r.rn, calDate = DATEADD(dd, r.rn, @startdate)
 FROM dbo.rangeAB(1, DATEDIFF(dd,@startdate,@enddate),1,0) r;
 GO

--===== 3. Splitting (tokenizing) a string with fixed sized items
 -- given a delimited string of identifiers that are always 7 characters long
 DECLARE @string VARCHAR(1000) = 'A601225,B435223,G008081,R678567';

 SELECT
   itemNumber = r.rn, -- item's ordinal position 
   itemIndex  = r.n1, -- item's position in the string (it's CHARINDEX value)
   item       = SUBSTRING(@string, r.n1, 7) -- item (token)
 FROM dbo.rangeAB(1, LEN(@string), 8,1)  r;
 GO

--===== 4. Splitting (tokenizing) a string with random delimiters
 DECLARE @string VARCHAR(1000) = 'ABC123,999F,XX,9994443335';

 SELECT
   itemNumber = ROW_NUMBER() OVER (ORDER BY r.rn), -- item's ordinal position 
   itemIndex  = r.n1+1, -- item's position in the string (it's CHARINDEX value)
   item       = SUBSTRING
               (
                 @string,
                 r.n1+1,
                 ISNULL(NULLIF(CHARINDEX(',',@string,r.n1+1),0)-r.n1-1, 8000)
               ) -- item (token)
 FROM dbo.rangeAB(0,DATALENGTH(@string),1,1) r
 WHERE SUBSTRING(@string,r.n1,1) = ',' OR r.n1 = 0;
 -- logic borrowed from: http://www.sqlservercentral.com/articles/Tally+Table/72993/

--===== 5. Grouping by a weekly intervals
 -- 5.1. how to create a series of start/end dates between @startDate & @endDate
 DECLARE @startDate DATE = '1/1/2015', @endDate DATE = '2/1/2015';
 SELECT 
   WeekNbr   = r.RN,
   WeekStart = DATEADD(DAY,r.N1,@StartDate), 
   WeekEnd   = DATEADD(DAY,r.N2-1,@StartDate)
 FROM dbo.rangeAB(0,datediff(DAY,@StartDate,@EndDate),7,1) r;
 GO

 -- 5.2. LEFT JOIN to the weekly interval table
 BEGIN
  DECLARE @startDate datetime = '1/1/2015', @endDate datetime = '2/1/2015';
  -- sample data 
  DECLARE @loans TABLE (loID INT, lockDate DATE);
  INSERT @loans SELECT r.rn, DATEADD(dd, ABS(CHECKSUM(NEWID())%32), @startDate)
  FROM dbo.rangeAB(1,50,1,1) r;

  -- solution 
  SELECT 
    WeekNbr   = r.RN,
    WeekStart = dt.WeekStart, 
    WeekEnd   = dt.WeekEnd,
    total     = COUNT(l.lockDate)
  FROM dbo.rangeAB(0,datediff(DAY,@StartDate,@EndDate),7,1) r
  CROSS APPLY (VALUES (
    CAST(DATEADD(DAY,r.N1,@StartDate) AS DATE), 
    CAST(DATEADD(DAY,r.N2-1,@StartDate) AS DATE))) dt(WeekStart,WeekEnd)
  LEFT JOIN @loans l ON l.lockDate BETWEEN  dt.WeekStart AND dt.WeekEnd
  GROUP BY r.RN, dt.WeekStart, dt.WeekEnd ;
 END;

--===== 6. Identify the first vowel and last vowel in a along with their positions
 DECLARE @string VARCHAR(200) = 'This string has vowels';

 SELECT TOP(1) position = r.rn, letter = SUBSTRING(@string,r.rn,1)
 FROM dbo.rangeAB(1,LEN(@string),1,1) r
 WHERE SUBSTRING(@string,r.rn,1) LIKE '%[aeiou]%'
 ORDER BY r.rn;

 -- To avoid a sort in the execution plan we'll use op instead of rn
 SELECT TOP(1) position = r.op, letter = SUBSTRING(@string,r.op,1)
 FROM dbo.rangeAB(1,LEN(@string),1,1) r
 WHERE SUBSTRING(@string,r.rn,1) LIKE '%[aeiou]%'
 ORDER BY r.rn;

---------------------------------------------------------------------------------------
[Revision History]:
 Rev 00 - 20140518 - Initial Development - Alan Burstein
 Rev 01 - 20151029 - Added 65 rows to make L1=465; 465^3=100.5M. Updated comment section
                   - Alan Burstein
 Rev 02 - 20180613 - Complete re-design including opposite number column (op)
 Rev 03 - 20180920 - Added additional CROSS JOIN to L2 for 530B rows max - Alan Burstein
****************************************************************************************/
RETURNS TABLE WITH SCHEMABINDING AS RETURN
WITH L1(N) AS 
(
  SELECT 1
  FROM (VALUES
   (0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),
   (0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),
   (0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),
   (0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),
   (0),(0)) T(N) -- 90 values 
),
L2(N)  AS (SELECT 1 FROM L1 a CROSS JOIN L1 b CROSS JOIN L1 c),
iTally AS (SELECT rn = ROW_NUMBER() OVER (ORDER BY (SELECT 1)) FROM L2 a CROSS JOIN L2 b)
SELECT
  r.RN,
  r.OP,
  r.N1,
  r.N2
FROM
(
  SELECT
    RN = 0,
    OP = (@high-@low)/@gap,
    N1 = @low,
    N2 = @gap+@low
  WHERE @row1 = 0
  UNION ALL -- ISNULL required in the TOP statement below for error handling purposes
  SELECT TOP (ABS((ISNULL(@high,0)-ISNULL(@low,0))/ISNULL(@gap,0)+ISNULL(@row1,1)))
    RN = i.rn,
    OP = (@high-@low)/@gap+(2*@row1)-i.rn,
    N1 = (i.rn-@row1)*@gap+@low,
    N2 = (i.rn-(@row1-1))*@gap+@low
  FROM iTally AS i
  ORDER BY i.rn
) AS r
WHERE @high&@low&@gap&@row1 IS NOT NULL AND @high >= @low AND @gap > 0;
GO
© www.soinside.com 2019 - 2024. All rights reserved.