View difference between Paste ID: ujJ3cjtF and u9QPm6Km
SHOW: | | - or go back to the newest paste.
1
-- Slow update on large table with subquery
2
-- http://dba.stackexchange.com/questions/115984/slow-update-on-large-table-with-subquery
3
/****************************************************************************
4
* GENERATE FAKE DATA
5
****************************************************************************/
6
7
-- Generate 15MM names, with lengths ranging from 3 to 105 characters
8
-- The average length is 33 characters
9
CREATE TABLE dbo.SourceTable (
10
	id INT IDENTITY(1,1)
11
		CONSTRAINT PK_SourceTable PRIMARY KEY,
12
	name VARCHAR(128) NOT NULL,
13
	bad_count INT NULL,
14
	bad_count_new INT NULL,
15
)
16
GO
17
INSERT INTO dbo.SourceTable WITH (TABLOCK) (name)
18
SELECT TOP 15000000 v1.name + v2.name + v3.name
19
FROM master..spt_values v1
20
CROSS JOIN master..spt_values v2
21
CROSS JOIN master..spt_values v3
22
WHERE NULLIF(v1.name,'') IS NOT NULL
23
	AND NULLIF(v2.name,'') IS NOT NULL
24
	AND NULLIF(v3.name,'') IS NOT NULL
25
GO
26
27
-- Generate 3K bad phrases, with lengths ranging from 6 to 18 characters
28
-- The average length is 10 characters
29
CREATE TABLE dbo.Bad_Phrase (
30
  phrase VARCHAR(50) NOT NULL PRIMARY KEY
31
)
32
GO
33
INSERT INTO dbo.Bad_Phrase (phrase)
34
SELECT DISTINCT TOP 3000 p.phrase
35
FROM (
36
	SELECT SUBSTRING(v1.name + v2.name,
37
			CONVERT(INT, FLOOR(0.3*LEN(v1.name+v2.name))),
38
			CONVERT(INT, FLOOR(5+0.2*LEN(v1.name+v2.name)))
39
		) as phrase
40
	FROM master..spt_values v1
41
	CROSS JOIN master..spt_values v2
42
	WHERE v1.name IS NOT NULL
43
		AND v2.name IS NOT NULL
44
) p
45
WHERE LEN(p.phrase) >= 3
46
GO
47
48
-- Examine names and phrases
49
SELECT MIN(LEN(name)), MAX(LEN(name)), AVG(LEN(name)) FROM SourceTable
50
SELECT MIN(LEN(phrase)), MAX(LEN(phrase)), AVG(LEN(phrase)) FROM Bad_Phrase
51
GO
52
53
-- Generate a tally table
54
CREATE TABLE dbo.Numbers (
55
    n INT PRIMARY KEY
56
)
57
INSERT INTO dbo.Numbers (n)
58
SELECT TOP 1000 ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
59
FROM master..spt_values v1
60
GO
61
62
/****************************************************************************
63
* PROC TO PERFORM ORIGINAL UPDATE FOR A BATCH OF NAMES
64
****************************************************************************/
65
66
CREATE PROC #oldUpdate (@minId INT, @batchSize INT)
67
AS
68
BEGIN
69
	DECLARE @startTime DATETIME = GETDATE()
70
	DECLARE @maxId INT = @minId + @batchSize - 1
71
72
	UPDATE dbo.SourceTable
73
	SET Bad_Count = (
74
				   SELECT  COUNT(*) 
75
				   FROM Bad_Phrase 
76
				   WHERE SourceTable.Name like '%'+Bad_Phrase.phrase+'%'
77
				 )
78
	WHERE id BETWEEN @minId AND @maxId
79
80
	DECLARE @time INT = DATEDIFF(ms, @startTime, GETDATE())
81
	RAISERROR('Completed %i rows, starting at id %i, in %ims', 0, 1,
82
		@batchSize, @minId, @time) WITH NOWAIT
83
END
84
GO
85
86
/****************************************************************************
87
* PROC TO PERFORM THE NEW UPDATE FOR A BATCH OF NAMES
88
****************************************************************************/
89
90
CREATE PROC #newUpdate (@minId INT, @batchSize INT)
91
AS
92
BEGIN
93
	DECLARE @startTime DATETIME = GETDATE()
94
	DECLARE @maxId INT = @minId + @batchSize - 1
95
96
	-- For each name, generate the string at each offset
97
	DECLARE @maxBadPhraseLen INT = (SELECT MAX(LEN(phrase)) FROM Bad_Phrase)
98
	SELECT s.id, sub.sub_name
99
	INTO #SubNames
100
	FROM (SELECT * FROM SourceTable WHERE id BETWEEN @minId AND @maxId) s
101
	CROSS APPLY (
102
		-- Create a row for each substring of the name, starting at each character
103
		-- offset within that string.  For example, if the name is "abcd", this CROSS APPLY
104
		-- will generate 4 rows, with values ("abcd"), ("bcd"), ("cd"), and ("d"). In order
105
		-- for the name to be LIKE the bad phrase, the bad phrase must match the leading X
106
		-- characters (where X is the length of the bad phrase) of at least one of these
107
		-- substrings. This can be efficiently computed after indexing the substrings.
108
		-- As an optimization, we only store @maxBadPhraseLen characters rather than
109
		-- storing the full remainder of the name from each offset; all other characters are
110
		-- simply extra space that isn't needed to determine whether a bad phrase matches.
111
		SELECT TOP(LEN(s.name)) SUBSTRING(s.name, n.n, @maxBadPhraseLen) AS sub_name 
112
		FROM Numbers n
113
		ORDER BY n.n
114
	) sub
115
	-- Create an index so that bad phrases can be quickly compared for a match
116
	CREATE CLUSTERED INDEX IX_SubNames ON #SubNames (sub_name)
117
118
	-- For each name, compute the number of distinct bad phrases that match
119
	-- By "match", we mean that the a substring starting from one or more 
120
	-- character offsets of the overall name matches the bad phrase
121
	SELECT s.id, COUNT(DISTINCT b.phrase) AS bad_count
122
	INTO #tempBadCounts
123
	FROM dbo.Bad_Phrase b
124
	JOIN #SubNames s
125
		ON s.sub_name LIKE b.phrase + '%'
126
	GROUP BY s.id
127
128
	-- Perform the actual update into a "bad_count_new" field
129
	-- For validation, we'll compare bad_count_new with the originally computed bad_count
130
	UPDATE s
131
	SET s.bad_count_new = COALESCE(b.bad_count, 0)
132
	FROM dbo.SourceTable s
133
	LEFT JOIN #tempBadCounts b
134
		ON b.id = s.id
135
	WHERE s.id BETWEEN @minId AND @maxId
136
137
	DECLARE @time INT = DATEDIFF(ms, @startTime, GETDATE())
138
	RAISERROR('Completed %i rows, starting at id %i, in %ims', 0, 1,
139
		@batchSize, @minId, @time) WITH NOWAIT
140
END
141
GO
142
143
/****************************************************************************
144
* PERFORMANCE TESTING
145
****************************************************************************/
146
147
SET STATISTICS TIME, IO OFF
148
GO
149
150
-- Perform 10% of the overall update, broken into batches of 1% at a time
151
-- and measuring the run-time perform (in seconds) of each batch:
152
-- 1:  204s old, 5.7s new
153
-- 2:  166s old, 4.8s new
154
-- 3:  161s old, 5.5s new
155
-- 4:  169s old, 4.9s new
156
-- 5:  168s old, 5.2s new
157
-- 6:  161s old, 6.0s new
158
-- 7:  135s old, 6.9s new
159
-- 8:  126s old, 6.5s new
160
-- 9:  127s old, 5.3s new
161
-- 10: 129s old, 5.2s new
162
-- Total: 1546s old, 56s new
163
DECLARE @minId INT = 1, @batchSize INT = 150000, @batchNum INT = 1
164
WHILE (@batchNum <= 10)
165
BEGIN
166
	EXEC #oldUpdate @minId = @minId, @batchSize = @batchSize
167
	EXEC #newUpdate @minId = @minId, @batchSize = @batchSize
168
	SET @minId = @minId + @batchSize
169
	SET @batchNum = @batchNum + 1
170
END
171
GO
172
173
/****************************************************************************
174
* CORRECTNESS TESTING
175
****************************************************************************/
176
177
SELECT COUNT(*) numNames, 
178
	COUNT(CASE WHEN s.bad_count = s.bad_count_new THEN 1 END) numMatching,
179
	COUNT(CASE WHEN bad_count > 0 THEN 1 END) numWithBadCount
180
FROM dbo.SourceTable s
181
WHERE s.id BETWEEN 1 AND 1500000
182
--numNames	numMatching	numWithBadCount
183
--1500000	1500000		900616
184
GO
185
186
/****************************************************************************
187
* CLEANUP
188
****************************************************************************/
189
190
DROP TABLE dbo.SourceTable
191
GO
192
DROP TABLE dbo.Bad_Phrase
193
GO
194
DROP TABLE dbo.Numbers
195
GO