Microsoft SQL Server 2008技术内幕:T-SQL查询---------查询优化
示例数据库:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
-- Listing 4-1: Creation Script for Sample Database and Tables SET NOCOUNT ON; USE master; IF DB_ID('Performance') IS NULL CREATE DATABASE Performance; GO USE Performance; GO -- Creating and Populating the Nums Auxiliary Table SET NOCOUNT ON; IF OBJECT_ID('dbo.Nums', 'U') IS NOT NULL DROP TABLE dbo.Nums; CREATE TABLE dbo.Nums(n INT NOT NULL PRIMARY KEY); DECLARE @max AS INT, @rc AS INT; SET @max = 1000000; SET @rc = 1; INSERT INTO dbo.Nums(n) VALUES(1); WHILE @rc * 2 <= @max BEGIN INSERT INTO dbo.Nums(n) SELECT n + @rc FROM dbo.Nums; SET @rc = @rc * 2; END INSERT INTO dbo.Nums(n) SELECT n + @rc FROM dbo.Nums WHERE n + @rc <= @max; GO -- Drop Data Tables if Exist IF OBJECT_ID('dbo.EmpOrders', 'V') IS NOT NULL DROP VIEW dbo.EmpOrders; GO IF OBJECT_ID('dbo.Orders', 'U') IS NOT NULL DROP TABLE dbo.Orders; GO IF OBJECT_ID('dbo.Customers', 'U') IS NOT NULL DROP TABLE dbo.Customers; GO IF OBJECT_ID('dbo.Employees', 'U') IS NOT NULL DROP TABLE dbo.Employees; GO IF OBJECT_ID('dbo.Shippers', 'U') IS NOT NULL DROP TABLE dbo.Shippers; GO -- Data Distribution Settings DECLARE @numorders AS INT, @numcusts AS INT, @numemps AS INT, @numshippers AS INT, @numyears AS INT, @startdate AS DATETIME; SELECT @numorders = 1000000, @numcusts = 20000, @numemps = 500, @numshippers = 5, @numyears = 4, @startdate = '20050101'; -- Creating and Populating the Customers Table CREATE TABLE dbo.Customers ( custid CHAR(11) NOT NULL, custname NVARCHAR(50) NOT NULL ); INSERT INTO dbo.Customers(custid, custname) SELECT 'C' + RIGHT('000000000' + CAST(n AS VARCHAR(10)), 10) AS custid, N'Cust_' + CAST(n AS VARCHAR(10)) AS custname FROM dbo.Nums WHERE n <= @numcusts; ALTER TABLE dbo.Customers ADD CONSTRAINT PK_Customers PRIMARY KEY(custid); -- Creating and Populating the Employees Table CREATE TABLE dbo.Employees ( empid INT NOT NULL, firstname NVARCHAR(25) NOT NULL, lastname NVARCHAR(25) NOT NULL ); INSERT INTO dbo.Employees(empid, firstname, lastname) SELECT n AS empid, N'Fname_' + CAST(n AS NVARCHAR(10)) AS firstname, N'Lname_' + CAST(n AS NVARCHAR(10)) AS lastname FROM dbo.Nums WHERE n <= @numemps; ALTER TABLE dbo.Employees ADD CONSTRAINT PK_Employees PRIMARY KEY(empid); -- Creating and Populating the Shippers Table CREATE TABLE dbo.Shippers ( shipperid VARCHAR(5) NOT NULL, shippername NVARCHAR(50) NOT NULL ); INSERT INTO dbo.Shippers(shipperid, shippername) SELECT shipperid, N'Shipper_' + shipperid AS shippername FROM (SELECT CHAR(ASCII('A') - 2 + 2 * n) AS shipperid FROM dbo.Nums WHERE n <= @numshippers) AS D; ALTER TABLE dbo.Shippers ADD CONSTRAINT PK_Shippers PRIMARY KEY(shipperid); -- Creating and Populating the Orders Table CREATE TABLE dbo.Orders ( orderid INT NOT NULL, custid CHAR(11) NOT NULL, empid INT NOT NULL, shipperid VARCHAR(5) NOT NULL, orderdate DATETIME NOT NULL, filler CHAR(155) NOT NULL DEFAULT('a') ); INSERT INTO dbo.Orders(orderid, custid, empid, shipperid, orderdate) SELECT n AS orderid, 'C' + RIGHT('000000000' + CAST( 1 + ABS(CHECKSUM(NEWID())) % @numcusts AS VARCHAR(10)), 10) AS custid, 1 + ABS(CHECKSUM(NEWID())) % @numemps AS empid, CHAR(ASCII('A') - 2 + 2 * (1 + ABS(CHECKSUM(NEWID())) % @numshippers)) AS shipperid, DATEADD(day, n / (@numorders / (@numyears * 365.25)), @startdate) -- late arrival with earlier date - CASE WHEN n % 10 = 0 THEN 1 + ABS(CHECKSUM(NEWID())) % 30 ELSE 0 END AS orderdate FROM dbo.Nums WHERE n <= @numorders ORDER BY CHECKSUM(NEWID()); CREATE CLUSTERED INDEX idx_cl_od ON dbo.Orders(orderdate); CREATE NONCLUSTERED INDEX idx_nc_sid_od_i_cid ON dbo.Orders(shipperid, orderdate) INCLUDE(custid); CREATE UNIQUE INDEX idx_unc_od_oid_i_cid_eid ON dbo.Orders(orderdate, orderid) INCLUDE(custid, empid); ALTER TABLE dbo.Orders ADD CONSTRAINT PK_Orders PRIMARY KEY NONCLUSTERED(orderid), CONSTRAINT FK_Orders_Customers FOREIGN KEY(custid) REFERENCES dbo.Customers(custid), CONSTRAINT FK_Orders_Employees FOREIGN KEY(empid) REFERENCES dbo.Employees(empid), CONSTRAINT FK_Orders_Shippers FOREIGN KEY(shipperid) REFERENCES dbo.Shippers(shipperid); GO
快速建成1--100000数值表的语句
DECLARE @max AS INT, @rc AS INT;
SET @max = 1000000;
SET @rc = 1;
INSERT INTO dbo.Nums(n) VALUES(1);
WHILE @rc * 2 <= @max
BEGIN
INSERT INTO dbo.Nums(n) SELECT n + @rc FROM dbo.Nums;
SET @rc = @rc * 2;
END
INSERT INTO dbo.Nums(n)
SELECT n + @rc FROM dbo.Nums WHERE n + @rc <= @max;
GO
生成随机custId,empId,shipperid 的语句
1+ABS(CHECKSUM(NEWID())%@NUMBERMAX 生成 1--->@NUMBERMAX内随机整数
INSERT INTO dbo.Orders(orderid, custid, empid, shipperid, orderdate)
SELECT n AS orderid,
'C' + RIGHT('000000000'
+ CAST(
1 + ABS(CHECKSUM(NEWID())) % @numcusts
AS VARCHAR(10)), 10) AS custid,
1 + ABS(CHECKSUM(NEWID())) % @numemps AS empid,
CHAR(ASCII('A') - 2
+ 2 * (1 + ABS(CHECKSUM(NEWID())) % @numshippers)) AS shipperid,
DATEADD(day, n / (@numorders / (@numyears * 365.25)), @startdate)
-- late arrival with earlier date
---在这个例子中是生成随机数是四年,四年有只有一个闰年,平均每年365.25天,这样跟据订单号可以算出每个订单大约放在哪一天,每个订单号为10倍的时候作了一些处理,+-31天,所以10倍的订单号有可能出现在上个月
- CASE WHEN n % 10 = 0
THEN 1 + ABS(CHECKSUM(NEWID())) % 30
ELSE 0
END AS orderdate
FROM dbo.Nums
WHERE n <= @numorders
ORDER BY CHECKSUM(NEWID());
优化方法论
返回系统里的等待信息
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 SELECT 2 wait_type, 3 waiting_tasks_count, 4 wait_time_ms, 5 max_wait_time_ms, 6 signal_wait_time_ms 7 FROM sys.dm_os_wait_stats 8 ORDER BY wait_type;
signal_wait_time_ms 表示从线程收到资源可用信号开始到得到CPU时间,开始资源为止经历的时间,如果这个属性很高,就表明CPU有问题.
分离重量级的等待
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
-- Isolate top waits WITH Waits AS ( SELECT wait_type, wait_time_ms / 1000. AS wait_time_s, 100. * wait_time_ms / SUM(wait_time_ms) OVER() AS pct, ROW_NUMBER() OVER(ORDER BY wait_time_ms DESC) AS rn, 100. * signal_wait_time_ms / wait_time_ms as signal_pct FROM sys.dm_os_wait_stats WHERE wait_time_ms > 0 AND wait_type NOT LIKE N'%SLEEP%' AND wait_type NOT LIKE N'%IDLE%' AND wait_type NOT LIKE N'%QUEUE%' AND wait_type NOT IN( N'CLR_AUTO_EVENT' , N'REQUEST_FOR_DEADLOCK_SEARCH' , N'SQLTRACE_BUFFER_FLUSH' /* filter out additional irrelevant waits */ ) ) SELECT W1.wait_type, CAST(W1.wait_time_s AS NUMERIC(12, 2)) AS wait_time_s, CAST(W1.pct AS NUMERIC(5, 2)) AS pct, CAST(SUM(W2.pct) AS NUMERIC(5, 2)) AS running_pct, CAST(W1.signal_pct AS NUMERIC(5, 2)) AS signal_pct FROM Waits AS W1 JOIN Waits AS W2 ON W2.rn <= W1.rn GROUP BY W1.rn, W1.wait_type, W1.wait_time_s, W1.pct, W1.signal_pct HAVING SUM(W2.pct) - W1.pct < 80 -- percentage threshold OR W1.rn <= 5 ORDER BY W1.rn; GO
这个语句返回累计等待时间(从最大的等待时间的等待类型向下加)占总等待时间达80%的等待类型 或 等待时间为前5位的等待类型
收集等待信息
这个可以使用SQL SERVER中的功能 Server Actity 来实现
也可以用一张表来不间断的收集当前的累计等待时间,通过计算差值来算出间隔内的等待时间(Interval Wait Time)
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
USE Performance; IF OBJECT_ID('dbo.WaitStats', 'U') IS NOT NULL DROP TABLE dbo.WaitStats; CREATE TABLE dbo.WaitStats ( dt DATETIME NOT NULL DEFAULT (CURRENT_TIMESTAMP), wait_type NVARCHAR(60) NOT NULL, waiting_tasks_count BIGINT NOT NULL, wait_time_ms BIGINT NOT NULL, max_wait_time_ms BIGINT NOT NULL, signal_wait_time_ms BIGINT NOT NULL );
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
-- Load waitstats data on regular intervals INSERT INTO Performance.dbo.WaitStats (wait_type, waiting_tasks_count, wait_time_ms, max_wait_time_ms, signal_wait_time_ms) SELECT wait_type, waiting_tasks_count, wait_time_ms, max_wait_time_ms, signal_wait_time_ms FROM sys.dm_os_wait_stats WHERE wait_type NOT IN (N'MISCELLANEOUS');
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
-- Creation script for IntervalWaits function IF OBJECT_ID('dbo.IntervalWaits', 'IF') IS NOT NULL DROP FUNCTION dbo.IntervalWaits; GO CREATE FUNCTION dbo.IntervalWaits (@fromdt AS DATETIME, @todt AS DATETIME) RETURNS TABLE AS RETURN WITH Waits AS ( SELECT dt, wait_type, wait_time_ms, ROW_NUMBER() OVER(PARTITION BY wait_type ORDER BY dt) AS rn FROM dbo.WaitStats ) SELECT Prv.wait_type, Prv.dt AS start_time, CAST((Cur.wait_time_ms - Prv.wait_time_ms) / 1000. AS NUMERIC(12, 2)) AS interval_wait_s FROM Waits AS Cur JOIN Waits AS Prv ON Cur.wait_type = Prv.wait_type AND Cur.rn = Prv.rn + 1 AND Prv.dt >= @fromdt AND Prv.dt < DATEADD(day, 1, @todt) GO
可以使用简单的查询来分析间隔等待时间,比如下面的查询,返回间隔内的新增等待时间,且按区间内的总新增时间倒序
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
SELECT wait_type, start_time, interval_wait_s FROM dbo.IntervalWaits('20090212', '20090213') AS F ORDER BY SUM(interval_wait_s) OVER(PARTITION BY wait_type) DESC, wait_type, start_time; GO
也可以将函数返回的表放在Excel中,通过Excel生成数据透视图来查看新增等待时间信息
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
-- Prepare view for pivot table IF OBJECT_ID('dbo.IntervalWaitsSample', 'V') IS NOT NULL DROP VIEW dbo.IntervalWaitsSample; GO CREATE VIEW dbo.IntervalWaitsSample AS SELECT wait_type, start_time, interval_wait_s FROM dbo.IntervalWaits('20090212', '20090213') AS F; GO
细化到数据库/文件级别
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
-- Analyze DB IO WITH DBIO AS ( SELECT DB_NAME(IVFS.database_id) AS db, MF.type_desc, SUM(IVFS.num_of_bytes_read + IVFS.num_of_bytes_written) AS io_bytes, SUM(IVFS.io_stall) AS io_stall_ms FROM sys.dm_io_virtual_file_stats(NULL, NULL) AS IVFS JOIN sys.master_files AS MF ON IVFS.database_id = MF.database_id AND IVFS.file_id = MF.file_id GROUP BY DB_NAME(IVFS.database_id), MF.type_desc ) SELECT db, type_desc, CAST(1. * io_bytes / (1024 * 1024) AS NUMERIC(12, 2)) AS io_mb, CAST(io_stall_ms / 1000. AS NUMERIC(12, 2)) AS io_stall_s, CAST(100. * io_stall_ms / SUM(io_stall_ms) OVER() AS NUMERIC(10, 2)) AS io_stall_pct, ROW_NUMBER() OVER(ORDER BY io_stall_ms DESC) AS rn FROM DBIO ORDER BY io_stall_ms DESC;
细化到进程级别
直接使用SQL SERVER PROFILER GUI会有一些性能问题,因为输出会同时到目标文件和客户端,可以由GUI生成TSQL 脚本如下
针对生产环镜中的工作负荷跟踪有时需要几小时,有的却要几天.
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
--------------------------------------------------------------------- -- Trace Performance Workload --------------------------------------------------------------------- SET NOCOUNT ON; USE master; GO IF OBJECT_ID('dbo.PerfworkloadTraceStart', 'P') IS NOT NULL DROP PROC dbo.PerfworkloadTraceStart; GO CREATE PROC dbo.PerfworkloadTraceStart @dbid AS INT, @tracefile AS NVARCHAR(245), @traceid AS INT OUTPUT AS -- Create a Queue DECLARE @rc AS INT; DECLARE @maxfilesize AS BIGINT; SET @maxfilesize = 5; EXEC @rc = sp_trace_create @traceid OUTPUT, 0, @tracefile, @maxfilesize, NULL IF (@rc != 0) GOTO error; -- Set the events DECLARE @on AS BIT; SET @on = 1; -- RPC:Completed exec sp_trace_setevent @traceid, 10, 15, @on; exec sp_trace_setevent @traceid, 10, 8, @on; exec sp_trace_setevent @traceid, 10, 16, @on; exec sp_trace_setevent @traceid, 10, 48, @on; exec sp_trace_setevent @traceid, 10, 1, @on; exec sp_trace_setevent @traceid, 10, 17, @on; exec sp_trace_setevent @traceid, 10, 10, @on; exec sp_trace_setevent @traceid, 10, 18, @on; exec sp_trace_setevent @traceid, 10, 11, @on; exec sp_trace_setevent @traceid, 10, 12, @on; exec sp_trace_setevent @traceid, 10, 13, @on; exec sp_trace_setevent @traceid, 10, 6, @on; exec sp_trace_setevent @traceid, 10, 14, @on; -- SP:Completed exec sp_trace_setevent @traceid, 43, 15, @on; exec sp_trace_setevent @traceid, 43, 8, @on; exec sp_trace_setevent @traceid, 43, 48, @on; exec sp_trace_setevent @traceid, 43, 1, @on; exec sp_trace_setevent @traceid, 43, 10, @on; exec sp_trace_setevent @traceid, 43, 11, @on; exec sp_trace_setevent @traceid, 43, 12, @on; exec sp_trace_setevent @traceid, 43, 13, @on; exec sp_trace_setevent @traceid, 43, 6, @on; exec sp_trace_setevent @traceid, 43, 14, @on; -- SP:StmtCompleted exec sp_trace_setevent @traceid, 45, 8, @on; exec sp_trace_setevent @traceid, 45, 16, @on; exec sp_trace_setevent @traceid, 45, 48, @on; exec sp_trace_setevent @traceid, 45, 1, @on; exec sp_trace_setevent @traceid, 45, 17, @on; exec sp_trace_setevent @traceid, 45, 10, @on; exec sp_trace_setevent @traceid, 45, 18, @on; exec sp_trace_setevent @traceid, 45, 11, @on; exec sp_trace_setevent @traceid, 45, 12, @on; exec sp_trace_setevent @traceid, 45, 13, @on; exec sp_trace_setevent @traceid, 45, 6, @on; exec sp_trace_setevent @traceid, 45, 14, @on; exec sp_trace_setevent @traceid, 45, 15, @on; -- SQL:BatchCompleted exec sp_trace_setevent @traceid, 12, 15, @on; exec sp_trace_setevent @traceid, 12, 8, @on; exec sp_trace_setevent @traceid, 12, 16, @on; exec sp_trace_setevent @traceid, 12, 48, @on; exec sp_trace_setevent @traceid, 12, 1, @on; exec sp_trace_setevent @traceid, 12, 17, @on; exec sp_trace_setevent @traceid, 12, 6, @on; exec sp_trace_setevent @traceid, 12, 10, @on; exec sp_trace_setevent @traceid, 12, 14, @on; exec sp_trace_setevent @traceid, 12, 18, @on; exec sp_trace_setevent @traceid, 12, 11, @on; exec sp_trace_setevent @traceid, 12, 12, @on; exec sp_trace_setevent @traceid, 12, 13, @on; -- SQL:StmtCompleted exec sp_trace_setevent @traceid, 41, 15, @on; exec sp_trace_setevent @traceid, 41, 8, @on; exec sp_trace_setevent @traceid, 41, 16, @on; exec sp_trace_setevent @traceid, 41, 48, @on; exec sp_trace_setevent @traceid, 41, 1, @on; exec sp_trace_setevent @traceid, 41, 17, @on; exec sp_trace_setevent @traceid, 41, 10, @on; exec sp_trace_setevent @traceid, 41, 18, @on; exec sp_trace_setevent @traceid, 41, 11, @on; exec sp_trace_setevent @traceid, 41, 12, @on; exec sp_trace_setevent @traceid, 41, 13, @on; exec sp_trace_setevent @traceid, 41, 6, @on; exec sp_trace_setevent @traceid, 41, 14, @on; -- Set the Filters -- Application name filter EXEC sp_trace_setfilter @traceid, 10, 0, 7, N'SQL Server Profiler%'; -- Database ID filter EXEC sp_trace_setfilter @traceid, 3, 0, 0, @dbid; -- Set the trace status to start EXEC sp_trace_setstatus @traceid, 1; -- Print trace id and file name for future references PRINT 'Trace ID: ' + CAST(@traceid AS VARCHAR(10)) + ', Trace File: ''' + @tracefile + '.trc'''; GOTO finish; error: PRINT 'Error Code: ' + CAST(@rc AS VARCHAR(10)); finish: GO
启动跟踪
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
-- Start the trace DECLARE @dbid AS INT, @traceid AS INT; SET @dbid = DB_ID('Performance'); EXEC master.dbo.PerfworkloadTraceStart @dbid = @dbid, @tracefile = 'c:\temp\Perfworkload 20090212', @traceid = @traceid OUTPUT; GO
停止跟踪
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
-- Stop the trace (assuming trace id was 2) EXEC sp_trace_setstatus 2, 0; EXEC sp_trace_setstatus 2, 2; GO
分析跟跟数据,使用fn_trace_gettable函数把跟踪文件加载到表
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
-- Load trace data to table SET NOCOUNT ON; USE Performance; IF OBJECT_ID('dbo.Workload', 'U') IS NOT NULL DROP TABLE dbo.Workload; GO SELECT CAST(TextData AS NVARCHAR(MAX)) AS tsql_code, Duration AS duration INTO dbo.Workload FROM sys.fn_trace_gettable('c:\temp\Perfworkload 20090212.trc', NULL) AS T WHERE Duration > 0 AND EventClass IN(41, 45); GO
分析跟踪表
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
-- Aggregate trace data by query SELECT tsql_code, SUM(duration) AS total_duration FROM dbo.Workload GROUP BY tsql_code;
缓存中的查询计划,和上面的SQL TRACE方法不同的是,这里不包含查询计划不在缓存中的查询(例如查询或过程使用的RECOMPILE项时)
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
-- Query Statistics SELECT TOP (5) MAX(query) AS sample_query, SUM(execution_count) AS cnt, SUM(total_worker_time) AS cpu, SUM(total_physical_reads) AS reads, SUM(total_logical_reads) AS logical_reads, SUM(total_elapsed_time) AS duration FROM (SELECT QS.*, SUBSTRING(ST.text, (QS.statement_start_offset/2) + 1, ((CASE statement_end_offset WHEN -1 THEN DATALENGTH(ST.text) ELSE QS.statement_end_offset END - QS.statement_start_offset)/2) + 1 ) AS query FROM sys.dm_exec_query_stats AS QS CROSS APPLY sys.dm_exec_sql_text(QS.sql_handle) AS ST CROSS APPLY sys.dm_exec_plan_attributes(QS.plan_handle) AS PA WHERE PA.attribute = 'dbid' AND PA.value = DB_ID('Performance')) AS D GROUP BY query_hash ORDER BY duration DESC;
物理读: 由硬盘上的块读到缓存中,每读一个块就是一个物理读
逻辑读: 从缓存中每读一行就是一次逻辑读
所以要测量查询的I/O信息需要先清空缓存
要查看查询的运行时间还要先清空缓存中的执行计划
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
-- First clear cache DBCC DROPCLEANBUFFERS; -- Then run SET STATISTICS IO ON; SELECT orderid, custid, empid, shipperid, orderdate, filler FROM dbo.Orders WHERE orderdate >= '20080101' AND orderdate < '20080201'; GO SET STATISTICS IO OFF; GO
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
--------------------------------------------------------------------- -- Measuring Runtime of Queries --------------------------------------------------------------------- -- STATISTICS TIME -- First clear cache DBCC DROPCLEANBUFFERS; DBCC FREEPROCCACHE; -- Then run SET STATISTICS TIME ON; SELECT orderid, custid, empid, shipperid, orderdate, filler FROM dbo.Orders WHERE orderdate >= '20080101' AND orderdate < '20080201'; SET STATISTICS TIME OFF; GO