diff --git a/sp_HumanEvents/sp_HumanEvents.sql b/sp_HumanEvents/sp_HumanEvents.sql index fd9e26cb..503359fb 100644 --- a/sp_HumanEvents/sp_HumanEvents.sql +++ b/sp_HumanEvents/sp_HumanEvents.sql @@ -3590,21 +3590,26 @@ BEGIN deqs.total_elapsed_time / 1000., avg_elapsed_time = CONVERT(decimal(38, 6), deqs.total_elapsed_time / 1000. / NULLIF(deqs.execution_count, 0)), + /* * 1.0 forces decimal division; bigint / integer truncates */ executions_per_second = ISNULL ( - deqs.execution_count / - NULLIF - ( - DATEDIFF + CONVERT + ( + decimal(38,2), + deqs.execution_count * 1.0 / + NULLIF ( - SECOND, - deqs.creation_time, - NULLIF(deqs.last_execution_time, '1900-01-01 00:00:00.000') - ), - 0 - ), - 0 + DATEDIFF + ( + SECOND, + deqs.creation_time, + NULLIF(deqs.last_execution_time, '1900-01-01 00:00:00.000') + ), + 0 + ) + ), + 0 ), total_physical_reads_mb = deqs.total_physical_reads * 8. / 1024., @@ -4239,7 +4244,7 @@ BEGIN SELECT N'HumanEvents_RecompilesByQuery', 0x430052004500410054004500200056004900450057002000640062006F002E00480075006D0061006E004500760065006E00740073005F005200650063006F006D00700069006C006500730042007900510075006500720079000D000A00410053000D000A0057004900540048000D000A0020002000200020006300620071002000410053000D000A0028000D000A002000200020002000530045004C004500430054000D000A0020002000200020002000200020002000730074006100740065006D0065006E0074005F0074006500780074005F0063006800650063006B00730075006D002C000D000A002000200020002000200020002000200074006F00740061006C005F007200650063006F006D00700069006C006500730020003D00200043004F0055004E0054005F0042004900470028002A0029002C000D000A002000200020002000200020002000200074006F00740061006C005F0063006F006D00700069006C0065005F0063007000750020003D002000530055004D00280063006F006D00700069006C0065005F006300700075005F006D00730029002C000D000A00200020002000200020002000200020006100760067005F0063006F006D00700069006C0065005F0063007000750020003D002000410056004700280063006F006D00700069006C0065005F006300700075005F006D00730029002C000D000A00200020002000200020002000200020006D00610078005F0063006F006D00700069006C0065005F0063007000750020003D0020004D0041005800280063006F006D00700069006C0065005F006300700075005F006D00730029002C000D000A002000200020002000200020002000200074006F00740061006C005F0063006F006D00700069006C0065005F006400750072006100740069006F006E0020003D002000530055004D00280063006F006D00700069006C0065005F006400750072006100740069006F006E005F006D00730029002C000D000A00200020002000200020002000200020006100760067005F0063006F006D00700069006C0065005F006400750072006100740069006F006E0020003D002000410056004700280063006F006D00700069006C0065005F006400750072006100740069006F006E005F006D00730029002C000D000A00200020002000200020002000200020006D00610078005F0063006F006D00700069006C0065005F006400750072006100740069006F006E0020003D0020004D0041005800280063006F006D00700069006C0065005F006400750072006100740069006F006E005F006D00730029000D000A002000200020002000460052004F004D0020005B007200650070006C006100630065005F006D0065005D000D000A002000200020002000470052004F005500500020004200590020000D000A0020002000200020002000200020002000730074006100740065006D0065006E0074005F0074006500780074005F0063006800650063006B00730075006D000D000A00200020002000200048004100560049004E00470020000D000A002000200020002000200020002000200043004F0055004E0054005F0042004900470028002A00290020003E003D002000310030000D000A0029000D000A00530045004C00450043005400200054004F00500020002800320031003400370034003800330036003400380029000D000A0020002000200020006B002E006F0062006A006500630074005F006E0061006D0065002C000D000A0020002000200020006B002E00730074006100740065006D0065006E0074005F0074006500780074002C000D000A00200020002000200063002E0074006F00740061006C005F007200650063006F006D00700069006C00650073002C000D000A00200020002000200063002E0074006F00740061006C005F0063006F006D00700069006C0065005F006300700075002C000D000A00200020002000200063002E006100760067005F0063006F006D00700069006C0065005F006300700075002C000D000A00200020002000200063002E006D00610078005F0063006F006D00700069006C0065005F006300700075002C000D000A00200020002000200063002E0074006F00740061006C005F0063006F006D00700069006C0065005F006400750072006100740069006F006E002C000D000A00200020002000200063002E006100760067005F0063006F006D00700069006C0065005F006400750072006100740069006F006E002C000D000A00200020002000200063002E006D00610078005F0063006F006D00700069006C0065005F006400750072006100740069006F006E000D000A00460052004F004D002000630062007100200041005300200063000D000A00430052004F005300530020004100500050004C0059000D000A0028000D000A002000200020002000530045004C00450043005400200054004F00500020002800310029000D000A00200020002000200020002000200020006B002E002A000D000A002000200020002000460052004F004D0020005B007200650070006C006100630065005F006D0065005D0020004100530020006B000D000A00200020002000200057004800450052004500200063002E00730074006100740065006D0065006E0074005F0074006500780074005F0063006800650063006B00730075006D0020003D0020006B002E00730074006100740065006D0065006E0074005F0074006500780074005F0063006800650063006B00730075006D000D000A0020002000200020004F00520044004500520020004200590020006B002E0069006400200044004500530043000D000A00290020004100530020006B000D000A004F005200440045005200200042005900200063002E0074006F00740061006C005F007200650063006F006D00700069006C0065007300200044004500530043003B000D000A00 WHERE @compile_events = 1; INSERT #view_check (view_name, view_definition) - SELECT N'HumanEvents_WaitsByDatabase', 0x430052004500410054004500200056004900450057002000640062006F002E00480075006D0061006E004500760065006E00740073005F005700610069007400730042007900440061007400610062006100730065000D000A00410053000D000A0057004900540048000D000A002000200020002000770061006900740073002000410053000D000A0028000D000A002000200020002000530045004C004500430054000D000A002000200020002000200020002000200077006100690074005F007000610074007400650072006E0020003D0020004E00270074006F00740061006C0020007700610069007400730020006200790020006400610074006100620061007300650027002C000D000A00200020002000200020002000200020006D0069006E005F006500760065006E0074005F00740069006D00650020003D0020004D0049004E002800770061002E006500760065006E0074005F00740069006D00650029002C000D000A00200020002000200020002000200020006D00610078005F006500760065006E0074005F00740069006D00650020003D0020004D00410058002800770061002E006500760065006E0074005F00740069006D00650029002C000D000A0020002000200020002000200020002000770061002E00640061007400610062006100730065005F006E0061006D0065002C000D000A0020002000200020002000200020002000770061002E0077006100690074005F0074007900700065002C000D000A002000200020002000200020002000200074006F00740061006C005F007700610069007400730020003D00200043004F0055004E0054005F0042004900470028002A0029002C000D000A0020002000200020002000200020002000730075006D005F006400750072006100740069006F006E005F006D00730020003D002000530055004D002800770061002E006400750072006100740069006F006E005F006D00730029002C000D000A0020002000200020002000200020002000730075006D005F007300690067006E0061006C005F006400750072006100740069006F006E005F006D00730020003D002000530055004D002800770061002E007300690067006E0061006C005F006400750072006100740069006F006E005F006D00730029002C000D000A00200020002000200020002000200020006100760067005F006D0073005F007000650072005F00770061006900740020003D00200043004F004E005600450052005400280064006500630069006D0061006C002800330038002C00320029002C002000530055004D002800770061002E006400750072006100740069006F006E005F006D007300290020002A00200031002E00300020002F00200043004F0055004E0054005F0042004900470028002A00290029000D000A002000200020002000460052004F004D0020005B007200650070006C006100630065005F006D0065005D002000410053002000770061000D000A002000200020002000470052004F00550050002000420059000D000A0020002000200020002000200020002000770061002E00640061007400610062006100730065005F006E0061006D0065002C000D000A0020002000200020002000200020002000770061002E0077006100690074005F0074007900700065000D000A0029000D000A00530045004C00450043005400200054004F00500020002800320031003400370034003800330036003400380029000D000A002000200020002000770061006900740073002E0077006100690074005F007000610074007400650072006E002C000D000A002000200020002000770061006900740073002E006D0069006E005F006500760065006E0074005F00740069006D0065002C000D000A002000200020002000770061006900740073002E006D00610078005F006500760065006E0074005F00740069006D0065002C000D000A002000200020002000770061006900740073002E00640061007400610062006100730065005F006E0061006D0065002C000D000A002000200020002000770061006900740073002E0077006100690074005F0074007900700065002C000D000A002000200020002000770061006900740073002E0074006F00740061006C005F00770061006900740073002C000D000A002000200020002000770061006900740073002E00730075006D005F006400750072006100740069006F006E005F006D0073002C000D000A002000200020002000770061006900740073002E00730075006D005F007300690067006E0061006C005F006400750072006100740069006F006E005F006D0073002C000D000A002000200020002000770061006900740073002E006100760067005F006D0073005F007000650072005F0077006100690074002C000D000A002000200020002000770061006900740073005F007000650072005F007300650063006F006E00640020003D0020000D000A0020002000200020002000200020002000490053004E0055004C004C00280043004F0055004E0054005F0042004900470028002A00290020002F0020004E0055004C004C004900460028004400410054004500440049004600460028005300450043004F004E0044002C002000770061006900740073002E006D0069006E005F006500760065006E0074005F00740069006D0065002C002000770061006900740073002E006D00610078005F006500760065006E0074005F00740069006D00650029002C002000300029002C002000300029002C000D000A002000200020002000770061006900740073005F007000650072005F0068006F007500720020003D0020000D000A0020002000200020002000200020002000490053004E0055004C004C00280043004F0055004E0054005F0042004900470028002A00290020002F0020004E0055004C004C0049004600280044004100540045004400490046004600280048004F00550052002C002000770061006900740073002E006D0069006E005F006500760065006E0074005F00740069006D0065002C002000770061006900740073002E006D00610078005F006500760065006E0074005F00740069006D00650029002C002000300029002C002000300029002C000D000A002000200020002000770061006900740073005F007000650072005F0064006100790020003D0020000D000A0020002000200020002000200020002000490053004E0055004C004C00280043004F0055004E0054005F0042004900470028002A00290020002F0020004E0055004C004C004900460028004400410054004500440049004600460028004400410059002C002000770061006900740073002E006D0069006E005F006500760065006E0074005F00740069006D0065002C002000770061006900740073002E006D00610078005F006500760065006E0074005F00740069006D00650029002C002000300029002C002000300029000D000A00460052004F004D002000770061006900740073000D000A00470052004F00550050002000420059000D000A002000200020002000770061006900740073002E0077006100690074005F007000610074007400650072006E002C000D000A002000200020002000770061006900740073002E006D0069006E005F006500760065006E0074005F00740069006D0065002C000D000A002000200020002000770061006900740073002E006D00610078005F006500760065006E0074005F00740069006D0065002C000D000A002000200020002000770061006900740073002E00640061007400610062006100730065005F006E0061006D0065002C000D000A002000200020002000770061006900740073002E0077006100690074005F0074007900700065002C000D000A002000200020002000770061006900740073002E0074006F00740061006C005F00770061006900740073002C000D000A002000200020002000770061006900740073002E00730075006D005F006400750072006100740069006F006E005F006D0073002C000D000A002000200020002000770061006900740073002E00730075006D005F007300690067006E0061006C005F006400750072006100740069006F006E005F006D0073002C000D000A002000200020002000770061006900740073002E006100760067005F006D0073005F007000650072005F0077006100690074000D000A004F00520044004500520020004200590020000D000A002000200020002000770061006900740073002E00730075006D005F006400750072006100740069006F006E005F006D007300200044004500530043003B00; + SELECT N'HumanEvents_WaitsByDatabase', 0x430052004500410054004500200056004900450057002000640062006F002E00480075006D0061006E004500760065006E00740073005F005700610069007400730042007900440061007400610062006100730065000D000A00410053000D000A0057004900540048000D000A002000200020002000770061006900740073002000410053000D000A0028000D000A002000200020002000530045004C004500430054000D000A002000200020002000200020002000200077006100690074005F007000610074007400650072006E0020003D0020004E00270074006F00740061006C0020007700610069007400730020006200790020006400610074006100620061007300650027002C000D000A00200020002000200020002000200020006D0069006E005F006500760065006E0074005F00740069006D00650020003D0020004D0049004E002800770061002E006500760065006E0074005F00740069006D00650029002C000D000A00200020002000200020002000200020006D00610078005F006500760065006E0074005F00740069006D00650020003D0020004D00410058002800770061002E006500760065006E0074005F00740069006D00650029002C000D000A0020002000200020002000200020002000770061002E00640061007400610062006100730065005F006E0061006D0065002C000D000A0020002000200020002000200020002000770061002E0077006100690074005F0074007900700065002C000D000A002000200020002000200020002000200074006F00740061006C005F007700610069007400730020003D00200043004F0055004E0054005F0042004900470028002A0029002C000D000A0020002000200020002000200020002000730075006D005F006400750072006100740069006F006E005F006D00730020003D002000530055004D002800770061002E006400750072006100740069006F006E005F006D00730029002C000D000A0020002000200020002000200020002000730075006D005F007300690067006E0061006C005F006400750072006100740069006F006E005F006D00730020003D002000530055004D002800770061002E007300690067006E0061006C005F006400750072006100740069006F006E005F006D00730029002C000D000A00200020002000200020002000200020006100760067005F006D0073005F007000650072005F00770061006900740020003D00200043004F004E005600450052005400280064006500630069006D0061006C002800330038002C00320029002C002000530055004D002800770061002E006400750072006100740069006F006E005F006D007300290020002A00200031002E00300020002F00200043004F0055004E0054005F0042004900470028002A00290029000D000A002000200020002000460052004F004D0020005B007200650070006C006100630065005F006D0065005D002000410053002000770061000D000A002000200020002000470052004F00550050002000420059000D000A0020002000200020002000200020002000770061002E00640061007400610062006100730065005F006E0061006D0065002C000D000A0020002000200020002000200020002000770061002E0077006100690074005F0074007900700065000D000A0029000D000A00530045004C00450043005400200054004F00500020002800320031003400370034003800330036003400380029000D000A002000200020002000770061006900740073002E0077006100690074005F007000610074007400650072006E002C000D000A002000200020002000770061006900740073002E006D0069006E005F006500760065006E0074005F00740069006D0065002C000D000A002000200020002000770061006900740073002E006D00610078005F006500760065006E0074005F00740069006D0065002C000D000A002000200020002000770061006900740073002E00640061007400610062006100730065005F006E0061006D0065002C000D000A002000200020002000770061006900740073002E0077006100690074005F0074007900700065002C000D000A002000200020002000770061006900740073002E0074006F00740061006C005F00770061006900740073002C000D000A002000200020002000770061006900740073002E00730075006D005F006400750072006100740069006F006E005F006D0073002C000D000A002000200020002000770061006900740073002E00730075006D005F007300690067006E0061006C005F006400750072006100740069006F006E005F006D0073002C000D000A002000200020002000770061006900740073002E006100760067005F006D0073005F007000650072005F0077006100690074002C000D000A002000200020002000770061006900740073005F007000650072005F007300650063006F006E00640020003D0020000D000A0020002000200020002000200020002000490053004E0055004C004C00280043004F004E005600450052005400280064006500630069006D0061006C002800330038002C00320029002C002000770061006900740073002E0074006F00740061006C005F007700610069007400730020002A00200031002E00300020002F0020004E0055004C004C004900460028004400410054004500440049004600460028005300450043004F004E0044002C002000770061006900740073002E006D0069006E005F006500760065006E0074005F00740069006D0065002C002000770061006900740073002E006D00610078005F006500760065006E0074005F00740069006D00650029002C0020003000290029002C002000300029002C000D000A002000200020002000770061006900740073005F007000650072005F0068006F007500720020003D0020000D000A0020002000200020002000200020002000490053004E0055004C004C00280043004F004E005600450052005400280064006500630069006D0061006C002800330038002C00320029002C002000770061006900740073002E0074006F00740061006C005F007700610069007400730020002A00200031002E00300020002F0020004E0055004C004C0049004600280044004100540045004400490046004600280048004F00550052002C002000770061006900740073002E006D0069006E005F006500760065006E0074005F00740069006D0065002C002000770061006900740073002E006D00610078005F006500760065006E0074005F00740069006D00650029002C0020003000290029002C002000300029002C000D000A002000200020002000770061006900740073005F007000650072005F0064006100790020003D0020000D000A0020002000200020002000200020002000490053004E0055004C004C00280043004F004E005600450052005400280064006500630069006D0061006C002800330038002C00320029002C002000770061006900740073002E0074006F00740061006C005F007700610069007400730020002A00200031002E00300020002F0020004E0055004C004C004900460028004400410054004500440049004600460028004400410059002C002000770061006900740073002E006D0069006E005F006500760065006E0074005F00740069006D0065002C002000770061006900740073002E006D00610078005F006500760065006E0074005F00740069006D00650029002C0020003000290029002C002000300029000D000A00460052004F004D002000770061006900740073000D000A00470052004F00550050002000420059000D000A002000200020002000770061006900740073002E0077006100690074005F007000610074007400650072006E002C000D000A002000200020002000770061006900740073002E006D0069006E005F006500760065006E0074005F00740069006D0065002C000D000A002000200020002000770061006900740073002E006D00610078005F006500760065006E0074005F00740069006D0065002C000D000A002000200020002000770061006900740073002E00640061007400610062006100730065005F006E0061006D0065002C000D000A002000200020002000770061006900740073002E0077006100690074005F0074007900700065002C000D000A002000200020002000770061006900740073002E0074006F00740061006C005F00770061006900740073002C000D000A002000200020002000770061006900740073002E00730075006D005F006400750072006100740069006F006E005F006D0073002C000D000A002000200020002000770061006900740073002E00730075006D005F007300690067006E0061006C005F006400750072006100740069006F006E005F006D0073002C000D000A002000200020002000770061006900740073002E006100760067005F006D0073005F007000650072005F0077006100690074000D000A004F00520044004500520020004200590020000D000A002000200020002000770061006900740073002E00730075006D005F006400750072006100740069006F006E005F006D007300200044004500530043003B00; INSERT #view_check (view_name, view_definition) SELECT N'HumanEvents_WaitsByQueryAndDatabase', 0x430052004500410054004500200056004900450057002000640062006F002E00480075006D0061006E004500760065006E00740073005F0057006100690074007300420079005100750065007200790041006E006400440061007400610062006100730065000D000A00410053000D000A0057004900540048000D000A00200020002000200070006C0061006E005F00770061006900740073002000410053000D000A0028000D000A002000200020002000530045004C004500430054000D000A002000200020002000200020002000200077006100690074005F007000610074007400650072006E0020003D0020004E00270077006100690074007300200062007900200071007500650072007900200061006E00640020006400610074006100620061007300650027002C000D000A00200020002000200020002000200020006D0069006E005F006500760065006E0074005F00740069006D00650020003D0020004D0049004E002800770061002E006500760065006E0074005F00740069006D00650029002C000D000A00200020002000200020002000200020006D00610078005F006500760065006E0074005F00740069006D00650020003D0020004D00410058002800770061002E006500760065006E0074005F00740069006D00650029002C000D000A0020002000200020002000200020002000770061002E00640061007400610062006100730065005F006E0061006D0065002C000D000A0020002000200020002000200020002000770061002E0077006100690074005F0074007900700065002C000D000A002000200020002000200020002000200074006F00740061006C005F007700610069007400730020003D00200043004F0055004E0054005F0042004900470028002A0029002C000D000A0020002000200020002000200020002000770061002E0070006C0061006E005F00680061006E0064006C0065002C000D000A0020002000200020002000200020002000770061002E00710075006500720079005F0070006C0061006E005F0068006100730068005F007300690067006E00650064002C000D000A0020002000200020002000200020002000770061002E00710075006500720079005F0068006100730068005F007300690067006E00650064002C000D000A0020002000200020002000200020002000730075006D005F006400750072006100740069006F006E005F006D00730020003D002000530055004D002800770061002E006400750072006100740069006F006E005F006D00730029002C000D000A0020002000200020002000200020002000730075006D005F007300690067006E0061006C005F006400750072006100740069006F006E005F006D00730020003D002000530055004D002800770061002E007300690067006E0061006C005F006400750072006100740069006F006E005F006D00730029002C000D000A00200020002000200020002000200020006100760067005F006D0073005F007000650072005F00770061006900740020003D00200043004F004E005600450052005400280064006500630069006D0061006C002800330038002C00320029002C002000530055004D002800770061002E006400750072006100740069006F006E005F006D007300290020002A00200031002E00300020002F00200043004F0055004E0054005F0042004900470028002A00290029000D000A002000200020002000460052004F004D0020005B007200650070006C006100630065005F006D0065005D002000410053002000770061000D000A002000200020002000470052004F00550050002000420059000D000A0020002000200020002000200020002000770061002E00640061007400610062006100730065005F006E0061006D0065002C000D000A0020002000200020002000200020002000770061002E0077006100690074005F0074007900700065002C000D000A0020002000200020002000200020002000770061002E00710075006500720079005F0068006100730068005F007300690067006E00650064002C000D000A0020002000200020002000200020002000770061002E00710075006500720079005F0070006C0061006E005F0068006100730068005F007300690067006E00650064002C000D000A0020002000200020002000200020002000770061002E0070006C0061006E005F00680061006E0064006C0065000D000A0029000D000A00530045004C00450043005400200054004F00500020002800320031003400370034003800330036003400380029000D000A002000200020002000700077002E0077006100690074005F007000610074007400650072006E002C000D000A002000200020002000700077002E006D0069006E005F006500760065006E0074005F00740069006D0065002C000D000A002000200020002000700077002E006D00610078005F006500760065006E0074005F00740069006D0065002C000D000A002000200020002000700077002E00640061007400610062006100730065005F006E0061006D0065002C000D000A002000200020002000700077002E0077006100690074005F0074007900700065002C000D000A002000200020002000700077002E0074006F00740061006C005F00770061006900740073002C000D000A002000200020002000700077002E00730075006D005F006400750072006100740069006F006E005F006D0073002C000D000A002000200020002000700077002E00730075006D005F007300690067006E0061006C005F006400750072006100740069006F006E005F006D0073002C000D000A002000200020002000700077002E006100760067005F006D0073005F007000650072005F0077006100690074002C000D000A002000200020002000730074002E0074006500780074002C000D000A002000200020002000710070002E00710075006500720079005F0070006C0061006E000D000A00460052004F004D00200070006C0061006E005F00770061006900740073002000410053002000700077000D000A004F00550054004500520020004100500050004C00590020007300790073002E0064006D005F0065007800650063005F00710075006500720079005F0070006C0061006E002800700077002E0070006C0061006E005F00680061006E0064006C00650029002000410053002000710070000D000A004F00550054004500520020004100500050004C00590020007300790073002E0064006D005F0065007800650063005F00730071006C005F0074006500780074002800700077002E0070006C0061006E005F00680061006E0064006C00650029002000410053002000730074000D000A004F00520044004500520020004200590020000D000A002000200020002000700077002E00730075006D005F006400750072006100740069006F006E005F006D007300200044004500530043003B00; INSERT #view_check (view_name, view_definition) diff --git a/sp_IndexCleanup/sp_IndexCleanup.sql b/sp_IndexCleanup/sp_IndexCleanup.sql index 49977943..eea4d41c 100644 --- a/sp_IndexCleanup/sp_IndexCleanup.sql +++ b/sp_IndexCleanup/sp_IndexCleanup.sql @@ -3710,6 +3710,9 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. AND ISNULL(ia2_inner.filter_definition, N'') = ISNULL(ia1.filter_definition, N'') AND NOT (ia1.is_unique = 1 AND ia2_inner.is_unique = 0) AND ia2_inner.consolidation_rule IS NULL + /* Same usage floor as the outer join below: an index that missed + @min_reads / @min_writes cannot serve as the merge target. */ + AND ia2_inner.meets_usage_floor = 1 AND EXISTS ( SELECT @@ -4327,7 +4330,10 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. AND ps_uq.partition_columns IS NOT NULL AND CHARINDEX(ps_uq.partition_columns, ia_nc.key_columns) = 0 ) - WHERE NOT EXISTS + /* Both sides must have cleared @min_reads / @min_writes on their own */ + WHERE ia_uc.meets_usage_floor = 1 + AND ia_nc.meets_usage_floor = 1 + AND NOT EXISTS ( /* Don't propose replacing a unique constraint that backs an inbound foreign key. Dropping it would be blocked by SQL Server, and @@ -4372,6 +4378,10 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. AND id_nc.is_unique_constraint = 0 /* This is not a unique constraint */ /* Same filtered-index guard as the DISABLE marking above. */ WHERE ia_nc.filter_definition IS NULL + /* Same usage floor as the DISABLE marking above: this index is the one + being rewritten as unique, so it must have cleared @min_reads / + @min_writes on its own. */ + AND ia_nc.meets_usage_floor = 1 /* Same Msg 1908 guard as the DISABLE marking above. */ AND NOT EXISTS ( @@ -4395,6 +4405,10 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ON id_uc.index_hash = ia_uc.index_hash AND id_uc.is_unique_constraint = 1 WHERE ia_uc.scope_hash = ia_nc.scope_hash + /* And the constraint has to clear the floor too. The DISABLE marking + above passed on any that didn't, so promoting this index would + leave a MAKE UNIQUE with no constraint to replace. */ + AND ia_uc.meets_usage_floor = 1 /* Check that both indexes have EXACTLY the same key columns */ AND ia_uc.key_columns = ia_nc.key_columns ) @@ -4478,6 +4492,9 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. AND ia_loser.consolidation_rule = N'Unique Constraint Replacement' AND ia_keeper.action = N'KEEP' AND ia_keeper.consolidation_rule = N'Unique Constraint Replacement' + /* Both sides must have cleared @min_reads / @min_writes on their own */ + AND ia_loser.meets_usage_floor = 1 + AND ia_keeper.meets_usage_floor = 1 /* Both rows must be unique constraints (not regular nonclustered indexes that Rule 7 also marked KEEP for some reason) */ AND EXISTS @@ -4579,6 +4596,9 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. AND ia_winner.consolidation_rule = N'Unique Constraint Replacement' WHERE ia_dup.consolidation_rule IS NULL AND ia_dup.action IS NULL + /* Both sides must have cleared @min_reads / @min_writes on their own */ + AND ia_dup.meets_usage_floor = 1 + AND ia_winner.meets_usage_floor = 1 /* Exclude unique constraints (they're handled separately) */ AND NOT EXISTS ( diff --git a/sp_IndexCleanup/tests/rule_coverage_test.py b/sp_IndexCleanup/tests/rule_coverage_test.py index fa88674c..4112b02f 100644 --- a/sp_IndexCleanup/tests/rule_coverage_test.py +++ b/sp_IndexCleanup/tests/rule_coverage_test.py @@ -185,6 +185,9 @@ def _sqlcmd_prefix(): DROP TABLE IF EXISTS dbo.ic_pk_exact_test; DROP TABLE IF EXISTS dbo.ic_sort_dir_test; DROP TABLE IF EXISTS dbo.ic_key_include_test; +DROP TABLE IF EXISTS dbo.ic_floor_target_test; +DROP TABLE IF EXISTS dbo.ic_floor_uc_test; +DROP TABLE IF EXISTS dbo.ic_floor_dup_test; GO /* @@ -323,6 +326,76 @@ def _sqlcmd_prefix(): filler varchar(100) NOT NULL, CONSTRAINT pk_ic_key_include_test PRIMARY KEY CLUSTERED (id) ); + +/* +ic_floor_target_test: the usage floor has to gate the index Rule 3 picks as a +merge TARGET, not only the subset it disables. + +ix_ft_sub (col_a) is the subset. Two indexes qualify as its superset: +ix_ft_cold (col_a, col_b) at ~5 reads and ix_ft_hot (col_a, col_c) at ~200. +Rule 3's target subquery orders by LEN(key_columns) then index_name -- the two +key lists are the same length, so the NAME decides and ix_ft_cold wins it. + +That ordering is the whole point. Under @min_reads = 100 the outer join still +matches, because ix_ft_hot clears the floor and gives Rule 3 something to fire +on, while the subquery on its own would still hand back ix_ft_cold: a below-floor +index named as the merge target and then rebuilt by Rule 4/6. The subset's +INCLUDE (col_d) is what makes that rebuild real -- without an include to merge, +the target is flipped back to KEEP and gets no script either way, and the +assertion would pass without proving anything. +*/ +CREATE TABLE + dbo.ic_floor_target_test +( + id integer NOT NULL, + col_a integer NOT NULL, + col_b integer NOT NULL, + col_c integer NOT NULL, + col_d integer NOT NULL, + filler varchar(100) NOT NULL, + CONSTRAINT pk_ic_floor_target_test PRIMARY KEY CLUSTERED (id) +); + +/* +ic_floor_uc_test: the usage floor has to gate Rule 7.5 as well. + +uq_floor is a unique constraint at ~200 reads; ix_floor_nc has its exact key +columns at ~5. Rule 7 will not promote ix_floor_nc under a floor of 100 -- it +checks meets_usage_floor -- but Rule 7.5 reaches the same outcome on its own: +DISABLE the constraint, rewrite the below-floor index as unique. Same shape as +Group G's icg_uc, which is the pair already proven to make Rule 7.5 fire. +*/ +CREATE TABLE + dbo.ic_floor_uc_test +( + id integer NOT NULL, + col_a integer NOT NULL, + col_c integer NOT NULL, + filler varchar(100) NOT NULL, + CONSTRAINT pk_ic_floor_uc_test PRIMARY KEY CLUSTERED (id) +); + +/* +ic_floor_dup_test: Rule 7.6, which disables key duplicates of an index Rule 7.5 +just made unique. + +uq_fd plus ix_fd_win (~200 reads each) is the pair that produces the MAKE UNIQUE +winner. ix_fd_cold carries the same keys with different includes at ~5 reads. + +Under a floor of 100 that cold index is exactly the case Rule 5 refuses -- same +keys, different includes, one side below the floor -- so it arrives at Rule 7.6 +still unprocessed, and Rule 7.6 had no floor check of its own to stop it. +*/ +CREATE TABLE + dbo.ic_floor_dup_test +( + id integer NOT NULL, + col_a integer NOT NULL, + col_c integer NOT NULL, + col_d integer NOT NULL, + filler varchar(100) NOT NULL, + CONSTRAINT pk_ic_floor_dup_test PRIMARY KEY CLUSTERED (id) +); GO INSERT INTO @@ -445,6 +518,63 @@ def _sqlcmd_prefix(): FROM sys.all_columns AS ac1 CROSS JOIN sys.all_columns AS ac2 OPTION(MAXDOP 1); + +INSERT INTO + dbo.ic_floor_target_test +( + id, + col_a, + col_b, + col_c, + col_d, + filler +) +SELECT TOP (20000) + id = ROW_NUMBER() OVER (ORDER BY (SELECT NULL)), + col_a = ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) % 500, + col_b = ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) % 100, + col_c = ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) % 250, + col_d = ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) % 50, + filler = REPLICATE('x', 100) +FROM sys.all_columns AS ac1 +CROSS JOIN sys.all_columns AS ac2 +OPTION(MAXDOP 1); + +INSERT INTO + dbo.ic_floor_uc_test +( + id, + col_a, + col_c, + filler +) +SELECT TOP (20000) + id = ROW_NUMBER() OVER (ORDER BY (SELECT NULL)), + col_a = ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) % 500, + col_c = ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) % 250, + filler = REPLICATE('x', 100) +FROM sys.all_columns AS ac1 +CROSS JOIN sys.all_columns AS ac2 +OPTION(MAXDOP 1); + +INSERT INTO + dbo.ic_floor_dup_test +( + id, + col_a, + col_c, + col_d, + filler +) +SELECT TOP (20000) + id = ROW_NUMBER() OVER (ORDER BY (SELECT NULL)), + col_a = ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) % 500, + col_c = ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) % 250, + col_d = ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) % 50, + filler = REPLICATE('x', 100) +FROM sys.all_columns AS ac1 +CROSS JOIN sys.all_columns AS ac2 +OPTION(MAXDOP 1); GO CREATE INDEX ix_hot ON dbo.ic_min_reads_test (col_a) INCLUDE (col_b); @@ -468,6 +598,27 @@ def _sqlcmd_prefix(): /* Subset whose include (col_b) is already a key column of the superset */ CREATE INDEX ix_ki_subset ON dbo.ic_key_include_test (col_a) INCLUDE (col_b); CREATE INDEX ix_ki_superset ON dbo.ic_key_include_test (col_a, col_b); + +/* +One subset and two candidate supersets. The key lists of ix_ft_cold and +ix_ft_hot are the same length, so Rule 3's ORDER BY falls through to the index +name and ix_ft_cold -- the below-floor one -- is what it prefers. +*/ +CREATE INDEX ix_ft_sub ON dbo.ic_floor_target_test (col_a) INCLUDE (col_d); +CREATE INDEX ix_ft_cold ON dbo.ic_floor_target_test (col_a, col_b); +CREATE INDEX ix_ft_hot ON dbo.ic_floor_target_test (col_a, col_c); + +/* A unique constraint and a plain index carrying its exact key columns */ +ALTER TABLE dbo.ic_floor_uc_test ADD CONSTRAINT uq_floor UNIQUE (col_a, id); +CREATE INDEX ix_floor_nc ON dbo.ic_floor_uc_test (col_a, id) INCLUDE (col_c); + +/* +Same pair again, plus a cold key duplicate of the winner for Rule 7.6 to find +after Rule 7.5 has promoted ix_fd_win. +*/ +ALTER TABLE dbo.ic_floor_dup_test ADD CONSTRAINT uq_fd UNIQUE (col_a, id); +CREATE INDEX ix_fd_win ON dbo.ic_floor_dup_test (col_a, id) INCLUDE (col_c); +CREATE INDEX ix_fd_cold ON dbo.ic_floor_dup_test (col_a, id) INCLUDE (col_d); GO /* @@ -557,6 +708,45 @@ def _sqlcmd_prefix(): SELECT @i += 1; END; GO + +/* +Reads for the usage-floor fixtures: ~200 on everything that must clear a floor +of 100, ~5 on the two that must miss it. + +Every index here gets SOME read, primary keys included. An index at exactly zero +reads is claimed by Rule 1 as an Unused Index on any instance with more than +seven days of uptime, and a Rule 1 DISABLE would land in these groups' dedupe +counts and fail them for a reason that has nothing to do with the usage floor. +*/ +DECLARE + @c bigint, + @i integer = 0; + +WHILE @i < 200 +BEGIN + SELECT @c = COUNT_BIG(*) FROM dbo.ic_floor_target_test AS t WITH (INDEX = ix_ft_sub) WHERE t.col_a = @i % 500 OPTION(MAXDOP 1); + SELECT @c = COUNT_BIG(*) FROM dbo.ic_floor_target_test AS t WITH (INDEX = ix_ft_hot) WHERE t.col_a = @i % 500 OPTION(MAXDOP 1); + SELECT @c = COUNT_BIG(*) FROM dbo.ic_floor_uc_test AS t WITH (INDEX = uq_floor) WHERE t.col_a = @i % 500 OPTION(MAXDOP 1); + SELECT @c = COUNT_BIG(*) FROM dbo.ic_floor_dup_test AS t WITH (INDEX = uq_fd) WHERE t.col_a = @i % 500 OPTION(MAXDOP 1); + SELECT @c = COUNT_BIG(*) FROM dbo.ic_floor_dup_test AS t WITH (INDEX = ix_fd_win) WHERE t.col_a = @i % 500 OPTION(MAXDOP 1); + + SELECT @i += 1; +END; + +SELECT @i = 0; + +WHILE @i < 5 +BEGIN + SELECT @c = COUNT_BIG(*) FROM dbo.ic_floor_target_test AS t WITH (INDEX = ix_ft_cold) WHERE t.col_a = @i % 500 OPTION(MAXDOP 1); + SELECT @c = COUNT_BIG(*) FROM dbo.ic_floor_uc_test AS t WITH (INDEX = ix_floor_nc) WHERE t.col_a = @i % 500 OPTION(MAXDOP 1); + SELECT @c = COUNT_BIG(*) FROM dbo.ic_floor_dup_test AS t WITH (INDEX = ix_fd_cold) WHERE t.col_a = @i % 500 OPTION(MAXDOP 1); + SELECT @c = COUNT_BIG(*) FROM dbo.ic_floor_target_test AS t WITH (INDEX = pk_ic_floor_target_test) WHERE t.id = @i OPTION(MAXDOP 1); + SELECT @c = COUNT_BIG(*) FROM dbo.ic_floor_uc_test AS t WITH (INDEX = pk_ic_floor_uc_test) WHERE t.id = @i OPTION(MAXDOP 1); + SELECT @c = COUNT_BIG(*) FROM dbo.ic_floor_dup_test AS t WITH (INDEX = pk_ic_floor_dup_test) WHERE t.id = @i OPTION(MAXDOP 1); + + SELECT @i += 1; +END; +GO """ CLEANUP_SQL = """ @@ -568,6 +758,9 @@ def _sqlcmd_prefix(): DROP TABLE IF EXISTS dbo.ic_pk_exact_test; DROP TABLE IF EXISTS dbo.ic_sort_dir_test; DROP TABLE IF EXISTS dbo.ic_key_include_test; +DROP TABLE IF EXISTS dbo.ic_floor_target_test; +DROP TABLE IF EXISTS dbo.ic_floor_uc_test; +DROP TABLE IF EXISTS dbo.ic_floor_dup_test; """ # Group F's fixture. Unlike everything above it, this one needs two databases: @@ -1513,6 +1706,164 @@ def assert_test(group, name, condition, detail=""): % (len(uc_disable), [(r["database_name"], r["script"]) for r in uc_disable])) + # ---- Group H: the usage floor gates every dedupe path, not just some ---- + # + # Group B covers the screen where it always worked: an index below the floor + # is not the SUBJECT of a dedupe rule. These are the two places it could still + # be the subject or the target of one, because the rule never consulted the + # flag: + # + # H1: Rule 3's target-selection subquery. The outer join requires both sides + # to clear the floor, so the rule only fires when SOME superset qualifies -- + # but the TOP (1) subquery that actually names target_index_name ordered by + # key length then name and never looked at the floor at all. With one + # qualifying superset to make the rule fire and a below-floor one that sorts + # first, the subquery handed back the cold index. Rule 4 then marked it + # MERGE INCLUDES and Rule 6 rebuilt it: a CREATE INDEX ... DROP_EXISTING + # against an index the caller's floor said to leave alone, paired with a + # DISABLE of the subset that runs regardless. + # + # H2: Rule 7.5. Rule 7 checks meets_usage_floor before promoting an index to + # replace a unique constraint. Rule 7.5 reaches the identical outcome -- + # DISABLE the constraint, rewrite the index as UNIQUE -- through a different + # pair of UPDATEs that did not check it. Under a floor high enough to + # exclude the index, Rule 7 correctly declined and Rule 7.5 did it anyway. + # + # Both are asserted against a floor of 100, and both are preceded by a run at + # @min_reads = 0 proving the rule fires on this fixture at all. Without that + # control, "the cold index is not the target" is equally true of a fixture + # where nothing matched in the first place. + + # H1: Rule 3 must not pick a below-floor index as the merge target. + # + # No floor first. This is the control that matters most here: it proves the + # subquery's ORDER BY genuinely prefers ix_ft_cold, so a different answer + # under a floor is the floor doing its job and not some unrelated ordering. + rows, _ = run_proc(server, password, "ic_floor_target_test", extra=", @min_reads = 0") + + sub_rows = dedupe_rows(rows, "ix_ft_sub") + assert_test("H-UsageFloor", + "positive control: @min_reads = 0, ix_ft_sub is disabled as a key subset", + len(sub_rows) >= 1, + "found %d (%s)" % (len(sub_rows), [r["script_type"] for r in sub_rows])) + + cold_targeted = [r for r in sub_rows if r.get("target_index_name") == "ix_ft_cold"] + assert_test("H-UsageFloor", + "positive control: @min_reads = 0, the target IS ix_ft_cold (it sorts first)", + len(cold_targeted) >= 1, + "targets: %s" % [r.get("target_index_name") for r in sub_rows]) + + # The assertion. ix_ft_cold has 5 reads against a floor of 100, so it may not + # be named as the target no matter where the subquery's ORDER BY would land. + rows, _ = run_proc(server, password, "ic_floor_target_test", extra=", @min_reads = 100") + + sub_rows = dedupe_rows(rows, "ix_ft_sub") + assert_test("H-UsageFloor", + "positive control: @min_reads = 100, Rule 3 still fires for ix_ft_sub", + len(sub_rows) >= 1, + "found %d (%s) -- ix_ft_hot clears the floor, so the rule has a target" + % (len(sub_rows), [r["script_type"] for r in sub_rows])) + + cold_targeted = [r for r in sub_rows if r.get("target_index_name") == "ix_ft_cold"] + assert_test("H-UsageFloor", + "@min_reads = 100: below-floor ix_ft_cold is not chosen as the merge target", + len(cold_targeted) == 0, + "targets: %s (expected ix_ft_hot)" + % [r.get("target_index_name") for r in sub_rows]) + + hot_targeted = [r for r in sub_rows if r.get("target_index_name") == "ix_ft_hot"] + assert_test("H-UsageFloor", + "@min_reads = 100: the target is ix_ft_hot, the superset that clears the floor", + len(hot_targeted) >= 1, + "targets: %s" % [r.get("target_index_name") for r in sub_rows]) + + # And nothing may rebuild the cold index either -- Rule 4/6 follow whatever + # Rule 3 named, so a target that slipped through becomes a MERGE SCRIPT. + cold_rows = dedupe_rows(rows, "ix_ft_cold") + assert_test("H-UsageFloor", + "@min_reads = 100: below-floor ix_ft_cold gets no dedupe action at all", + len(cold_rows) == 0, + "found %d (%s)" % (len(cold_rows), [r["script_type"] for r in cold_rows])) + + # Positive control, same as Group B's: the floor gates dedupe only. If the fix + # worked by making the cold index invisible, this is what would catch it. + matches = find_rows(rows, index_name="ix_ft_cold", script_type="COMPRESSION SCRIPT") + assert_test("H-UsageFloor", + "@min_reads = 100: ix_ft_cold still gets COMPRESSION SCRIPT", + len(matches) == 1, "found %d" % len(matches)) + + # H2: Rule 7.5 must not promote a below-floor index over a unique constraint. + rows, _ = run_proc(server, password, "ic_floor_uc_test", extra=", @min_reads = 0") + + uc_drop = find_rows(rows, index_name="uq_floor", + script_type="DISABLE CONSTRAINT SCRIPT") + assert_test("H-UsageFloor", + "positive control: @min_reads = 0, uq_floor gets a DISABLE CONSTRAINT SCRIPT", + len(uc_drop) == 1, "found %d (Rule 7.5 fires on this fixture)" % len(uc_drop)) + + nc_promoted = find_rows(rows, index_name="ix_floor_nc", script_type="MERGE SCRIPT") + assert_test("H-UsageFloor", + "positive control: @min_reads = 0, ix_floor_nc is the MAKE UNIQUE replacement", + len(nc_promoted) == 1, "found %d" % len(nc_promoted)) + + # The assertion. ix_floor_nc has 5 reads against a floor of 100. Rule 7 + # already declined to promote it; Rule 7.5 must decline too. + rows, _ = run_proc(server, password, "ic_floor_uc_test", extra=", @min_reads = 100") + + nc_rows = dedupe_rows(rows, "ix_floor_nc") + assert_test("H-UsageFloor", + "@min_reads = 100: below-floor ix_floor_nc is not promoted to unique", + len(nc_rows) == 0, + "found %d (%s) -- a MERGE SCRIPT here rewrites an index the floor excluded" + % (len(nc_rows), [r["script_type"] for r in nc_rows])) + + # The other half of the same recommendation: the constraint must survive too. + # Dropping it while its replacement is never created leaves the uniqueness + # unenforced, and the DROP CONSTRAINT runs cleanly on its own. + uc_rows = dedupe_rows(rows, "uq_floor") + assert_test("H-UsageFloor", + "@min_reads = 100: uq_floor is not dropped in favor of a below-floor index", + len(uc_rows) == 0, + "found %d (%s)" % (len(uc_rows), [r["script_type"] for r in uc_rows])) + + matches = find_rows(rows, index_name="ix_floor_nc", script_type="COMPRESSION SCRIPT") + assert_test("H-UsageFloor", + "@min_reads = 100: ix_floor_nc still gets COMPRESSION SCRIPT", + len(matches) == 1, "found %d" % len(matches)) + + # H3: Rule 7.6 must not disable a below-floor key duplicate of the winner. + # + # This one needs no separate @min_reads = 0 run. The controls it needs all + # come from the floored run itself: Rule 7.5 has to have produced a MAKE + # UNIQUE winner, or Rule 7.6 has nothing to pair the cold index against and + # the assertion below would hold for the wrong reason. + rows, _ = run_proc(server, password, "ic_floor_dup_test", extra=", @min_reads = 100") + + uc_drop = find_rows(rows, index_name="uq_fd", + script_type="DISABLE CONSTRAINT SCRIPT") + assert_test("H-UsageFloor", + "positive control: @min_reads = 100, uq_fd still gets a DISABLE CONSTRAINT SCRIPT", + len(uc_drop) == 1, "found %d" % len(uc_drop)) + + win_rows = find_rows(rows, index_name="ix_fd_win", script_type="MERGE SCRIPT") + assert_test("H-UsageFloor", + "positive control: @min_reads = 100, ix_fd_win is the MAKE UNIQUE winner", + len(win_rows) == 1, + "found %d (Rule 7.6 needs this winner to pair against)" % len(win_rows)) + + # The assertion. ix_fd_cold is a key duplicate of that winner and sits below + # the floor, which is precisely the pairing Rule 5 already refuses. + cold_rows = dedupe_rows(rows, "ix_fd_cold") + assert_test("H-UsageFloor", + "@min_reads = 100: below-floor ix_fd_cold is not disabled as a key duplicate", + len(cold_rows) == 0, + "found %d (%s)" % (len(cold_rows), [r["script_type"] for r in cold_rows])) + + matches = find_rows(rows, index_name="ix_fd_cold", script_type="COMPRESSION SCRIPT") + assert_test("H-UsageFloor", + "@min_reads = 100: ix_fd_cold still gets COMPRESSION SCRIPT", + len(matches) == 1, "found %d" % len(matches)) + return results diff --git a/sp_PerfCheck/README.md b/sp_PerfCheck/README.md index eda676f1..8fcd7138 100644 --- a/sp_PerfCheck/README.md +++ b/sp_PerfCheck/README.md @@ -30,6 +30,8 @@ | @wait_medium_pct | decimal(38, 2) | 20.0 | A resource wait at or above this percent of uptime is Medium priority | | @memory_grant_warning | integer | 100 | Forced memory grants at or above this cumulative count are Medium | | @memory_grant_critical | integer | 10000 | Forced memory grants at or above this cumulative count are High | +| @memory_grant_timeout_warning | decimal(38, 2) | 10.0 | Memory grant timeouts above this cumulative count are Medium | +| @memory_grant_timeout_critical | decimal(38, 2) | 100.0 | Memory grant timeouts above this cumulative count are High | | @help | bit | 0 | Displays help information | | @debug | bit | 0 | Print diagnostic messages and intermediate query results | | @version | varchar(30) | NULL OUTPUT | Returns version number | @@ -132,7 +134,7 @@ respectively, so a barely-used file cannot trip them on a handful of slow sample | 4001 | Offline CPU Schedulers | Critical (10) | CPUs offline, reducing processing power | | 4101 | Memory-Starved Queries: Forced Grants | High (20) / Medium (30) / Low (40) | Graduated on @memory_grant_critical (10000) and @memory_grant_warning (100) | | 4102 | Memory Dumps Detected In Last 90 Days | Critical (10) | Server crashing in the last 90 days | -| 4103 | Memory-Starved Queries: Grant Timeouts | High (20) / Medium (30) / Low (40) | More than 100 = High, more than 10 = Medium, otherwise Low | +| 4103 | Memory-Starved Queries: Grant Timeouts | High (20) / Medium (30) / Low (40) | Graduated on @memory_grant_timeout_critical (100.0) and @memory_grant_timeout_warning (10.0) | | 4104 | Large Security Token Cache | High (20) / Medium (30) / Low (40) | Over 5 GB = High, over 2 GB = Medium, fires at 1 GB | | 4105 | Lock Pages in Memory Not Enabled | Low (40) | Best practice for servers with 32 GB or more of RAM | | 4106 | Instant File Initialization Disabled | Low (40) | Best practice for file creation and growth operations | diff --git a/sp_PerfCheck/sp_PerfCheck.sql b/sp_PerfCheck/sp_PerfCheck.sql index c1529a42..0236b3b2 100644 --- a/sp_PerfCheck/sp_PerfCheck.sql +++ b/sp_PerfCheck/sp_PerfCheck.sql @@ -65,6 +65,8 @@ ALTER PROCEDURE @wait_medium_pct decimal(38, 2) = 20.0, /* Resource wait at/above this % of uptime is Medium */ @memory_grant_warning integer = 100, /* Forced grants at/above this count are Medium */ @memory_grant_critical integer = 10000, /* Forced grants at/above this count are High */ + @memory_grant_timeout_warning decimal(38, 2) = 10.0, /* Grant timeouts above this count are Medium */ + @memory_grant_timeout_critical decimal(38, 2) = 100.0, /* Grant timeouts above this count are High */ @help bit = 0, /*For helpfulness*/ @debug bit = 0, /* Print diagnostic messages */ @version varchar(30) = NULL OUTPUT, /* Returns version */ @@ -125,6 +127,8 @@ BEGIN WHEN N'@wait_medium_pct' THEN 'a resource wait at or above this percent of uptime is Medium priority' WHEN N'@memory_grant_warning' THEN 'forced memory grants at or above this cumulative count are Medium' WHEN N'@memory_grant_critical' THEN 'forced memory grants at or above this cumulative count are High' + WHEN N'@memory_grant_timeout_warning' THEN 'memory grant timeouts above this cumulative count are Medium' + WHEN N'@memory_grant_timeout_critical' THEN 'memory grant timeouts above this cumulative count are High' ELSE NULL END, valid_inputs = @@ -142,6 +146,8 @@ BEGIN WHEN N'@wait_medium_pct' THEN 'any positive percentage' WHEN N'@memory_grant_warning' THEN 'any positive integer' WHEN N'@memory_grant_critical' THEN 'any positive integer' + WHEN N'@memory_grant_timeout_warning' THEN 'any positive number' + WHEN N'@memory_grant_timeout_critical' THEN 'any positive number' ELSE NULL END, defaults = @@ -159,6 +165,8 @@ BEGIN WHEN N'@wait_medium_pct' THEN '20.0' WHEN N'@memory_grant_warning' THEN '100' WHEN N'@memory_grant_critical' THEN '10000' + WHEN N'@memory_grant_timeout_warning' THEN '10.0' + WHEN N'@memory_grant_timeout_critical' THEN '100.0' ELSE NULL END FROM sys.all_parameters AS ap @@ -250,6 +258,18 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. WHEN @memory_grant_critical >= 0 THEN @memory_grant_critical ELSE 10000 + END, + @memory_grant_timeout_warning = + CASE + WHEN @memory_grant_timeout_warning >= 0 + THEN @memory_grant_timeout_warning + ELSE 10.0 + END, + @memory_grant_timeout_critical = + CASE + WHEN @memory_grant_timeout_critical >= 0 + THEN @memory_grant_timeout_critical + ELSE 100.0 END; /* @@ -494,8 +514,12 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. WHERE d.name = @database_name ) BEGIN + /* + Severity 11 hands control to the CATCH block at the end of the + procedure, whose THROW re-raises this to the caller, so no + RETURN is needed (or reachable) here. + */ RAISERROR(N'The database %s does not exist on this server. Check the name and try again.', 11, 1, @database_name) WITH NOWAIT; - RETURN; END; IF NOT EXISTS @@ -507,8 +531,12 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. AND d.state = 0 /* ONLINE */ ) BEGIN + /* + Severity 11 hands control to the CATCH block at the end of the + procedure, whose THROW re-raises this to the caller, so no + RETURN is needed (or reachable) here. + */ RAISERROR(N'The database %s exists but is not ONLINE, so its configuration cannot be analyzed. Run without @database_name for the server-level checks.', 11, 1, @database_name) WITH NOWAIT; - RETURN; END; END; @@ -534,19 +562,14 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. is_auto_create_stats_incremental_on bit NOT NULL, is_auto_update_stats_on bit NOT NULL, is_auto_update_stats_async_on bit NOT NULL, - is_ansi_null_default_on bit NOT NULL, is_ansi_nulls_on bit NOT NULL, - is_ansi_padding_on bit NOT NULL, is_ansi_warnings_on bit NOT NULL, - is_arithabort_on bit NOT NULL, is_concat_null_yields_null_on bit NOT NULL, - is_numeric_roundabort_on bit NOT NULL, is_quoted_identifier_on bit NOT NULL, is_parameterization_forced bit NOT NULL, is_query_store_on bit NOT NULL, is_distributor bit NOT NULL, is_cdc_enabled bit NOT NULL, - target_recovery_time_in_seconds integer NULL, delayed_durability_desc nvarchar(60) NULL, /* Nullable on purpose. A database that is closed (AUTO_CLOSE), OFFLINE, or @@ -1098,9 +1121,9 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. check_id = 4103, priority = CASE - WHEN MAX(ders.timeout_error_count) > 100 + WHEN MAX(ders.timeout_error_count) > @memory_grant_timeout_critical THEN 20 /* High: queries repeatedly failing to get memory */ - WHEN MAX(ders.timeout_error_count) > 10 + WHEN MAX(ders.timeout_error_count) > @memory_grant_timeout_warning THEN 30 /* Medium */ ELSE 40 /* Low: a few since startup, likely transient */ END, @@ -4087,19 +4110,14 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. d.is_auto_create_stats_incremental_on, d.is_auto_update_stats_on, d.is_auto_update_stats_async_on, - d.is_ansi_null_default_on, d.is_ansi_nulls_on, - d.is_ansi_padding_on, d.is_ansi_warnings_on, - d.is_arithabort_on, d.is_concat_null_yields_null_on, - d.is_numeric_roundabort_on, d.is_quoted_identifier_on, d.is_parameterization_forced, d.is_query_store_on, d.is_distributor, d.is_cdc_enabled, - d.target_recovery_time_in_seconds, d.delayed_durability_desc,'; /* Handle accelerated database recovery column */ @@ -4174,19 +4192,14 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. is_auto_create_stats_incremental_on, is_auto_update_stats_on, is_auto_update_stats_async_on, - is_ansi_null_default_on, is_ansi_nulls_on, - is_ansi_padding_on, is_ansi_warnings_on, - is_arithabort_on, is_concat_null_yields_null_on, - is_numeric_roundabort_on, is_quoted_identifier_on, is_parameterization_forced, is_query_store_on, is_distributor, is_cdc_enabled, - target_recovery_time_in_seconds, delayed_durability_desc, is_accelerated_database_recovery_on, is_ledger_on diff --git a/sp_PressureDetector/sp_PressureDetector.sql b/sp_PressureDetector/sp_PressureDetector.sql index eb14363c..94a0e0da 100644 --- a/sp_PressureDetector/sp_PressureDetector.sql +++ b/sp_PressureDetector/sp_PressureDetector.sql @@ -3540,6 +3540,11 @@ OPTION(MAXDOP 1, RECOMPILE);', (error 1222, plausible under the very plan-cache churn being diagnosed) would abort the entire procedure, losing every section after this one. Degrade to a warning instead. + + This branch only runs when @log_to_table = 0, so the caller is + a human watching the results and a severity 10 note is the + right outcome. The logging path below raises instead, because + nobody is watching an Agent job. */ BEGIN TRY EXECUTE sys.sp_executesql @@ -3614,14 +3619,23 @@ OPTION(MAXDOP 1, RECOMPILE);', PRINT @insert_sql; END; - /* Same plan-fetch TRY/CATCH as the client path above. */ + /* + Same plan-fetch TRY/CATCH as the client path above, except this + branch only runs when @log_to_table = 1. A logging failure here + (dropped table, revoked permission, schema drift) means nothing + was recorded, and severity 10 is informational: an Agent job + would swallow it and report success with an empty log. Severity + 11 returns a real error so the job step fails and somebody finds + out. Execution still continues past the RAISERROR, so the rest of + the collection is unaffected. + */ BEGIN TRY EXECUTE sys.sp_executesql @insert_sql; END TRY BEGIN CATCH SET @pd_catch_msg = ERROR_MESSAGE(); - RAISERROR(N'Memory-grant query logging skipped: %s', 10, 1, @pd_catch_msg) WITH NOWAIT; + RAISERROR(N'Memory-grant query logging skipped: %s', 11, 1, @pd_catch_msg) WITH NOWAIT; END CATCH; END; END; @@ -4370,7 +4384,11 @@ OPTION(MAXDOP 1, RECOMPILE);', IF @log_to_table = 0 BEGIN - /* Same plan-fetch TRY/CATCH as the memory-grants path. */ + /* + Same plan-fetch TRY/CATCH as the memory-grants path. Client + path only (@log_to_table = 0), so a failure stays a severity + 10 note for the human reading the results. + */ BEGIN TRY EXECUTE sys.sp_executesql @cpu_sql; @@ -4443,14 +4461,21 @@ OPTION(MAXDOP 1, RECOMPILE);', PRINT @insert_sql; END; - /* Same plan-fetch TRY/CATCH as the client path above. */ + /* + Same plan-fetch TRY/CATCH as the client path above, except + this branch only runs when @log_to_table = 1. Severity 11 + instead of 10 for the same reason as the memory-grant + logging path: an informational message lets an Agent job + report success while recording nothing, so a logging failure + has to come back as a real error. + */ BEGIN TRY EXECUTE sys.sp_executesql @insert_sql; END TRY BEGIN CATCH SET @pd_catch_msg = ERROR_MESSAGE(); - RAISERROR(N'CPU query logging skipped: %s', 10, 1, @pd_catch_msg) WITH NOWAIT; + RAISERROR(N'CPU query logging skipped: %s', 11, 1, @pd_catch_msg) WITH NOWAIT; END CATCH; END; END; /*End not skipping queries*/ diff --git a/sp_QueryReproBuilder/sp_QueryReproBuilder.sql b/sp_QueryReproBuilder/sp_QueryReproBuilder.sql index 99b37f15..6232b31f 100644 --- a/sp_QueryReproBuilder/sp_QueryReproBuilder.sql +++ b/sp_QueryReproBuilder/sp_QueryReproBuilder.sql @@ -262,7 +262,49 @@ DECLARE @new bit = 0, @current_table nvarchar(100), @include_plans_seeded bit = 0, - @match nvarchar(max) = N'' + @match nvarchar(max) = N''; + +/* +Attempt at overloading procedure name so it can accept a schema.procedure or +db.schema.procedure pasted from the results of sp_QuickieStore or another run of +this procedure. +PARSENAME copes with bracketed ([dbo].[proc]) and unbracketed (dbo.proc) forms +alike; the old LIKE '[[]%].[[]%]' pattern only matched the fully bracketed +two-part form, so an unbracketed dbo.proc fell through and was later QUOTENAME'd +whole into a single mangled [dbo.proc] identifier that could never resolve. +This runs before the @database_name default below so a three-part paste can +supply its own database, matching sp_QuickieStore. +The blank string cleanup for these two parameters happens here rather than with +the rest of the parameters further down, because the parsing keys off +@procedure_schema being NULL. +*/ +SELECT + @procedure_schema = + NULLIF(@procedure_schema, ''), + @procedure_name = + NULLIF(@procedure_name, ''); + +IF @procedure_name IS NOT NULL +AND @procedure_schema IS NULL +AND PARSENAME(@procedure_name, 2) IS NOT NULL +BEGIN + /* + A three-part db.schema.proc paste: adopt its database when the caller didn't + pass @database_name, rather than dropping it (which looked the procedure up + in the wrong database) or refusing to split it (a false 'not found'). + An explicitly passed @database_name always wins. + */ + IF PARSENAME(@procedure_name, 3) IS NOT NULL + AND @database_name IS NULL + BEGIN + SELECT + @database_name = PARSENAME(@procedure_name, 3); + END; + + SELECT + @procedure_schema = PARSENAME(@procedure_name, 2), + @procedure_name = PARSENAME(@procedure_name, 1); +END; /*Fix NULL @database_name*/ IF @@ -577,13 +619,11 @@ BEGIN END; /* -NULLIF blank strings to NULL for consistent handling +NULLIF blank strings to NULL for consistent handling. +@procedure_schema and @procedure_name are cleaned up earlier, alongside the +procedure name parsing that depends on them. */ SELECT - @procedure_schema = - NULLIF(@procedure_schema, ''), - @procedure_name = - NULLIF(@procedure_name, ''), @include_plan_ids = NULLIF(@include_plan_ids, ''), @include_query_ids = @@ -609,26 +649,6 @@ SELECT @query_text_search_not = NULLIF(@query_text_search_not, ''); -/* -Parse schema from procedure name if provided in schema.procedure format. -PARSENAME copes with bracketed ([dbo].[proc]) and unbracketed (dbo.proc) -alike; the old LIKE '[[]%].[[]%]' pattern only matched the fully bracketed -form, so an unbracketed dbo.proc fell through and was later QUOTENAME'd whole -into a single mangled [dbo.proc] identifier that could never resolve. -Only split when the caller didn't pass @procedure_schema separately, the name -has a schema part, and it's exactly two parts (part 3 NULL) so a stray -three-part name doesn't silently drop its database component here. -*/ -IF @procedure_name IS NOT NULL -AND @procedure_schema IS NULL -AND PARSENAME(@procedure_name, 2) IS NOT NULL -AND PARSENAME(@procedure_name, 3) IS NULL -BEGIN - SELECT - @procedure_schema = PARSENAME(@procedure_name, 2), - @procedure_name = PARSENAME(@procedure_name, 1); -END; - /*Initialize procedure variables*/ IF @procedure_name IS NOT NULL BEGIN diff --git a/sp_QueryReproBuilder/tests/README.md b/sp_QueryReproBuilder/tests/README.md index e2da88e5..a2ab526b 100644 --- a/sp_QueryReproBuilder/tests/README.md +++ b/sp_QueryReproBuilder/tests/README.md @@ -27,16 +27,19 @@ password). Expect `237 passed, 0 failed` and `5 of 5 mutations caught`. | File | What it does | | --- | --- | -| `run_tests.py` | 44 cases, 237 assertions. Each embeds a ShowPlanXML, drives it through `@query_plan_xml`, extracts the emitted repro, executes it, and asserts it built, is correct, and RAN. Self-contained and self-cleaning. | +| `run_tests.py` | 265 assertions. Most cases embed a ShowPlanXML, drive it through `@query_plan_xml`, extract the emitted repro, execute it, and assert it built, is correct, and RAN. The `procname:` family instead resolves real one/two/three-part `@procedure_name` values against scratch Query Store databases it creates and drops. Self-contained and self-cleaning. | | `mutation_check.py` | Plants five plausible generation bugs in a scratch copy of the procedure, installs each, and asserts `run_tests.py` goes RED on every one -- proof the suite has teeth. Restores the real build afterward. | | `template_generate.sql` | The generation half: runs the procedure in `@query_plan_xml` mode and lets its result set print so the repro can be read off stdout. Driven by `run_tests.py`. | | `template_execute.sql` | The execution half: takes the repro back (as base64) into a real `nvarchar(max)` variable and runs it with `sys.sp_executesql` inside `BEGIN TRANSACTION ... ROLLBACK` and `TRY/CATCH`. Driven by `run_tests.py`. | Everything is embedded or synthesized. There is no dependency on a captured plan -cache, on `StackOverflow2013`, or on any particular user database. Plans +cache, on `StackOverflow2013`, or on any pre-existing user database. Plans reference `sys` objects, which are always present; the single case that needs a real user table (a parameterized `UPDATE`) uses a small fixture the harness -creates in `tempdb` and drops on the way out. +creates in `tempdb` and drops on the way out. The `procname:` family is the one +exception to plan embedding: resolving `@procedure_name` means reading Query +Store in a real database, so those cases build two scratch databases with Query +Store enabled and drop them in a teardown that runs even when setup fails. ## The execute check is the one that earns its keep diff --git a/sp_QueryReproBuilder/tests/run_tests.py b/sp_QueryReproBuilder/tests/run_tests.py index 36b66c83..8eae3d27 100644 --- a/sp_QueryReproBuilder/tests/run_tests.py +++ b/sp_QueryReproBuilder/tests/run_tests.py @@ -22,6 +22,12 @@ database. Plans reference sys objects (always present); the one case that needs a real user table uses a small fixture the harness creates in tempdb and drops. +The one exception is the procname: family, which covers @procedure_name +resolution (including three-part MyDb.dbo.MyProc pastes). Resolving a name means +reading Query Store in a real database, which no synthetic plan can exercise, so +those cases build and drop two scratch databases of their own. See +run_procname_cases. + The authentic ParameterCompiledValue serializations used below (e.g. numeric as (12.50), money as ($99.9500), datetimeoffset as '... +05:30', guid as {guid'...'}) were captured from real sniffed plans on SQL Server, so the @@ -747,6 +753,296 @@ def _run_inline(server, password, sql, tag): return _run_sql_file(server, password, path) +# ------------------------------------------------------- procedure name paths + +# The @procedure_name cases can't ride the @query_plan_xml entry point the rest +# of the suite uses: resolving a name means hitting Query Store in a real +# database. So they get their own fixture, two scratch databases with Query +# Store on. qrb_pn_db owns the procedure; qrb_pn_other exists only to be the +# WRONG current database, so a pasted three-part name has to override it to +# resolve. Query Store is on in both because the precedence case has to reach +# the procedure lookup in qrb_pn_other rather than stopping at the earlier +# "Query Store isn't enabled for database" guard. +PN_DB = "qrb_pn_db" +PN_OTHER = "qrb_pn_other" + +PROCNAME_SETUP = """\ +SET NOCOUNT ON; +IF DB_ID('qrb_pn_db') IS NOT NULL +BEGIN + ALTER DATABASE qrb_pn_db SET SINGLE_USER WITH ROLLBACK IMMEDIATE; + DROP DATABASE qrb_pn_db; +END; +IF DB_ID('qrb_pn_other') IS NOT NULL +BEGIN + ALTER DATABASE qrb_pn_other SET SINGLE_USER WITH ROLLBACK IMMEDIATE; + DROP DATABASE qrb_pn_other; +END; +CREATE DATABASE qrb_pn_db; +CREATE DATABASE qrb_pn_other; +GO +ALTER DATABASE qrb_pn_db SET QUERY_STORE = ON; +ALTER DATABASE qrb_pn_db SET QUERY_STORE + (OPERATION_MODE = READ_WRITE, QUERY_CAPTURE_MODE = ALL, INTERVAL_LENGTH_MINUTES = 1); +ALTER DATABASE qrb_pn_other SET QUERY_STORE = ON; +ALTER DATABASE qrb_pn_other SET QUERY_STORE + (OPERATION_MODE = READ_WRITE, QUERY_CAPTURE_MODE = ALL, INTERVAL_LENGTH_MINUTES = 1); +GO +/* +Query Store initialization is asynchronous: on a cold instance the +desired READ_WRITE state can lead the actual state, and compiles that +happen during initialization are silently lost. Wait for the real +state in both scratch databases before running the workload, or the +resolution cases fail with zero captured rows on slow CI containers. +*/ +DECLARE + @qs_state_tries integer = 0; + +WHILE @qs_state_tries < 30 +BEGIN + IF EXISTS (SELECT 1/0 FROM qrb_pn_db.sys.database_query_store_options AS dqso WHERE dqso.actual_state_desc = N'READ_WRITE') + AND EXISTS (SELECT 1/0 FROM qrb_pn_other.sys.database_query_store_options AS dqso WHERE dqso.actual_state_desc = N'READ_WRITE') + BEGIN + BREAK; + END; + + WAITFOR DELAY '00:00:01'; + SET @qs_state_tries += 1; +END; +GO +USE qrb_pn_db; +GO +CREATE TABLE dbo.qrb_pn_table +( + id integer NOT NULL, + val integer NOT NULL, + name nvarchar(50) NULL +); +INSERT dbo.qrb_pn_table (id, val, name) VALUES (1, 10, N'a'), (2, 20, N'b'), (3, 30, N'c'); +GO +CREATE PROCEDURE dbo.qrb_pn_proc +( + @val integer, + @name nvarchar(50) +) +AS +BEGIN + SET NOCOUNT ON; + SELECT c = COUNT_BIG(*) FROM dbo.qrb_pn_table AS q WHERE q.val > @val AND q.name = @name; +END; +GO +/* +Execute, flush, and verify capture in a retry loop rather than once: +even after the state wait above, a cold Query Store can miss the first +compiles, and a single-shot check turns that latency into a hard +fixture failure. Ten more executions and a re-flush cost nothing. +*/ +DECLARE + @qs_capture_tries integer = 0, + @qs_rows bigint = 0; + +WHILE @qs_capture_tries < 10 AND @qs_rows = 0 +BEGIN + EXECUTE dbo.qrb_pn_proc @val = 5, @name = N'a'; + EXECUTE dbo.qrb_pn_proc @val = 5, @name = N'a'; + EXECUTE dbo.qrb_pn_proc @val = 5, @name = N'a'; + EXECUTE sys.sp_query_store_flush_db; + + SELECT @qs_rows = COUNT_BIG(*) + FROM qrb_pn_db.sys.query_store_query AS qsq + WHERE qsq.object_id = OBJECT_ID('qrb_pn_db.dbo.qrb_pn_proc'); + + IF @qs_rows = 0 + BEGIN + WAITFOR DELAY '00:00:01'; + END; + + SET @qs_capture_tries += 1; +END; + +SELECT marker = 'QS_ROWS:' + CONVERT(varchar(20), @qs_rows); +GO +""" + +PROCNAME_TEARDOWN = """\ +SET NOCOUNT ON; +IF DB_ID('qrb_pn_db') IS NOT NULL +BEGIN + ALTER DATABASE qrb_pn_db SET SINGLE_USER WITH ROLLBACK IMMEDIATE; + DROP DATABASE qrb_pn_db; +END; +IF DB_ID('qrb_pn_other') IS NOT NULL +BEGIN + ALTER DATABASE qrb_pn_other SET SINGLE_USER WITH ROLLBACK IMMEDIATE; + DROP DATABASE qrb_pn_other; +END; +""" + +# USE runs in its own batch, then the procedure is called unqualified so the +# sp_ prefix rule finds it in master while DB_NAME() stays the scratch database +# -- which is the whole point: the current database has to be the wrong one. +PROCNAME_TEMPLATE = """\ +SET NOCOUNT ON; +SET XACT_ABORT OFF; +USE {db}; +GO +BEGIN TRY + EXECUTE dbo.sp_QueryReproBuilder + {args}; +END TRY +BEGIN CATCH + PRINT 'PROC_ERROR: Msg ' + CONVERT(varchar(20), ERROR_NUMBER()) + + ' Lvl ' + CONVERT(varchar(20), ERROR_SEVERITY()) + + ' : ' + ERROR_MESSAGE(); +END CATCH; +""" + +# The message the procedure emits when a name resolves to no Query Store +# entries. It reports the name and database AFTER parsing, so its exact text is +# what proves which database was adopted and that the name was split. +PN_MISS = "does not appear to have any entries in Query Store for database" + +PROCNAME_CASES = [ + { + "name": "procname:three_part_adopts_database", + "db": PN_OTHER, + "args": "@procedure_name = 'qrb_pn_db.dbo.qrb_pn_proc'", + "expect_repro": True, + "note": "the pasted database wins over the current one when none was passed", + }, + { + "name": "procname:three_part_bracketed", + "db": PN_OTHER, + "args": "@procedure_name = '[qrb_pn_db].[dbo].[qrb_pn_proc]'", + "expect_repro": True, + "note": "bracketed three-part paste splits the same way", + }, + { + "name": "procname:three_part_wildcard", + "db": PN_OTHER, + "args": "@procedure_name = 'qrb_pn_db.dbo.qrb_pn_pro%'", + "expect_repro": True, + "note": "three-part name on the wildcard lookup path", + }, + { + "name": "procname:explicit_database_wins", + "db": PN_OTHER, + "args": ("@database_name = 'qrb_pn_other',\n" + " @procedure_name = 'qrb_pn_db.dbo.qrb_pn_proc'"), + "expect_repro": False, + # Matches sp_QuickieStore: a three-part name only supplies the database + # when the caller passed none, so this looks in qrb_pn_other and misses. + "expect_text": ["The stored procedure qrb_pn_proc " + PN_MISS + " qrb_pn_other"], + "note": "explicitly passed @database_name beats the pasted one", + }, + { + "name": "procname:three_part_missing_proc", + "db": PN_OTHER, + "args": "@procedure_name = 'qrb_pn_db.dbo.qrb_pn_nope'", + "expect_repro": False, + # Proves the split and the adoption without depending on any captured + # Query Store data: the message names the bare procedure and qrb_pn_db. + "expect_text": ["The stored procedure qrb_pn_nope " + PN_MISS + " qrb_pn_db"], + "note": "a three-part miss reports the parsed name and parsed database", + }, + { + "name": "procname:two_part_unchanged", + "db": PN_DB, + "args": "@procedure_name = 'dbo.qrb_pn_proc'", + "expect_repro": True, + "note": "two-part behavior must be untouched by the three-part support", + }, + { + "name": "procname:one_part_unchanged", + "db": PN_DB, + "args": "@procedure_name = 'qrb_pn_proc'", + "expect_repro": True, + "note": "one-part behavior must be untouched", + }, +] + + +def run_procname_cases(server, password, only=None, verbose=False): + """Run the @procedure_name resolution cases against their own fixture. + + Returns (results, setup_error) where results matches the plan-case shape: + (case_name, ok, label, detail). + """ + cases = PROCNAME_CASES + if only: + cases = [c for c in cases if only in c["name"]] + if not cases: + return [], None + + def bail(message): + """Setup can fail after creating one of the two databases, so tear down + on the way out rather than leaving a scratch database behind.""" + _run_inline(server, password, PROCNAME_TEARDOWN, "procname_teardown") + return [], message + + setup_out = _run_inline(server, password, PROCNAME_SETUP, "procname_setup") + setup_err = find_sql_errors(setup_out) + if setup_err: + return bail("could not build the procedure-name fixture: " + "; ".join(setup_err)) + # If Query Store captured nothing the resolution cases would fail for a + # reason that has nothing to do with name parsing, so say so plainly. + m = re.search(r"QS_ROWS:(\d+)", setup_out) + if not m or int(m.group(1)) < 1: + return bail("Query Store captured no rows for qrb_pn_db.dbo.qrb_pn_proc " + "(marker=%s); the resolution cases cannot run" % + (m.group(1) if m else "absent")) + + results = [] + try: + for case in cases: + sql = PROCNAME_TEMPLATE.format(db=case["db"], args=case["args"]) + out = _run_inline(server, password, sql, "procname_case") + mm = re.search(r"<\?_(.*?)\?>", out, re.DOTALL) + repro = mm.group(1).strip("\r\n").lstrip() if mm else None + + checks = [] + + def ck(ok, label, detail=""): + checks.append((bool(ok), label, detail)) + + ck(not find_sql_errors(out), "no severe SQL error", + str(find_sql_errors(out))) + + if case["expect_repro"]: + ck(repro is not None, "repro built for %s" % case["name"], + "no repro; output tail=%r" % out[-300:]) + ck(repro is not None and "qrb_pn_table" in repro, + "repro is the fixture procedure's query", + "repro[:200]=%r" % (repro[:200] if repro else None)) + ck(PN_MISS not in out, "no 'not in Query Store' miss reported", + "output claimed a miss") + else: + ck(repro is None, "no repro built (expected miss)", + "unexpected repro[:200]=%r" % (repro[:200] if repro else None)) + + for t in (case.get("expect_text") or []): + ck(t in out, "message present: %s" % t, + "not found; output tail=%r" % out[-400:]) + + case_failed = any(not ok for (ok, _l, _d) in checks) + for (ok, label, detail) in checks: + results.append((case["name"], ok, label, detail)) + if case_failed and not verbose: + print("FAIL %s" % case["name"]) + for (ok, label, detail) in checks: + if not ok: + print(" - %s :: %s" % (label, detail)) + elif not case_failed: + print("PASS %s" % case["name"]) + if verbose: + for (ok, label, _d) in checks: + print(" - %s" % label) + finally: + _run_inline(server, password, PROCNAME_TEARDOWN, "procname_teardown") + + return results, None + + # ------------------------------------------------------------------ main def main(): @@ -810,6 +1106,13 @@ def main(): finally: _run_inline(args.server, args.password, FIXTURE_TEARDOWN, "fixture_teardown") + procname_results, procname_setup_error = run_procname_cases( + args.server, args.password, only=args.only, verbose=args.verbose) + if procname_setup_error: + print("ERROR: " + procname_setup_error) + sys.exit(1) + results.extend(procname_results) + passed = sum(1 for (_n, ok, _l, _d) in results if ok) failed = sum(1 for (_n, ok, _l, _d) in results if not ok)