2011-01-06 42 views
3

我有一個事件表,其中有一個字段,指定事件發生的頻率(以天爲單位)。我想在給定的日期範圍內選擇事件的所有事件,包括計算出的事件(例如,如果第一個事件日期是2011年1月6日,並且每7天發生一次,您會在1月13日和1月20日看到結果)。使用計數表選擇重複日期的查詢

這裏是我的事件表是什麼樣子:

 

event_ID INT, 
event_title NVARCHAR(50), 
first_event_date DATETIME, 
occurs_every INT 
 

閱讀this article後,它似乎是最有效的方式來處理,這是一個符合表,但我一直沒能包住我的頭圍繞如何返回我正在尋找的結果。

比方說,我有數據,看起來像這樣:

 
event_ID | event_title | first_event_date | occurs_every 
1  | Event 1  | 1/6/2011  |  7 
2  | Event 2  | 1/8/2011  |  3 

我正在尋找將結果:

 
event_ID | event_title | event_date | 
1  | Event 1  | 1/6/2011 | 
2  | Event 2  | 1/8/2011 | 
1  | Event 1  | 1/13/2011 | 
2  | Event 2  | 1/12/2011 | 
2  | Event 2  | 1/16/2011 | 
1  | Event 1  | 1/20/2011 | 
(etc) 

有什麼建議?編輯:我使用的是SQL Server 2008的

附加信息:

我有一個工作查詢,但它似乎很缺憾,我關心的性能,一旦我得到更多的數據放入桌子。

首先,以供參考,這是帳簿桌:

 

SELECT TOP 11000 
     IDENTITY(INT,1,1) AS N 
    INTO dbo.Tally 
    FROM Master.dbo.SysColumns sc1, 
     Master.dbo.SysColumns sc2 

    ALTER TABLE dbo.Tally 
    ADD CONSTRAINT PK_Tally_N 
     PRIMARY KEY CLUSTERED (N) WITH FILLFACTOR = 100 
 

現在,這裏的缺憾選擇查詢:

 

SELECT event_ID, 
     event_title, 
     first_event_date, 
     DATEADD(dd, occurs_every * (t.N - 1), [first_event_date]) AS occurrence 
FROM dbo.Events 
     CROSS JOIN dbo.Tally t 
WHERE DATEADD(dd, occurs_every * (t.N - 1), [first_event_date]) 

現在,這個工作 - 但是當我加1000行樣本數據到了真正陷入困境的表格。我認爲這是我的交叉連接。另外,上面的代碼奇怪地沒有顯示我的選擇查詢的最後一行,即「ORDER BY occurrence」。

+1

您使用的是什麼數據庫引擎?和哪個版本? – Lamak 2011-01-06 19:43:00

+0

SQL Server 2008. – Ethan 2011-01-06 20:49:12

回答

4

首先,請接受我最誠摯的歉意,不要回到此帖。我作爲序言發表了一些意見,並且完全打算稍後發佈一個有用的答案,而不僅僅是「聖人的建議」,然後發生了真實的事情,我完全失去了這篇文章的軌道。

讓我們首先通過構建他所說的他正在使用的表並重新構建OP的帖子,並像他說的那樣填充了1000個事件。我將通過使用高性能的「僞遊標」來提供2015年和2016年的隨機開始日期,以提供我們需要的「存在行」,而不是使用While Loop或rCTE的RBAR(遞歸CTE )。

作爲一個側欄,我保持2005年所有的兼容性,因爲仍然有很多人使用2005年,並且在使用2008+技術方面沒有性能上的提升。

下面是構建測試表的代碼。詳情在評論中。

--==================================================================== 
--  Presets 
--==================================================================== 
--===== Declare and prepopulate some obviously named variables 
DECLARE @StartDate  DATETIME 
     ,@EndDate  DATETIME 
     ,@Days   INT 
     ,@Events  INT 
     ,@MaxEventGap INT 
; 
SELECT @StartDate  = '2015-01-01' --Inclusive date 
     ,@EndDate  = '2017-01-01' --Exclusive date 
     ,@Days   = DATEDIFF(dd,@StartDate,@EndDate) 
     ,@Events  = 1000 
     ,@MaxEventGap = 30 --Note that 1 day will be the next day 
; 
--==================================================================== 
--  Create the Test Table 
--==================================================================== 
--===== If the test table already exists, drop it to make reruns of 
    -- this demo easier. I also use a Temp Table so that we don't 
    -- accidenttly screw up a real table. 
    IF OBJECT_ID('tempdb..#Events','U') IS NOT NULL 
     DROP TABLE #Events 
; 
--===== Build the test table. 
    -- I'm following what the OP did so that anyone with a case 
    -- sensitive server won't have a problem. 
CREATE TABLE #Events 
     (
     event_ID   INT, 
     event_title   NVARCHAR(50), 
     first_event_date DATETIME, 
     occurs_every  INT 
     ) 
; 
--==================================================================== 
--  Populate the Test Table 
--==================================================================== 
--===== Build @Events number of events using the previously defined 
    -- start date and number of days as limits for the random dates. 
    -- To make life a little easier, I'm using a CTE with a 
    -- "pseudo-cursor" to form most of the data and then an 
    -- external INSERT so that I can name the event after the 
    -- event_ID. 
    WITH cteGenData AS 
     (
     SELECT TOP (@Events) 
       event_ID   = ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) 
       ,first_event_date = DATEADD(dd, ABS(CHECKSUM(NEWID())) % @Days, @StartDate) 
       ,occurs_every  = ABS(CHECKSUM(NEWID())) % 30 + 1 
      FROM  sys.all_columns ac1 --Has at least 4000 rows in it for most editions 
      CROSS JOIN sys.all_columns ac2 --Just in case it doesn't for Express ;-) 
     ) 
INSERT INTO #Events 
     (event_ID, event_title, first_event_date, occurs_every) 
SELECT event_ID 
     ,event_title = 'Event #' + CAST(event_id AS VARCHAR(10)) 
     ,first_event_date 
     ,occurs_every 
    FROM cteGenData 
; 
--===== Let's see the first 10 rows 
SELECT TOP 10 * 
    FROM #Events 
    ORDER BY event_ID 
; 

這裏是前10行會是什麼樣的理解是對first_even_datet和occurs_every值會有很大的不同,因爲方法我用來產生約束的隨機數據。

event_ID event_title first_event_date  occurs_every 
-------- ----------- ----------------------- ------------ 
1  Event #1 2016-10-12 00:00:00.000 10 
2  Event #2 2015-04-25 00:00:00.000 28 
3  Event #3 2015-11-08 00:00:00.000 4 
4  Event #4 2016-02-16 00:00:00.000 25 
5  Event #5 2016-06-11 00:00:00.000 15 
6  Event #6 2016-04-29 00:00:00.000 14 
7  Event #7 2016-04-16 00:00:00.000 9 
8  Event #8 2015-03-29 00:00:00.000 2 
9  Event #9 2016-02-14 00:00:00.000 29 
10  Event #10 2016-01-23 00:00:00.000 8 

可以肯定的是,您將需要一個Tally表來複制OPs實驗。這是代碼。如果您已經擁有一個,請確保它具有所需的唯一聚集索引(通常以PK的形式),這是出於性能原因。我將代碼的「僞遊標」部分中的行源表現代化爲不使用棄用的「syscolumns」視圖。

--===== Create a Tally Table with enough sequential numbers 
    -- for more than 30 years worth of dates. 
SELECT TOP 11000 
     IDENTITY(INT,1,1) AS N 
    INTO dbo.Tally 
    FROM  sys.all_columns sc1 
    CROSS JOIN sys.all_columns sc2 
; 
--===== Add the quintessential Unique Clustered Index as the PK. 
    ALTER TABLE dbo.Tally 
    ADD CONSTRAINT PK_Tally_N 
     PRIMARY KEY CLUSTERED (N) WITH FILLFACTOR = 100 
; 

我們準備好搖滾。 OP的代碼的一部分被論壇吞噬了,但我能夠通過編輯他原來的帖子來恢復它。它實際上看起來像這樣,只是我改變了「結束日期」以匹配我剛剛生成的數據(這是我做出的唯一更改)。由於該代碼不包含標量或多語句UDF,因此我還打開了統計數據以解釋發生了什麼。

下面是OP的代碼和所提到的改變。

SET STATISTICS TIME,IO ON 
; 
SELECT event_id, 
     event_title, 
     first_event_date, 
     DATEADD(dd, occurs_every * (t.N - 1), [first_event_date]) AS Occurrence 
    FROM #Events 
    CROSS JOIN dbo.Tally t 
    WHERE t.N <= DATEDIFF(dd,first_event_date,'2017-03-01')/occurs_every + 1 
    ORDER BY Occurrence 
; 
SET STATISTICS TIME,IO OFF 
; 

以下是運行OP代碼的統計數據。對不起,所有的滾動,但他們是很長的路線。

(61766 row(s) affected) 
Table 'Worktable'. Scan count 4, logical reads 118440, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0. 
Table 'Tally'. Scan count 4, logical reads 80, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0. 
Table '#Events_____________________________________________________________________________________________________________00000000001F'. Scan count 5, logical reads 7, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0. 
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0. 

SQL Server Execution Times: 
    CPU time = 4196 ms, elapsed time = 1751 ms. 

顯然,該性能正在吸吮聲音,即使是While Loop或rCTE也能擊敗。問題是什麼?

如果您在下面的執行計劃中查看突出顯示的箭頭,則會發現它包含11萬個實際行,因爲非SARGable(SARG =「搜索ARGument」,非SARGable意味着它不能使用索引正確)導致11,000行Tally表與1000行#Events表之間的完整CROSS JOIN的條件。那些是ACTUAL行,而不是ESTIMATED行。

Non-SARGable Query scanned Tally Table 1,000 times for total of 11 million rows

的原因是因爲該帳簿表的「N」列是一個公式中使用和整個帳簿表必須被掃描作爲在#Events表中每一行的結果。這是一個常見的錯誤,讓人們理解Tally Tables生成的代碼很慢。

那麼,我們該如何解決它?我們不是使用t.N來計算每一行的日期,而是使用日期的差異和除以天數來計算將t.N等同於並看看會發生什麼所需的事件數量。請注意,我在下面的代碼中改變的唯一一件事是在WHERE子句中對t進行查找的條件。N SARGable(能夠使用索引來啓動和停止搜索,然後進行範圍掃描)。

SET STATISTICS TIME,IO ON 
; 
SELECT event_id, 
     event_title, 
     first_event_date, 
     DATEADD(dd, occurs_every * (t.N - 1), [first_event_date]) AS Occurrence 
    FROM #Events 
    CROSS JOIN dbo.Tally t 
    WHERE t.N <= DATEDIFF(dd,first_event_date,'2017-03-01')/occurs_every + 1 
    ORDER BY Occurrence 
; 
SET STATISTICS TIME,IO OFF 
; 

下面是新的執行計劃的樣子。 61,766行的實際行(全部在緩存中)與11千萬行不同。

enter image description here 以下是計算天堂小片上的統計數據。

(61766 row(s) affected) 
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0. 
Table '#Events_____________________________________________________________________________________________________________00000000001F'. Scan count 5, logical reads 7, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0. 
Table 'Tally'. Scan count 1000, logical reads 3011, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0. 

SQL Server Execution Times: 
    CPU time = 78 ms, elapsed time = 528 ms. 
  • CPU時間52.79倍或5279%下降。
  • 經過時間減少了2.32倍,即232%。
  • 總讀取由38.27倍或3827%改變代碼

總量... 1線WHERE子句的降低。

我們可以通過使用Itzik Ben-Gan的內聯級聯CTE(不是rCTE)將閱讀總數降低到7。

底線是,雖然使用Tally表幾乎是一個靈丹妙藥的表現,但您必須正確使用它,就像其他任何東西一樣。您必須使用「最佳實踐」,例如編寫一個SARGable WHERE子句,以正確地向我們提供索引,就像其他任何東西一樣。

再一次,我最誠摯的道歉,特別是OP,因爲這麼晚了。我希望它能幫助未來的人。我也很抱歉沒有時間在這個線程上重寫rCTE示例來顯示它有多糟糕。如果你對rCTE爲什麼如此糟糕以及你不介意SQLServerCentral.com成員感興趣,那麼這裏有一篇關於這個主題的文章。我會在這裏發佈所有內容,但這樣做太長了。

Hidden RBAR: Counting with Recursive CTE's

1

以下是使用Oracle的一種方法(您可以通過修改生成連續數字的子查詢將其切換爲其他引擎,請參見下文)。這個查詢背後的想法是生成一個連續的乘法器列表(例如0,1,2,3 ...,n),直到窗口大小(日期之間的天數)爲止。這是子查詢返回的內容。我們使用它與事件表交叉連接,然後將結果限制到所需的日期範圍。

SELECT t.event_id, t.event_title, t.event_date + t.occurs_every*x.r event_date 
FROM tally_table t CROSS JOIN (
SELECT rownum-1 r FROM DUAL 
     connect by level <= (date '2011-1-20' - date '2011-1-6') + 1 
) x 
WHERE t.event_date + t.occurs_every*x.r <= date '2011-1-20' 
ORDER BY t.event_date + t.occurs_every*x.r, t.event_id; 

查詢中的tally_table是您在問題中指定的表。

9

在SQL Server 2008中,您可以使用遞歸CTE。

DECLARE @StartDate DATE, @EndDate DATE 
SET @StartDate = '20110106' 
SET @EndDate = '20110228'; 


WITH DateTable AS 
(
    SELECT Event_id, event_title, event_date, occurs_every 
    FROM tally_table 
    UNION ALL 
    SELECT event_ID, event_title, DATEADD(DAY,occurs_every,event_date), occurs_every 
    FROM DateTable 
    WHERE DATEADD(DAY,occurs_every,event_date) BETWEEN @StartDate AND @EndDate 
) 
SELECT Event_id, event_title, event_date 
FROM DateTable 
WHERE event_date BETWEEN @StartDate AND @EndDate 
ORDER BY event_date 

您必須記住按日期範圍過濾,因此它不會陷入無限循環。或者使用MAXRECURSION提示來限制結果(默認情況下此值爲100)