#!/usr/bin/env bash
# 盈尺（Surinch）SQL Server 参数嗅探演示脚本 v1.0（2026-10-23，在 SQL Server 2022 CU27-GDR 16.0.4305.5 Developer 容器上跑通）
# 演示：同一个存储过程，先用小客户编译 -> 大客户 153 万逻辑读；先用大客户编译 -> 小客户也扫全表；
#       兼容级别 160 的 PSP 为什么没触发（扩展事件看 skipped reason）、倾斜 20 万倍时 PSP 才拆变体；
#       OPTION(RECOMPILE) / OPTIMIZE FOR UNKNOWN / Query Store 提示 / 覆盖索引 四种处理的实测。
# !!! 只能在测试实例上跑：会 DROP/CREATE 数据库 sniff_lab、清该库计划缓存、建/删服务器级扩展事件会话 psp_lab、改兼容级别。
# 用法（容器内 sqlcmd 示例）：
#   SQLCMD="docker exec -i mssql-lab /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P '<测试密码>' -C" bash sqlserver-parameter-sniffing-demo-v1.0.sh | tee demo.txt
# 输出很长，看关键行：grep -E "^#####|===|logical reads|elapsed time = [1-9]|reason|dispatcher" demo.txt
# 相关文章：https://surinch.com/resources/sql-server-parameter-sniffing-psp-recompile-query-store-hints
set -euo pipefail
: "${SQLCMD:?请设置 SQLCMD，例如 docker exec -i mssql-lab /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P xxx -C}"
run(){ local name=$1; shift; echo "##### $name"; eval "$SQLCMD -I -W $*"; }

run init  <<'TSQL'
SET NOCOUNT ON;
SELECT @@VERSION AS version;
IF DB_ID('sniff_lab') IS NOT NULL BEGIN ALTER DATABASE sniff_lab SET SINGLE_USER WITH ROLLBACK IMMEDIATE; DROP DATABASE sniff_lab; END
CREATE DATABASE sniff_lab;
GO
USE sniff_lab;
ALTER DATABASE sniff_lab SET RECOVERY SIMPLE;
ALTER DATABASE sniff_lab SET QUERY_STORE = ON (OPERATION_MODE = READ_WRITE, QUERY_CAPTURE_MODE = ALL);
SELECT name, compatibility_level, is_query_store_on FROM sys.databases WHERE name='sniff_lab';
CREATE TABLE dbo.orders(
  id INT IDENTITY PRIMARY KEY,
  customer_id INT NOT NULL,
  status TINYINT NOT NULL,
  amount DECIMAL(10,2) NOT NULL,
  created_at DATETIME2(0) NOT NULL,
  pad CHAR(200) NOT NULL DEFAULT 'x');
-- 自造数据：1,000,000 行；customer_id=1 是"大客户"占 50 万行，其余 10,000 个客户每人 50 行
;WITH n AS (SELECT TOP (1000000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS i FROM sys.all_objects a CROSS JOIN sys.all_objects b CROSS JOIN sys.all_objects c)
INSERT INTO dbo.orders(customer_id,status,amount,created_at)
SELECT CASE WHEN i <= 500000 THEN 1 ELSE 2 + (i % 10000) END, i % 5, (i % 9000)/10.0 + 10, DATEADD(SECOND, i*20, '2026-01-01')
FROM n;
CREATE INDEX ix_orders_customer ON dbo.orders(customer_id);
SELECT COUNT(*) total_rows, SUM(CASE WHEN customer_id=1 THEN 1 ELSE 0 END) big_customer_rows, COUNT(DISTINCT customer_id) customers FROM dbo.orders;
EXEC sp_spaceused 'dbo.orders';
GO
CREATE OR ALTER PROCEDURE dbo.customer_summary @cid INT AS
BEGIN
  SET NOCOUNT ON;
  SELECT COUNT(*) AS orders_cnt, SUM(amount) AS total_amount, MAX(created_at) AS last_order
  FROM dbo.orders WHERE customer_id = @cid;
END
GO
TSQL

run round_cid12_first -v COMPAT=160 FIRST=12 <<'TSQL'
-- 用法：sqlcmd -v COMPAT=160 FIRST=12 -i ...   FIRST 为第一次编译时传入的 customer_id
SET NOCOUNT ON;
USE sniff_lab;
ALTER DATABASE sniff_lab SET COMPATIBILITY_LEVEL = $(COMPAT);
ALTER DATABASE SCOPED CONFIGURATION CLEAR PROCEDURE_CACHE;
SELECT 'compat' k, compatibility_level v FROM sys.databases WHERE name='sniff_lab';
GO
PRINT '=== first compile with cid=$(FIRST)';
SET STATISTICS TIME ON; SET STATISTICS IO ON;
EXEC dbo.customer_summary @cid = $(FIRST);
SET STATISTICS TIME OFF; SET STATISTICS IO OFF;
GO
PRINT '=== cid=12 (50 rows) x3';
SET STATISTICS TIME ON; SET STATISTICS IO ON;
EXEC dbo.customer_summary @cid = 12;
EXEC dbo.customer_summary @cid = 12;
EXEC dbo.customer_summary @cid = 12;
SET STATISTICS TIME OFF; SET STATISTICS IO OFF;
GO
PRINT '=== cid=1 (500000 rows) x3';
SET STATISTICS TIME ON; SET STATISTICS IO ON;
EXEC dbo.customer_summary @cid = 1;
EXEC dbo.customer_summary @cid = 1;
EXEC dbo.customer_summary @cid = 1;
SET STATISTICS TIME OFF; SET STATISTICS IO OFF;
GO
PRINT '=== cached plans for the proc statement';
SELECT qs.plan_handle, qs.execution_count, qs.total_logical_reads/qs.execution_count AS avg_reads,
       qs.total_elapsed_time/qs.execution_count/1000.0 AS avg_ms,
       STUFF((SELECT DISTINCT ', ' + r.value('@PhysicalOp','varchar(60)') FROM qp.query_plan.nodes('declare namespace p="http://schemas.microsoft.com/sqlserver/2004/07/showplan"; //p:RelOp') x(r) FOR XML PATH('')),1,2,'') AS ops,
       CASE WHEN CAST(qp.query_plan AS nvarchar(max)) LIKE '%Dispatcher%' THEN 'dispatcher' ELSE '' END AS psp,
       CASE WHEN CAST(qp.query_plan AS nvarchar(max)) LIKE '%PLAN PER VALUE%' THEN 'variant' ELSE '' END AS variant
FROM sys.dm_exec_query_stats qs CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) st CROSS APPLY sys.dm_exec_query_plan(qs.plan_handle) qp
WHERE st.text LIKE '%FROM dbo.orders WHERE customer_id%' AND st.text NOT LIKE '%dm_exec%';
GO
TSQL

run round_cid1_first -v COMPAT=160 FIRST=1 <<'TSQL'
-- 用法：sqlcmd -v COMPAT=160 FIRST=12 -i ...   FIRST 为第一次编译时传入的 customer_id
SET NOCOUNT ON;
USE sniff_lab;
ALTER DATABASE sniff_lab SET COMPATIBILITY_LEVEL = $(COMPAT);
ALTER DATABASE SCOPED CONFIGURATION CLEAR PROCEDURE_CACHE;
SELECT 'compat' k, compatibility_level v FROM sys.databases WHERE name='sniff_lab';
GO
PRINT '=== first compile with cid=$(FIRST)';
SET STATISTICS TIME ON; SET STATISTICS IO ON;
EXEC dbo.customer_summary @cid = $(FIRST);
SET STATISTICS TIME OFF; SET STATISTICS IO OFF;
GO
PRINT '=== cid=12 (50 rows) x3';
SET STATISTICS TIME ON; SET STATISTICS IO ON;
EXEC dbo.customer_summary @cid = 12;
EXEC dbo.customer_summary @cid = 12;
EXEC dbo.customer_summary @cid = 12;
SET STATISTICS TIME OFF; SET STATISTICS IO OFF;
GO
PRINT '=== cid=1 (500000 rows) x3';
SET STATISTICS TIME ON; SET STATISTICS IO ON;
EXEC dbo.customer_summary @cid = 1;
EXEC dbo.customer_summary @cid = 1;
EXEC dbo.customer_summary @cid = 1;
SET STATISTICS TIME OFF; SET STATISTICS IO OFF;
GO
PRINT '=== cached plans for the proc statement';
SELECT qs.plan_handle, qs.execution_count, qs.total_logical_reads/qs.execution_count AS avg_reads,
       qs.total_elapsed_time/qs.execution_count/1000.0 AS avg_ms,
       STUFF((SELECT DISTINCT ', ' + r.value('@PhysicalOp','varchar(60)') FROM qp.query_plan.nodes('declare namespace p="http://schemas.microsoft.com/sqlserver/2004/07/showplan"; //p:RelOp') x(r) FOR XML PATH('')),1,2,'') AS ops,
       CASE WHEN CAST(qp.query_plan AS nvarchar(max)) LIKE '%Dispatcher%' THEN 'dispatcher' ELSE '' END AS psp,
       CASE WHEN CAST(qp.query_plan AS nvarchar(max)) LIKE '%PLAN PER VALUE%' THEN 'variant' ELSE '' END AS variant
FROM sys.dm_exec_query_stats qs CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) st CROSS APPLY sys.dm_exec_query_plan(qs.plan_handle) qp
WHERE st.text LIKE '%FROM dbo.orders WHERE customer_id%' AND st.text NOT LIKE '%dm_exec%';
GO
TSQL

run psp_skipped_reason  <<'TSQL'
SET NOCOUNT ON;
IF EXISTS (SELECT 1 FROM sys.server_event_sessions WHERE name='psp_lab') DROP EVENT SESSION psp_lab ON SERVER;
CREATE EVENT SESSION psp_lab ON SERVER
 ADD EVENT sqlserver.parameter_sensitive_plan_optimization_skipped_reason,
 ADD EVENT sqlserver.parameter_sensitive_plan_optimization
 ADD TARGET package0.ring_buffer;
ALTER EVENT SESSION psp_lab ON SERVER STATE = START;
GO
USE sniff_lab;
ALTER DATABASE sniff_lab SET COMPATIBILITY_LEVEL = 160;
ALTER DATABASE SCOPED CONFIGURATION CLEAR PROCEDURE_CACHE;
EXEC dbo.customer_summary @cid = 12;
GO
WAITFOR DELAY '00:00:02';
SELECT x.e.value('@name','varchar(80)') AS event_name,
       x.e.value('(data[@name="reason"]/text)[1]','varchar(80)') AS reason,
       x.e.value('(data[@name="reason"]/value)[1]','varchar(20)') AS reason_code
FROM (SELECT CAST(t.target_data AS xml) AS d FROM sys.dm_xe_session_targets t JOIN sys.dm_xe_sessions s ON s.address=t.event_session_address WHERE s.name='psp_lab') q
CROSS APPLY q.d.nodes('//event') x(e);
SELECT name, value FROM sys.database_scoped_configurations WHERE name LIKE 'PARAMETER_SENSITIVE%';
DBCC SHOW_STATISTICS ('dbo.orders','ix_orders_customer') WITH STAT_HEADER;
DBCC SHOW_STATISTICS ('dbo.orders','ix_orders_customer') WITH HISTOGRAM;
GO
TSQL

run psp_skew_table  <<'TSQL'
SET NOCOUNT ON;
USE sniff_lab;
IF OBJECT_ID('dbo.orders_skew') IS NOT NULL DROP TABLE dbo.orders_skew;
CREATE TABLE dbo.orders_skew(id INT IDENTITY PRIMARY KEY, customer_id INT NOT NULL, amount DECIMAL(10,2) NOT NULL, created_at DATETIME2(0) NOT NULL, pad CHAR(200) NOT NULL DEFAULT 'x');
-- 自造数据：1,100,000 行；customer_id=1 占 1,000,000 行，其余 20,000 个客户每人 5 行（最多/最少 = 200,000 倍）
;WITH n AS (SELECT TOP (1100000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS i FROM sys.all_objects a CROSS JOIN sys.all_objects b CROSS JOIN sys.all_objects c)
INSERT INTO dbo.orders_skew(customer_id,amount,created_at)
SELECT CASE WHEN i <= 1000000 THEN 1 ELSE 2 + (i % 20000) END, (i % 9000)/10.0 + 10, DATEADD(SECOND, i*20, '2026-01-01') FROM n;
CREATE INDEX ix_skew_customer ON dbo.orders_skew(customer_id);
GO
CREATE OR ALTER PROCEDURE dbo.customer_summary_skew @cid INT AS
BEGIN
  SET NOCOUNT ON;
  SELECT COUNT(*) AS orders_cnt, SUM(amount) AS total_amount, MAX(created_at) AS last_order
  FROM dbo.orders_skew WHERE customer_id = @cid;
END
GO
IF EXISTS (SELECT 1 FROM sys.server_event_sessions WHERE name='psp_lab') DROP EVENT SESSION psp_lab ON SERVER;
CREATE EVENT SESSION psp_lab ON SERVER ADD EVENT sqlserver.parameter_sensitive_plan_optimization_skipped_reason, ADD EVENT sqlserver.parameter_sensitive_plan_optimization ADD TARGET package0.ring_buffer;
ALTER EVENT SESSION psp_lab ON SERVER STATE = START;
GO
ALTER DATABASE SCOPED CONFIGURATION CLEAR PROCEDURE_CACHE;
PRINT '=== skew table: first cid=12, then cid=1';
SET STATISTICS TIME ON; SET STATISTICS IO ON;
EXEC dbo.customer_summary_skew @cid = 12;
EXEC dbo.customer_summary_skew @cid = 12;
EXEC dbo.customer_summary_skew @cid = 1;
EXEC dbo.customer_summary_skew @cid = 1;
EXEC dbo.customer_summary_skew @cid = 1;
EXEC dbo.customer_summary_skew @cid = 12;
SET STATISTICS TIME OFF; SET STATISTICS IO OFF;
GO
WAITFOR DELAY '00:00:02';
SELECT x.e.value('@name','varchar(80)') AS event_name, x.e.value('(data[@name="reason"]/text)[1]','varchar(80)') AS reason
FROM (SELECT CAST(t.target_data AS xml) AS d FROM sys.dm_xe_session_targets t JOIN sys.dm_xe_sessions s ON s.address=t.event_session_address WHERE s.name='psp_lab') q CROSS APPLY q.d.nodes('//event') x(e);
SELECT LEFT(st.text,80) AS stmt, qs.execution_count, qs.total_logical_reads/qs.execution_count AS avg_reads,
  STUFF((SELECT DISTINCT ', ' + r.value('@PhysicalOp','varchar(60)') FROM qp.query_plan.nodes('declare namespace p="http://schemas.microsoft.com/sqlserver/2004/07/showplan"; //p:RelOp') x(r) FOR XML PATH('')),1,2,'') AS ops,
  CASE WHEN CAST(qp.query_plan AS nvarchar(max)) LIKE '%Dispatcher%' THEN 'dispatcher' ELSE '' END AS psp,
  CASE WHEN st.text LIKE '%PLAN PER VALUE%' THEN 'variant' ELSE '' END AS variant
FROM sys.dm_exec_query_stats qs CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) st CROSS APPLY sys.dm_exec_query_plan(qs.plan_handle) qp
WHERE st.text LIKE '%FROM dbo.orders_skew WHERE customer_id%' AND st.text NOT LIKE '%dm_exec%';
SELECT * FROM sys.query_store_query_variant;
GO
TSQL

run fixes  <<'TSQL'
SET NOCOUNT ON;
USE sniff_lab;
ALTER DATABASE sniff_lab SET COMPATIBILITY_LEVEL = 160;
GO
CREATE OR ALTER PROCEDURE dbo.customer_summary_recompile @cid INT AS
BEGIN SET NOCOUNT ON;
  SELECT COUNT(*) AS orders_cnt, SUM(amount) AS total_amount, MAX(created_at) AS last_order FROM dbo.orders WHERE customer_id = @cid OPTION (RECOMPILE);
END
GO
CREATE OR ALTER PROCEDURE dbo.customer_summary_unknown @cid INT AS
BEGIN SET NOCOUNT ON;
  SELECT COUNT(*) AS orders_cnt, SUM(amount) AS total_amount, MAX(created_at) AS last_order FROM dbo.orders WHERE customer_id = @cid OPTION (OPTIMIZE FOR UNKNOWN);
END
GO
ALTER DATABASE SCOPED CONFIGURATION CLEAR PROCEDURE_CACHE;
PRINT '=== F0 baseline plan cache (cid=12 first, then cid=1 x1)';
EXEC dbo.customer_summary @cid = 12; EXEC dbo.customer_summary @cid = 1;
SELECT 'F0' tag, qs.execution_count, qs.total_logical_reads/qs.execution_count AS avg_reads, qs.max_logical_reads, qs.max_elapsed_time/1000.0 AS max_ms,
  STUFF((SELECT DISTINCT ', ' + r.value('@PhysicalOp','varchar(60)') FROM qp.query_plan.nodes('declare namespace p="http://schemas.microsoft.com/sqlserver/2004/07/showplan"; //p:RelOp') x(r) FOR XML PATH('')),1,2,'') AS ops,
  qp.query_plan.value('declare namespace p="http://schemas.microsoft.com/sqlserver/2004/07/showplan"; (//p:ColumnReference[@Column="@cid"]/@ParameterCompiledValue)[1]','varchar(20)') AS compiled_value
FROM sys.dm_exec_query_stats qs CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) st CROSS APPLY sys.dm_exec_query_plan(qs.plan_handle) qp
WHERE st.objectid = OBJECT_ID('dbo.customer_summary');
GO
PRINT '=== F1 OPTION(RECOMPILE)';
SET STATISTICS TIME ON; SET STATISTICS IO ON;
EXEC dbo.customer_summary_recompile @cid = 12; EXEC dbo.customer_summary_recompile @cid = 12;
EXEC dbo.customer_summary_recompile @cid = 1; EXEC dbo.customer_summary_recompile @cid = 1;
SET STATISTICS TIME OFF; SET STATISTICS IO OFF;
GO
PRINT '=== F2 OPTIMIZE FOR UNKNOWN';
SET STATISTICS TIME ON; SET STATISTICS IO ON;
EXEC dbo.customer_summary_unknown @cid = 12; EXEC dbo.customer_summary_unknown @cid = 1; EXEC dbo.customer_summary_unknown @cid = 1;
SET STATISTICS TIME OFF; SET STATISTICS IO OFF;
GO
PRINT '=== F3 Query Store hint on original proc (no code change)';
EXEC sys.sp_query_store_flush_db;
DECLARE @qid BIGINT = (SELECT TOP 1 q.query_id FROM sys.query_store_query q JOIN sys.query_store_query_text t ON t.query_text_id=q.query_text_id WHERE q.object_id = OBJECT_ID('dbo.customer_summary') ORDER BY q.query_id DESC);
SELECT 'qs_query_id' k, @qid v;
EXEC sys.sp_query_store_set_hints @query_id = @qid, @query_hints = N'OPTION(RECOMPILE)';
SELECT query_id, query_hint_text FROM sys.query_store_query_hints;
GO
ALTER DATABASE SCOPED CONFIGURATION CLEAR PROCEDURE_CACHE;
SET STATISTICS TIME ON; SET STATISTICS IO ON;
EXEC dbo.customer_summary @cid = 12; EXEC dbo.customer_summary @cid = 1; EXEC dbo.customer_summary @cid = 1; EXEC dbo.customer_summary @cid = 12;
SET STATISTICS TIME OFF; SET STATISTICS IO OFF;
GO
DECLARE @qid BIGINT = (SELECT TOP 1 query_id FROM sys.query_store_query_hints);
EXEC sys.sp_query_store_clear_hints @query_id = @qid;
GO
PRINT '=== F4 covering index INCLUDE(amount, created_at), original proc, cid=12 first';
CREATE INDEX ix_orders_customer_cover ON dbo.orders(customer_id) INCLUDE(amount, created_at);
ALTER DATABASE SCOPED CONFIGURATION CLEAR PROCEDURE_CACHE;
SET STATISTICS TIME ON; SET STATISTICS IO ON;
EXEC dbo.customer_summary @cid = 12; EXEC dbo.customer_summary @cid = 1; EXEC dbo.customer_summary @cid = 1; EXEC dbo.customer_summary @cid = 1;
SET STATISTICS TIME OFF; SET STATISTICS IO OFF;
EXEC sp_spaceused 'dbo.orders';
GO
SELECT 'F4' tag, qs.execution_count, qs.total_logical_reads/qs.execution_count AS avg_reads,
  STUFF((SELECT DISTINCT ', ' + r.value('@PhysicalOp','varchar(60)') FROM qp.query_plan.nodes('declare namespace p="http://schemas.microsoft.com/sqlserver/2004/07/showplan"; //p:RelOp') x(r) FOR XML PATH('')),1,2,'') AS ops
FROM sys.dm_exec_query_stats qs CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) st CROSS APPLY sys.dm_exec_query_plan(qs.plan_handle) qp
WHERE st.objectid = OBJECT_ID('dbo.customer_summary');
DROP INDEX ix_orders_customer_cover ON dbo.orders;
GO
TSQL

run batch_mode_and_cover  <<'TSQL'
SET NOCOUNT ON;
USE sniff_lab;
GO
PRINT '=== G1 scan plan (cid=1 compiled) execution modes';
ALTER DATABASE SCOPED CONFIGURATION CLEAR PROCEDURE_CACHE;
EXEC dbo.customer_summary @cid = 1;
SELECT r.value('@PhysicalOp','varchar(40)') op, r.value('@EstimatedExecutionMode','varchar(10)') mode, r.value('@Parallel','varchar(5)') par
FROM sys.dm_exec_query_stats qs CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) st CROSS APPLY sys.dm_exec_query_plan(qs.plan_handle) qp
CROSS APPLY qp.query_plan.nodes('declare namespace p="http://schemas.microsoft.com/sqlserver/2004/07/showplan"; //p:RelOp') x(r)
WHERE st.objectid = OBJECT_ID('dbo.customer_summary');
GO
PRINT '=== G2 scan plan with batch mode off (query hint via QS not needed; use ad-hoc copy)';
SET STATISTICS TIME ON; SET STATISTICS IO ON;
SELECT COUNT(*), SUM(amount), MAX(created_at) FROM dbo.orders WHERE customer_id = 1 OPTION (USE HINT('DISALLOW_BATCH_MODE'));
SELECT COUNT(*), SUM(amount), MAX(created_at) FROM dbo.orders WHERE customer_id = 1 OPTION (USE HINT('DISALLOW_BATCH_MODE'));
SELECT COUNT(*), SUM(amount), MAX(created_at) FROM dbo.orders WHERE customer_id = 1 OPTION (MAXDOP 1);
SELECT COUNT(*), SUM(amount), MAX(created_at) FROM dbo.orders WHERE customer_id = 1 OPTION (MAXDOP 1);
SET STATISTICS TIME OFF; SET STATISTICS IO OFF;
GO
PRINT '=== G3 covering index, compiled for cid=1';
CREATE INDEX ix_orders_customer_cover ON dbo.orders(customer_id) INCLUDE(amount, created_at);
ALTER DATABASE SCOPED CONFIGURATION CLEAR PROCEDURE_CACHE;
SET STATISTICS TIME ON; SET STATISTICS IO ON;
EXEC dbo.customer_summary @cid = 1; EXEC dbo.customer_summary @cid = 1; EXEC dbo.customer_summary @cid = 12;
SET STATISTICS TIME OFF; SET STATISTICS IO OFF;
SELECT r.value('@PhysicalOp','varchar(40)') op, r.value('@EstimatedExecutionMode','varchar(10)') mode, r.value('@Parallel','varchar(5)') par
FROM sys.dm_exec_query_stats qs CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) st CROSS APPLY sys.dm_exec_query_plan(qs.plan_handle) qp
CROSS APPLY qp.query_plan.nodes('declare namespace p="http://schemas.microsoft.com/sqlserver/2004/07/showplan"; //p:RelOp') x(r)
WHERE st.objectid = OBJECT_ID('dbo.customer_summary');
DROP INDEX ix_orders_customer_cover ON dbo.orders;
GO
TSQL

echo "##### cleanup 扩展事件会话"
eval "$SQLCMD -I -W" <<'TSQL'
IF EXISTS (SELECT 1 FROM sys.server_event_sessions WHERE name='psp_lab') DROP EVENT SESSION psp_lab ON SERVER;
GO
TSQL
echo "完成。清理测试库：DROP DATABASE sniff_lab;"
