Skip to content

Commit b30766c

Browse files
Merge pull request #728 from erikdarlingdata/fix/indexcleanup-tests-and-preserve-unique
sp_IndexCleanup: preserve UNIQUE in merge scripts + adversarial tests
2 parents d426372 + b088752 commit b30766c

2 files changed

Lines changed: 530 additions & 0 deletions

File tree

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
/*
2+
sp_IndexCleanup Adversarial Test Suite — Setup & Execute
3+
========================================================
4+
This script:
5+
1. Creates test tables with data and edge-case index configurations
6+
2. Generates usage stats
7+
3. Runs sp_IndexCleanup @dedupe_only = 1
8+
9+
Output is captured by the test runner (run_tests.py) for validation.
10+
Run with: python run_tests.py
11+
12+
Direct execution (visual inspection only):
13+
sqlcmd -S SQL2022 -U sa -P "password" -d StackOverflow2013 -i adversarial_test.sql
14+
*/
15+
SET NOCOUNT ON;
16+
17+
USE StackOverflow2013;
18+
GO
19+
20+
/* ============================================= */
21+
/* Cleanup previous test artifacts */
22+
/* ============================================= */
23+
IF OBJECT_ID('dbo.test_ic_view') IS NOT NULL
24+
BEGIN
25+
IF EXISTS (SELECT 1/0 FROM sys.indexes WHERE object_id = OBJECT_ID('dbo.test_ic_view') AND name = N'cx_test_ic_view')
26+
DROP INDEX cx_test_ic_view ON dbo.test_ic_view;
27+
END;
28+
GO
29+
IF OBJECT_ID('dbo.test_ic_view') IS NOT NULL DROP VIEW dbo.test_ic_view;
30+
GO
31+
DROP TABLE IF EXISTS dbo.test_ic_basic;
32+
DROP TABLE IF EXISTS dbo.test_ic_uc;
33+
DROP TABLE IF EXISTS dbo.test_ic_filtered;
34+
DROP TABLE IF EXISTS dbo.test_ic_heap;
35+
DROP TABLE IF EXISTS dbo.test_ic_multi;
36+
DROP TABLE IF EXISTS dbo.test_ic_view_base;
37+
GO
38+
39+
/* ============================================= */
40+
/* Create test tables with data */
41+
/* ============================================= */
42+
43+
CREATE TABLE dbo.test_ic_basic
44+
(
45+
id bigint IDENTITY(1,1) NOT NULL PRIMARY KEY CLUSTERED,
46+
col_a integer NOT NULL,
47+
col_b integer NOT NULL,
48+
col_c integer NOT NULL,
49+
col_d integer NOT NULL,
50+
col_e nvarchar(100) NULL,
51+
col_f datetime NOT NULL DEFAULT GETDATE()
52+
);
53+
54+
CREATE TABLE dbo.test_ic_uc
55+
(
56+
id bigint IDENTITY(1,1) NOT NULL PRIMARY KEY CLUSTERED,
57+
col_a integer NOT NULL,
58+
col_b integer NOT NULL,
59+
col_c integer NOT NULL,
60+
col_d integer NOT NULL,
61+
col_e nvarchar(100) NULL
62+
);
63+
64+
CREATE TABLE dbo.test_ic_filtered
65+
(
66+
id bigint IDENTITY(1,1) NOT NULL PRIMARY KEY CLUSTERED,
67+
col_a integer NOT NULL,
68+
col_b integer NOT NULL,
69+
status_code integer NOT NULL
70+
);
71+
72+
CREATE TABLE dbo.test_ic_heap
73+
(
74+
col_a integer NOT NULL,
75+
col_b integer NOT NULL,
76+
col_c integer NOT NULL
77+
);
78+
79+
CREATE TABLE dbo.test_ic_multi
80+
(
81+
id bigint IDENTITY(1,1) NOT NULL PRIMARY KEY CLUSTERED,
82+
col_a integer NOT NULL,
83+
col_b integer NOT NULL
84+
);
85+
86+
CREATE TABLE dbo.test_ic_view_base
87+
(
88+
id bigint IDENTITY(1,1) NOT NULL PRIMARY KEY CLUSTERED,
89+
col_a integer NOT NULL,
90+
col_b integer NOT NULL,
91+
col_c integer NOT NULL
92+
);
93+
GO
94+
95+
CREATE VIEW dbo.test_ic_view WITH SCHEMABINDING
96+
AS
97+
SELECT
98+
col_a = tvb.col_a,
99+
col_b = tvb.col_b,
100+
row_count = COUNT_BIG(*)
101+
FROM dbo.test_ic_view_base AS tvb
102+
GROUP BY tvb.col_a, tvb.col_b;
103+
GO
104+
105+
/* Populate with 10K+ rows */
106+
INSERT INTO dbo.test_ic_basic (col_a, col_b, col_c, col_d, col_e)
107+
SELECT TOP (10000) ABS(CHECKSUM(NEWID())) % 1000, ABS(CHECKSUM(NEWID())) % 500,
108+
ABS(CHECKSUM(NEWID())) % 200, ABS(CHECKSUM(NEWID())) % 100, LEFT(NEWID(), 20)
109+
FROM sys.all_objects AS a CROSS JOIN sys.all_objects AS b;
110+
111+
INSERT INTO dbo.test_ic_uc (col_a, col_b, col_c, col_d, col_e)
112+
SELECT TOP (10000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)),
113+
ABS(CHECKSUM(NEWID())) % 500, ABS(CHECKSUM(NEWID())) % 200,
114+
ABS(CHECKSUM(NEWID())) % 100, LEFT(NEWID(), 20)
115+
FROM sys.all_objects AS a CROSS JOIN sys.all_objects AS b;
116+
117+
INSERT INTO dbo.test_ic_filtered (col_a, col_b, status_code)
118+
SELECT TOP (10000) ABS(CHECKSUM(NEWID())) % 1000, ABS(CHECKSUM(NEWID())) % 500,
119+
ABS(CHECKSUM(NEWID())) % 5
120+
FROM sys.all_objects AS a CROSS JOIN sys.all_objects AS b;
121+
122+
INSERT INTO dbo.test_ic_heap (col_a, col_b, col_c)
123+
SELECT TOP (10000) ABS(CHECKSUM(NEWID())) % 1000, ABS(CHECKSUM(NEWID())) % 500,
124+
ABS(CHECKSUM(NEWID())) % 200
125+
FROM sys.all_objects AS a CROSS JOIN sys.all_objects AS b;
126+
127+
INSERT INTO dbo.test_ic_multi (col_a, col_b)
128+
SELECT TOP (10000) ABS(CHECKSUM(NEWID())) % 1000, ABS(CHECKSUM(NEWID())) % 500
129+
FROM sys.all_objects AS a CROSS JOIN sys.all_objects AS b;
130+
131+
INSERT INTO dbo.test_ic_view_base (col_a, col_b, col_c)
132+
SELECT TOP (10000) ABS(CHECKSUM(NEWID())) % 100, ABS(CHECKSUM(NEWID())) % 50,
133+
ABS(CHECKSUM(NEWID())) % 200
134+
FROM sys.all_objects AS a CROSS JOIN sys.all_objects AS b;
135+
GO
136+
137+
/* ============================================= */
138+
/* Create test indexes */
139+
/* ============================================= */
140+
141+
/* Group 1: UC as superset (#721, #724) */
142+
ALTER TABLE dbo.test_ic_uc ADD CONSTRAINT uq_uc_abc UNIQUE (col_a, col_b, col_c);
143+
CREATE NONCLUSTERED INDEX ix_uc_ab ON dbo.test_ic_uc (col_a, col_b);
144+
CREATE NONCLUSTERED INDEX ix_uc_ab_inc ON dbo.test_ic_uc (col_a, col_b) INCLUDE (col_e);
145+
CREATE NONCLUSTERED INDEX ix_uc_bc ON dbo.test_ic_uc (col_b, col_c);
146+
CREATE UNIQUE NONCLUSTERED INDEX uix_uc_acd ON dbo.test_ic_uc (col_a, col_c, col_d);
147+
CREATE NONCLUSTERED INDEX ix_uc_ac ON dbo.test_ic_uc (col_a, col_c);
148+
ALTER TABLE dbo.test_ic_uc ADD CONSTRAINT uq_uc_ad UNIQUE (col_a, col_d);
149+
150+
/* Group 2: Sort direction */
151+
CREATE INDEX ix_sort_a_desc ON dbo.test_ic_basic (col_a DESC);
152+
CREATE INDEX ix_sort_a_desc2 ON dbo.test_ic_basic (col_a DESC);
153+
CREATE INDEX ix_sort_a_asc ON dbo.test_ic_basic (col_a ASC);
154+
CREATE INDEX ix_sort_ab_asc ON dbo.test_ic_basic (col_a ASC, col_b ASC);
155+
CREATE INDEX ix_sort_ab_mixed ON dbo.test_ic_basic (col_a DESC, col_b ASC);
156+
157+
/* Group 3: Filtered indexes */
158+
CREATE INDEX ix_filt_a_s1 ON dbo.test_ic_filtered (col_a) WHERE status_code = 1;
159+
CREATE INDEX ix_filt_a_s1_dup ON dbo.test_ic_filtered (col_a) WHERE status_code = 1;
160+
CREATE INDEX ix_filt_a_s2 ON dbo.test_ic_filtered (col_a) WHERE status_code = 2;
161+
CREATE INDEX ix_filt_ab_s3 ON dbo.test_ic_filtered (col_a, col_b) WHERE status_code = 3;
162+
CREATE INDEX ix_filt_a_s3 ON dbo.test_ic_filtered (col_a) WHERE status_code = 3;
163+
CREATE INDEX ix_filt_ab_s4 ON dbo.test_ic_filtered (col_a, col_b) WHERE status_code = 4;
164+
CREATE INDEX ix_filt_a_s0 ON dbo.test_ic_filtered (col_a) WHERE status_code = 0;
165+
166+
/* Group 4a: Key Duplicate — same keys, different includes, no wider index */
167+
CREATE INDEX ix_inc_f_inc_b ON dbo.test_ic_basic (col_f) INCLUDE (col_b);
168+
CREATE INDEX ix_inc_f_inc_c ON dbo.test_ic_basic (col_f) INCLUDE (col_c);
169+
170+
/* Group 4b: Key Subset — narrower key with includes absorbed by wider key */
171+
CREATE INDEX ix_inc_cd_inc_e ON dbo.test_ic_basic (col_c, col_d) INCLUDE (col_e);
172+
CREATE INDEX ix_inc_c_inc_b ON dbo.test_ic_basic (col_c) INCLUDE (col_b);
173+
174+
/* Group 5: Indexed view */
175+
CREATE UNIQUE CLUSTERED INDEX cx_test_ic_view ON dbo.test_ic_view (col_a, col_b);
176+
CREATE NONCLUSTERED INDEX ix_view_a ON dbo.test_ic_view (col_a);
177+
CREATE NONCLUSTERED INDEX ix_view_a_dup ON dbo.test_ic_view (col_a);
178+
179+
/* Group 6: Heap */
180+
CREATE NONCLUSTERED INDEX ix_heap_a ON dbo.test_ic_heap (col_a);
181+
CREATE NONCLUSTERED INDEX ix_heap_a_dup ON dbo.test_ic_heap (col_a);
182+
183+
/* Group 7: Multi-table isolation */
184+
CREATE INDEX ix_multi_a ON dbo.test_ic_multi (col_a);
185+
CREATE INDEX ix_basic_col_d ON dbo.test_ic_basic (col_d);
186+
GO
187+
188+
/* ============================================= */
189+
/* Generate usage stats */
190+
/* ============================================= */
191+
DECLARE @c bigint, @i integer = 0;
192+
WHILE @i < 10
193+
BEGIN
194+
SELECT @c = COUNT_BIG(*) FROM dbo.test_ic_uc WITH (INDEX = uq_uc_abc) WHERE col_a = 1;
195+
SELECT @c = COUNT_BIG(*) FROM dbo.test_ic_uc WITH (INDEX = ix_uc_ab) WHERE col_a = 1;
196+
SELECT @c = COUNT_BIG(*) FROM dbo.test_ic_uc WITH (INDEX = ix_uc_ab_inc) WHERE col_a = 1;
197+
SELECT @c = COUNT_BIG(*) FROM dbo.test_ic_uc WITH (INDEX = ix_uc_bc) WHERE col_b = 1;
198+
SELECT @c = COUNT_BIG(*) FROM dbo.test_ic_uc WITH (INDEX = uix_uc_acd) WHERE col_a = 1;
199+
SELECT @c = COUNT_BIG(*) FROM dbo.test_ic_uc WITH (INDEX = ix_uc_ac) WHERE col_a = 1;
200+
SELECT @c = COUNT_BIG(*) FROM dbo.test_ic_uc WITH (INDEX = uq_uc_ad) WHERE col_a = 1;
201+
SELECT @c = COUNT_BIG(*) FROM dbo.test_ic_basic WITH (INDEX = ix_sort_a_desc) WHERE col_a > 500;
202+
SELECT @c = COUNT_BIG(*) FROM dbo.test_ic_basic WITH (INDEX = ix_sort_a_desc2) WHERE col_a > 600;
203+
SELECT @c = COUNT_BIG(*) FROM dbo.test_ic_basic WITH (INDEX = ix_sort_a_asc) WHERE col_a < 100;
204+
SELECT @c = COUNT_BIG(*) FROM dbo.test_ic_basic WITH (INDEX = ix_sort_ab_asc) WHERE col_a = 1;
205+
SELECT @c = COUNT_BIG(*) FROM dbo.test_ic_basic WITH (INDEX = ix_sort_ab_mixed) WHERE col_a = 2;
206+
SELECT @c = COUNT_BIG(*) FROM dbo.test_ic_filtered WITH (INDEX = ix_filt_a_s1) WHERE col_a > 500 AND status_code = 1;
207+
SELECT @c = COUNT_BIG(*) FROM dbo.test_ic_filtered WITH (INDEX = ix_filt_a_s1_dup) WHERE col_a > 600 AND status_code = 1;
208+
SELECT @c = COUNT_BIG(*) FROM dbo.test_ic_filtered WITH (INDEX = ix_filt_a_s2) WHERE col_a > 500 AND status_code = 2;
209+
SELECT @c = COUNT_BIG(*) FROM dbo.test_ic_filtered WITH (INDEX = ix_filt_ab_s3) WHERE col_a = 1 AND status_code = 3;
210+
SELECT @c = COUNT_BIG(*) FROM dbo.test_ic_filtered WITH (INDEX = ix_filt_a_s3) WHERE col_a = 2 AND status_code = 3;
211+
SELECT @c = COUNT_BIG(*) FROM dbo.test_ic_filtered WITH (INDEX = ix_filt_ab_s4) WHERE col_a = 1 AND status_code = 4;
212+
SELECT @c = COUNT_BIG(*) FROM dbo.test_ic_filtered WITH (INDEX = ix_filt_a_s0) WHERE col_a = 1 AND status_code = 0;
213+
SELECT @c = COUNT_BIG(*) FROM dbo.test_ic_basic WITH (INDEX = ix_inc_f_inc_b) WHERE col_f > '2020-01-01';
214+
SELECT @c = COUNT_BIG(*) FROM dbo.test_ic_basic WITH (INDEX = ix_inc_f_inc_c) WHERE col_f > '2021-01-01';
215+
SELECT @c = COUNT_BIG(*) FROM dbo.test_ic_basic WITH (INDEX = ix_inc_cd_inc_e) WHERE col_c = 1 AND col_d = 1;
216+
SELECT @c = COUNT_BIG(*) FROM dbo.test_ic_basic WITH (INDEX = ix_inc_c_inc_b) WHERE col_c = 2;
217+
SELECT @c = COUNT_BIG(*) FROM dbo.test_ic_view WITH (INDEX = ix_view_a, NOEXPAND) WHERE col_a = 1;
218+
SELECT @c = COUNT_BIG(*) FROM dbo.test_ic_view WITH (INDEX = ix_view_a_dup, NOEXPAND) WHERE col_a = 2;
219+
SELECT @c = COUNT_BIG(*) FROM dbo.test_ic_heap WITH (INDEX = ix_heap_a) WHERE col_a = 1;
220+
SELECT @c = COUNT_BIG(*) FROM dbo.test_ic_heap WITH (INDEX = ix_heap_a_dup) WHERE col_a = 2;
221+
SELECT @c = COUNT_BIG(*) FROM dbo.test_ic_multi WITH (INDEX = ix_multi_a) WHERE col_a = 1;
222+
SELECT @c = COUNT_BIG(*) FROM dbo.test_ic_basic WITH (INDEX = ix_basic_col_d) WHERE col_d = 1;
223+
SELECT @i += 1;
224+
END;
225+
GO
226+
227+
/* ============================================= */
228+
/* Run sp_IndexCleanup */
229+
/* ============================================= */
230+
EXECUTE dbo.sp_IndexCleanup
231+
@database_name = N'StackOverflow2013',
232+
@dedupe_only = 1;
233+
GO
234+
235+
/* ============================================= */
236+
/* Cleanup */
237+
/* ============================================= */
238+
IF OBJECT_ID('dbo.test_ic_view') IS NOT NULL
239+
BEGIN
240+
DROP INDEX ix_view_a ON dbo.test_ic_view;
241+
DROP INDEX ix_view_a_dup ON dbo.test_ic_view;
242+
DROP INDEX cx_test_ic_view ON dbo.test_ic_view;
243+
END;
244+
GO
245+
IF OBJECT_ID('dbo.test_ic_view') IS NOT NULL DROP VIEW dbo.test_ic_view;
246+
GO
247+
DROP TABLE IF EXISTS dbo.test_ic_basic;
248+
DROP TABLE IF EXISTS dbo.test_ic_uc;
249+
DROP TABLE IF EXISTS dbo.test_ic_filtered;
250+
DROP TABLE IF EXISTS dbo.test_ic_heap;
251+
DROP TABLE IF EXISTS dbo.test_ic_multi;
252+
DROP TABLE IF EXISTS dbo.test_ic_view_base;
253+
GO

0 commit comments

Comments
 (0)