-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmissing_primary_keys.sql
More file actions
157 lines (156 loc) · 5.2 KB
/
Copy pathmissing_primary_keys.sql
File metadata and controls
157 lines (156 loc) · 5.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
/*******************************************************************************
Script Name: missing_primary_keys.sql
Purpose: Finds user tables without a primary key and reports row count, storage type, unique indexes, and incoming foreign keys. Temporal history tables are excluded because they normally cannot have a primary key.
Scope: Current database
SQL Server: 2016+
Azure SQL: Azure SQL Database and Managed Instance; see docs/COMPATIBILITY.md
Permissions: Metadata visibility; physical-statistics scripts require VIEW DATABASE STATE
Risk: Read-only; review and test any generated SQL before execution.
Output: Priority, Category, Object, Finding, Evidence, Recommendation, SuggestedSql, Risk
Author: TheMax-Lab
Version: 1.0
License: MIT
*******************************************************************************/
;WITH TableRows AS
(
SELECT
p.object_id,
SUM(CONVERT(bigint, p.rows)) AS RowCount
FROM sys.partitions AS p
WHERE p.index_id IN (0, 1)
GROUP BY
p.object_id
),
IndexSummary AS
(
SELECT
i.object_id,
SUM
(
CASE
WHEN i.is_unique = 1
AND i.index_id > 0
AND i.is_disabled = 0
THEN 1
ELSE 0
END
) AS UniqueIndexCount,
SUM
(
CASE
WHEN i.is_unique = 1
AND i.has_filter = 0
AND i.index_id > 0
AND i.is_disabled = 0
THEN 1
ELSE 0
END
) AS UnfilteredUniqueIndexCount,
MAX
(
CASE
WHEN i.index_id = 0 THEN 1
ELSE 0
END
) AS IsHeap,
MAX
(
CASE
WHEN i.type = 1 THEN 1
ELSE 0
END
) AS HasClusteredIndex
FROM sys.indexes AS i
WHERE i.is_hypothetical = 0
GROUP BY
i.object_id
),
IncomingForeignKeys AS
(
SELECT
fk.referenced_object_id AS object_id,
COUNT(*) AS IncomingForeignKeyCount
FROM sys.foreign_keys AS fk
WHERE fk.is_ms_shipped = 0
GROUP BY
fk.referenced_object_id
)
SELECT
CASE
WHEN COALESCE(tr.RowCount, 0) > 0
AND COALESCE(ix.UnfilteredUniqueIndexCount, 0) = 0
THEN 'High'
ELSE 'Medium'
END AS [Priority],
'Schema' AS [Category],
CONCAT
(
QUOTENAME(s.name),
'.',
QUOTENAME(t.name)
) AS [Object],
CASE
WHEN COALESCE(ix.UnfilteredUniqueIndexCount, 0) > 0 THEN
'Table has no primary key; an unfiltered unique index may be a candidate'
ELSE
'Table has no primary key or obvious unfiltered unique candidate'
END AS [Finding],
CONCAT
(
'rows=', COALESCE(tr.RowCount, 0),
'; storage=',
CASE
WHEN COALESCE(ix.IsHeap, 0) = 1 THEN 'HEAP'
WHEN COALESCE(ix.HasClusteredIndex, 0) = 1 THEN 'CLUSTERED INDEX'
WHEN t.is_memory_optimized = 1 THEN 'MEMORY OPTIMIZED'
ELSE 'NONCLUSTERED OR SPECIALIZED'
END,
'; unique indexes=', COALESCE(ix.UniqueIndexCount, 0),
'; unfiltered unique indexes=',
COALESCE(ix.UnfilteredUniqueIndexCount, 0),
'; incoming foreign keys=',
COALESCE(ifk.IncomingForeignKeyCount, 0)
) AS [Evidence],
'Identify a narrow, stable, unique, and NOT NULL business or surrogate key. Check for duplicates and NULL values, evaluate clustered versus nonclustered placement, and document intentional exceptions such as staging or transient tables.' AS [Recommendation],
CONCAT
(
'-- Replace <key_column> after profiling uniqueness, NULL values, and application dependencies.',
CHAR(13),
CHAR(10),
'ALTER TABLE ',
QUOTENAME(s.name),
'.',
QUOTENAME(t.name),
' ADD CONSTRAINT ',
QUOTENAME(LEFT(N'PK_' + t.name, 128)),
' PRIMARY KEY (<key_column>);'
) AS [SuggestedSql],
'High: choosing an incorrect key can break application behavior. Creating a primary key can fail on duplicates or NULLs, consume log and storage, block activity, and alter physical access paths if created as clustered.' AS [Risk]
FROM sys.tables AS t
INNER JOIN sys.schemas AS s
ON s.schema_id = t.schema_id
LEFT JOIN TableRows AS tr
ON tr.object_id = t.object_id
LEFT JOIN IndexSummary AS ix
ON ix.object_id = t.object_id
LEFT JOIN IncomingForeignKeys AS ifk
ON ifk.object_id = t.object_id
WHERE t.is_ms_shipped = 0
AND t.temporal_type <> 1
AND NOT EXISTS
(
SELECT 1
FROM sys.key_constraints AS kc
WHERE kc.parent_object_id = t.object_id
AND kc.type = 'PK'
)
ORDER BY
CASE
WHEN COALESCE(tr.RowCount, 0) > 0
AND COALESCE(ix.UnfilteredUniqueIndexCount, 0) = 0
THEN 0
ELSE 1
END,
COALESCE(tr.RowCount, 0) DESC,
s.name,
t.name;