首先,請接受我最誠摯的歉意,不要回到此帖。我作爲序言發表了一些意見,並且完全打算稍後發佈一個有用的答案,而不僅僅是「聖人的建議」,然後發生了真實的事情,我完全失去了這篇文章的軌道。
讓我們首先通過構建他所說的他正在使用的表並重新構建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行。
的原因是因爲該帳簿表的「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千萬行不同。
以下是計算天堂小片上的統計數據。
(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
您使用的是什麼數據庫引擎?和哪個版本? – Lamak 2011-01-06 19:43:00
SQL Server 2008. – Ethan 2011-01-06 20:49:12