T-Sql

如何將游標轉換為 CTE?T-SQL

  • July 10, 2020

為了澄清,我添加了更多資訊並刪除了“簡化”程式碼。

我有帶有日期時間打卡資訊的表格。

CREATE TABLE [dbo].[AT_Punches](
   [PunchID] [int] IDENTITY(1,1) NOT NULL,
   [Facility] [char](3) NULL,
   [person_code] [varchar](12) NULL,
   [TimeStamp] [datetime] NULL,
   [AltJob] [varchar](12) NULL,
   [INOUT] [char](3) NULL,
   [edit_user] [varchar](20) NULL,
   [edit_datetime] [datetime] NULL,
   [create_datetime] [datetime] NULL,
CONSTRAINT [PK__AT_Punch] PRIMARY KEY CLUSTERED 
(
   [PunchID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, FILLFACTOR = 100) ON [PRIMARY]
) ON [PRIMARY]
GO
-- TEST DATA
INSERT INTO [dbo].[AT_Punches] ([Facility],[person_code],[TimeStamp],[AltJob],[INOUT],[edit_user],[edit_datetime],[create_datetime])
                       VALUES ('ABC','JDOE','2020-07-06 15:03:48.960','RN','In','tester',GETDATE(),GETDATE())
INSERT INTO [dbo].[AT_Punches] ([Facility],[person_code],[TimeStamp],[AltJob],[INOUT],[edit_user],[edit_datetime],[create_datetime])
                       VALUES ('ABC','JDOE','2020-07-07 03:43:38.000','RN','Out','tester',GETDATE(),GETDATE())
INSERT INTO [dbo].[AT_Punches] ([Facility],[person_code],[TimeStamp],[AltJob],[INOUT],[edit_user],[edit_datetime],[create_datetime])
                       VALUES ('XYZ','KLO','2020-07-07 06:18:08.000','RN','In','tester',GETDATE(),GETDATE())
INSERT INTO [dbo].[AT_Punches] ([Facility],[person_code],[TimeStamp],[AltJob],[INOUT],[edit_user],[edit_datetime],[create_datetime])
                       VALUES ('XYZ','JSMITH','2020-07-07 15:01:40.000','RN','In','tester',GETDATE(),GETDATE())
INSERT INTO [dbo].[AT_Punches] ([Facility],[person_code],[TimeStamp],[AltJob],[INOUT],[edit_user],[edit_datetime],[create_datetime])
                       VALUES ('XYZ','JSMITH','2020-07-08 03:43:38.000','RN','Out','tester',GETDATE(),GETDATE())
INSERT INTO [dbo].[AT_Punches] ([Facility],[person_code],[TimeStamp],[AltJob],[INOUT],[edit_user],[edit_datetime],[create_datetime])
                       VALUES ('XYZ','ABARR','2020-07-08 04:12:33.000','RN','In','tester',GETDATE(),GETDATE())
INSERT INTO [dbo].[AT_Punches] ([Facility],[person_code],[TimeStamp],[AltJob],[INOUT],[edit_user],[edit_datetime],[create_datetime])
                       VALUES ('XYZ','ABARR','2020-07-08 07:19:22.000','RN','Out','tester',GETDATE(),GETDATE())

我需要將這些記錄變成一份報告,將拳頭配對,同時記下“失去的”拳頭。它看起來像這樣。

Facility Person              Vendor Job In               Out              Hours 
ABC      Jane Doe (JDOE)     12345  RN  7/6/2020 3:00 PM 7/7/2020 3:45 AM 12.75 
XYZ      Kim Lo (KLO)        34567  RN  7/7/2020 6:15 AM *** MISSING ***  0.00 
XYZ      Jon Smith (JSMITH)  34567  RN  7/7/2020 3:00 PM 7/8/2020 3:45 AM 12.75 
XYZ      Angela Barr (ABARR) 34567  RN  7/8/2020 4:15 AM 7/8/2020 7:15 AM 3.00 

我目前正在使用 sp 中的游標來解決這個問題,但執行時間太長。處理 700 條記錄大約需要 45 秒。我覺得完成得更快。這是那個sp。

CREATE PROCEDURE [dbo].[sp_Missing_Punch_Formatter]
@StartDate date, @EndDate date, @faccode varchar(4), @userID varchar(20)
WITH EXEC AS CALLER
AS
DECLARE @boolIN   BIT

DECLARE @PunchesCursor AS CURSOR

DECLARE @strLastStamp   DATETIME
DECLARE @strLastOut   VARCHAR (25)
DECLARE @strLastPerson   VARCHAR (12)

DECLARE @strLastRecNo   INT
DECLARE @counter   INT

DECLARE @facility   CHAR (3)
DECLARE @vendorid   VARCHAR (8)
DECLARE @person_code   VARCHAR (12)
DECLARE @altjob   VARCHAR (12)
DECLARE @Inout   CHAR (3)
DECLARE @timestamp   DATETIME
DECLARE @InPunch   VARCHAR (25)
DECLARE @OutPunch   VARCHAR (25)
DECLARE @is_round  char(1)

BEGIN
  /********************* delete temp work table data *********************/

  DELETE FROM atrax.dbo.at_punchwork
   WHERE userid = @userid AND faccode = @faccode;

  /********************* reformat and calc punches *********************/
  SET @PunchesCursor =
         CURSOR FOR
            SELECT facility,
                   vendorid,
                   person_code,
                   altjob,
                   Inout,
                   timestamp,
                   InPunch,
                   OutPunch, is_round
              FROM (SELECT p.facility,
                           a.vendorid,
                           p.person_code,
                           p.altjob,
                           p.timestamp,
                           p.INOUT,
                           isnull (format (p.timestamp, 'g'), '****')
                              AS InPunch,
                           '****' AS OutPunch, 'Y' as is_round
                      FROM atrax.dbo.AT_Punches p
                           JOIN atrax.dbo.AT_person e
                              ON     p.Facility = e.Facility
                                 AND p.person_code = e.Person_code
                           JOIN atrax.dbo.AT_agency a
                              ON e.VendorID = a.VendorID
                     WHERE p.INOUT = 'In'
                    UNION ALL
                    SELECT p.facility,
                           a.vendorid,
                           p.person_code,
                           p.altjob,
                           p.timestamp,
                           p.INOUT,
                           '****' AS InPunch,
                           isnull (format (timestamp, 'g'), '****')
                              AS OutPunch, 'Y' as is_round
                      FROM atrax.dbo.AT_Punches p
                           JOIN atrax.dbo.AT_person e
                              ON     p.Facility = e.Facility
                                 AND p.person_code = e.Person_code
                           JOIN atrax.dbo.AT_agency a
                              ON e.VendorID = a.VendorID
                     WHERE p.INOUT = 'Out') AS t
             WHERE     timestamp >= @startdate
                   AND timestamp <= dateadd(DD,1,@enddate)
                   AND facility = @faccode
            ORDER BY facility,
                     VendorID,
                     person_code,
                     timestamp;

  SET @strLastPerson = '';
  SET @strLastStamp = '';
  SET @strLastOut = '';

  SET @boolIN = 0;                                              

  SET @strLastRecNo = 0;
  SET @counter = 0;
  
-- Open PUNCHES for Processing 
  
  OPEN @PunchesCursor
  FETCH NEXT FROM @PunchesCursor
       INTO @facility,
            @vendorid,
            @person_code,
            @altjob,
            @Inout,
            @timestamp,
            @InPunch,
            @OutPunch,
            @is_round

  WHILE @@FETCH_STATUS = 0
  BEGIN
     SET @counter += 1;                                 -- Record # = counter

     -- start new record for new person if not first record in
     IF @person_code <> @strLastPerson AND @counter <> 1
        BEGIN
           -- if record already pending finish last record
           IF @boolIN = 1
              BEGIN
                 -- if record has been started then finish with it, save out data from in punch (missing)
                 BEGIN TRANSACTION;

                 UPDATE atrax.dbo.at_punchwork
                    SET timestampout = @strLastStamp, OutPunch = @strLastOut
                  WHERE     recno = @strLastRecNo
                        AND userid = @userid
                        AND faccode = @faccode;

                 COMMIT;
                 -- Reset Vars
                 SET @boolIN = 0                                      --False

                 SET @strLastStamp = ''
                 SET @strLastOut = ''
              END
        END                                                 -- end new person

     IF @Inout = 'In'
        BEGIN
           -- Process the in record
           IF @boolIN = 1
              BEGIN
                 -- if record has been started then finish with it, save out data from in punch (missing)
                 BEGIN TRANSACTION;

                 UPDATE atrax.dbo.at_punchwork
                    SET timestampout = @strLastStamp, OutPunch = @strLastOut
                  WHERE     recno = @strLastRecNo
                        AND userid = @userid
                        AND faccode = @faccode;

                 COMMIT;

                 SET @boolIN = 0                                      --False

                 SET @strLastStamp = ''
                 SET @strLastOut = ''
              END                                                   -- boolin

           -- Start a new IN for current record
           SET @boolIN = 1                                             --true

           INSERT INTO atrax.dbo.at_punchwork (recno,
                                               faccode,
                                               vendorid,
                                               person_code,
                                               WorkedJob,
                                               timestamp,
                                               Inout,
                                               InPunch,
                                               userid,
                                               is_round)
           VALUES (@Counter,
                   @faccode,
                   @vendorid,
                   @person_code,
                   @Altjob,
                   @timestamp,
                   @Inout,
                   @inpunch,
                   @userid,
                   @is_round);


           -- save out data from this record
           SET @strLastStamp = @timestamp
           SET @strLastOut = @outpunch
        END
     ELSE                                                      -- INout = Out
        BEGIN
           -- process out record

           IF @boolIN = 1
              BEGIN
                 -- Record all ready started -- Add Out part from curr record
                 UPDATE atrax.dbo.at_punchwork
                    SET timestampout = @timestamp, OutPunch = @outpunch
                  WHERE     recno = @strLastRecNo
                        AND userid = @userid
                        AND faccode = @faccode;

                 SET @strLastStamp = ''
                 SET @strLastOut = ''

                 SET @boolIN = 0
              END
           ELSE
              BEGIN
                 -- add whole out Record (no in found).
                 INSERT INTO atrax.dbo.at_punchwork (recno,
                                                     faccode,
                                                     vendorid,
                                                     person_code,
                                                     WorkedJob,
                                                     timestamp,
                                                     Inout,
                                                     InPunch,
                                                     timestampout,
                                                     OutPunch,
                                                     userid, 
                                                     is_round)
                 VALUES (@counter,
                         @faccode,
                         @vendorid,
                         @person_code,
                         @Altjob,
                         @timestamp,
                         @inout,
                         @inpunch,
                         @timestamp,
                         @outpunch,
                         @userid,
                         @is_round);

                 SET @strLastStamp = ''
                 SET @strLastOut = ''

                 SET @boolIN = 0
              END                                                  -- bool IN
        END                                                          -- INOUT

     SET @strLastRecNo = @counter;
     SET @strLastPerson = @person_code;

     FETCH NEXT FROM @PunchesCursor
          INTO @facility,
               @vendorid,
               @person_code,
               @altjob,
               @Inout,
               @timestamp,
               @InPunch,
               @OutPunch,
               @is_round
  END                                                              --end loop
END
GO

有沒有更快的方法來做這種事情,也許沒有游標?

修改後的答案,給出了問題的澄清

問題基本上是,對於 person_code 和設施的給定組合,當您對記錄進行排序時

$$ timestamp $$順序,您要確保顯示 inout 值為 ‘in’ 的每一行後跟 ‘out’,反之亦然 - 並且最後一行顯示 ‘out’。 這是使用的解決方案lead- 這是一個 T-SQL 函式,用於從比目前行更靠後的行中讀取數據值。我們基本上用它來檢查:下一行是同一個人-設施組合嗎?如果是這樣,那麼下一行的 INOUT 值是否與該行不同?如果是這樣,那就太好了。如果不是,那就有問題了。但是,如果下一行是針對某些不同的人員-設施組合,那麼在目前行上,INOUT 值是否“out”?如果沒有,那麼他們還沒有打卡。

select 
   *,
case
   when lead(Facility,1,'') over (order by person_code, Facility, [TimeStamp]) = Facility
       and lead(person_code,1,'') over (order by person_code, Facility, [TimeStamp]) = person_code
       and lead(INOUT,1,'') over (order by person_code, Facility, [TimeStamp]) = INOUT
       then 'boohoo - next row is the same'
   when lead(Facility,1,'') over (order by person_code, Facility, [TimeStamp]) = Facility
       and lead(person_code,1,'') over (order by person_code, Facility, [TimeStamp]) = person_code
       and lead(INOUT,1,'') over (order by person_code, Facility, [TimeStamp]) <> INOUT
       then 'hooray - next row is different'
   when 
       (
           lead(Facility,1,'') over (order by person_code, Facility, [TimeStamp]) <> Facility
           or lead(person_code,1,'') over (order by person_code, Facility, [TimeStamp]) <> person_code
       )
       and INOUT = 'In'
       then 'boohoo - Not clocked out yet...'
    else 'hooray - last row for this person-location - and they clocked out :)' end
from at_punches 
order by person_code, Facility, [TimeStamp];

初步答案

(這個答案是當我認為他們只需要顯示每個人的下班次數不匹配的任何行時。我會在此處留下答案,儘管這很簡單)。

試試下面的。

select * from tblPunches
where
   (select count(1) from tblPunches p2 where p2.userID = tblPunches.userID and p2.punchType = 'IN')
<> (select count(1) from tblPunches p3 where p3.userID = tblPunches.userID and p2.punchType = 'OUT')

在我看來 - 如果給定使用者的“IN”和“OUT”拳數不相等,那就有問題了,不是嗎?

以上不包含確保“OUT”打孔與特定“IN”打孔相關的邏輯 - 例如,在同一天,在 24 小時內或其他任何時間 - 但您的問題似乎也沒有暗示該要求。

PS。在我寫這個答案時,您發布了一條澄清評論,您希望根據時間戳遍歷行。如果該使用者在 IN 和 OUT 出拳之間有任何不匹配,則上述內容將返回給定使用者的所有行。

如果您只想返回IN 連續出現兩次或 OUT 連續出現兩次(按時間戳排序時)的行,請參閱我最近對類似問題的回答。我真的在五分鐘前發布了這個,這種方法只會為你拉出有問題的單行。

引用自:https://dba.stackexchange.com/questions/270699