kshah29

Slow update on large table with subquery

Sep 25th, 2015
267
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  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
Advertisement
Add Comment
Please, Sign In to add comment