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 |