T-SQL调优学习笔记


-- 第一章 SQL窗口函数
/*
窗口函数的作用域是由OVER 子句定义的数据行集合。其概念的精髓在于可以通过对数据行集合
或对数据行窗口进行多种计算(汇总,移动平均值,找数据岛),最后的得到单个值
*/

1.1 窗口函数的背景

1.1.1 窗口函数的描述

USE TSQL2012;

SELECT orderid, orderdate, val,
			 rank() OVER (ORDER BY val DESC) as rnk
FROM Sales.OrderValues
ORDER BY rnk;
-- 这里有个问题就是究竟什么叫窗口?伏笔

/*
窗口函数有四种类型:集合,排序,分布,和偏移。


聚合函数是诸如:SUM,COUNT,MIN,MAX,(之前在GROUP BY 里面用过)一个聚合函数的作用域是一个记录集,
这个记录集由一个分组查询或一个窗口描述来定义。
剩下三种用到了再说。

下面这本书将教我完成:
1.分页
2.去除重复数据
3.返回每组前n条记录
4.计算累积合计
5.对时间间隔进行操作,统计最大的并发会话数
6.找出数据差距(gap)和数据岛(island)
7.计算百分比
8.计算分布的模式
9.排序层次结构
10.数据透视
11.计算时效性(计算近因)


声明性语言(SQL)和优化
在SQL中,我们仅仅从逻辑上声明我们的需求,而不是具体描述如何实现它。为什么目的一致,形式不同的两种需求
如一个使用窗口函数,另一个不用将是不同的性能?SQL不能分辨这两种不同的形式代表同一个意思,从而为两种形式
解析出同一种查询执行。

*/

1.1.2 基于集 合与基于迭代/游标的编程
--什么是游标?
游标(CURSOR)是一个存储在sql服务器上得到数据库查询,它不是一条SELECT语句,而是被该语句检索出来的结果集。
在存储了游标之后,应用程序可以根据需要滚动或浏览其中的数据。

在《mysql必知必会》这本书上有一个关于游标的例子:循环检索数据,从第一行到最后一行。

CREATE PROCEDURE processorders()
BEGIN
			-- DECLARE local variables
			DECLARE done boolean DEFAULT 0;
			DECLARE o INT;
			DECLARE t DECIMAL(8,2);

			-- DECLARE the cursor
			DECLARE ordernumbers CURSOR
			FOR
			SELECT order_num FROM orders;
			-- DECLARE continue handler
			DECLARE countinue handler FOR SQLSTATE '02000' SET done = 1;

			--CREATE TABLE TO store the results
			CREATE TABLE IF NOT EXISTS ordertotals
			  (order_num INT, total DECIMAL(8,2));
      
			-- OPEN the cursor
			OPEN ordernumbers;
			--loop all rows
			repeat
				FETCH ordernumbers into o;
				call ordertotal(o, 1, t);
				INSERT INTO ordertotals(order_num, total)
				VALUES(o, t);
				until done END repeat;
				CLOSE ordernumbers;
END;

T-SQL的查询任务的解决方案要么基于集合,要么基于迭代/游标。
集合的定义和内涵很丰富,重点在于两个,整体和顺序。

整体:一个集合应该被当做一个整体来被感知和操作,我们的关注点应该放在集合这个整体上,而不是其中的各个元素。
迭代操作则正好相反,因为文件中的记录或游标都是逐个处理的。当用基于集合的查询与数据库中的表进行交互时,我们是与
整个表交互,而不是与表中的单个记录进行交互——这个思维一上来很难接纳。

顺序:集合中没有对元素顺序进行规定。文件和游标中的记录都有特定的顺序,每次提取一条记录时,我们知道它们的提取就是
按照和这个顺序,表中的行是没有顺序的,因为一张表就是一个集合。不要混淆 数据模型的逻辑表达和物理层面的实现语言。

编写SQL查询,我们的思维要基于集合的概念进行,这也就是为什么窗口函数可以基于迭代的思维(每次一条,以确定顺序)和
基于集合的思维(集合是一个整体,没有顺序)之间架起桥梁,帮助我们从一种思维模式转换到另一种思维模式,这是窗口函数的独创思维。

注意,函数支持顺序限定,并时不时说明它违反了关系型概念,查询的输入是关系型,对于没有顺序的期待,查询的输出也是关系型,并不保证顺序,
排序仅仅作为查询中计算描述的一部分,在结果中作为一特性产生。


1.1.3 窗口函数为什么优秀?orz

分组查询以聚合的形式,提供一些新的信息,代价是损失了详细信息。

当我们对数据进行分组时,不得不把计算应用到组中的所有内容上。但如果我们想在计算中涉及聚合信息和详细信息就GG
eg:
查询 Sales.OrderValues视图,计算每笔订单占当前客户订单总金额的百分比,以及与客户订单平均金额的差异。

当前订单金额是详细信息,客户订单总金额和平均金额是聚合值。如果按照客户来对数据进行分组,就获取不到每笔订单的金额
传统的分组查询对于这种需求的处理方式是:建立一个查询,按客户对数据进行分组,根据这个查询定义一个表表达式,把CTE表与基表联合

WITH a AS
(
	SELECT custid, SUM(val) as sumval, AVG(val) as avgal
	FROM Sales.OrderValues
	GROUP BY custid
)
SELECT b.orderid, b.custid, b.val,
			 CAST((100. * b.val / a.sumval) as NUMERIC(5,2)) as pctcust,
			 b.val - a.avgal as diffcust
FROM Sales.OrderValues as b
		 JOIN a
		 ON a.custid = b.custid;

现在还想要计算占总合计的百分比以及与总平均金额的差异?再加一个表表达式。
注意这里再连接表的时候用CROSS JOIN因为总的
略


另一种就是对于每个计算使用独立的子查询

SELECT orderid, custid, val,
			 CAST(100. * val / 
						(SELECT SUM(o2.val)
						 FROM Sales.OrderValues as o2
						 WHERE o2.custid = o1.custid) as NUMERIC(5,2)) as pctcust,
			 val - (SELECT AVG(o2.val)
							FROM Sales.OrderValues as o2
							WHERE o2.custid = o1.custid) as diffcust
FROM Sales.OrderValues as o1;
......
这个代码太长性能也不好,需要两次扫库


SELECT *
FROM Sales.OrderValues as o1,Sales.OrderValues as o2
WHERE o2.custid = o1.custid
AND o2.custid = 85

自连接有5*5=25条记录
卧槽,这个思想很牛逼的一点就是通过SUM成倍扩大了一个用户的订单价格,然后val也出现了这个倍数次,相除倍数就没有了。


窗口函数背后的理念是要定义一个函数的操作窗口或行集。聚合函数也应该作用于行集。使用OVER子句来定义该函数的窗口
分区的含义是过滤而不是分组

1.返回当前订单占此客户订单总金额的百分比
2.val与该客户订单平均金额的差异
3.每笔订单占总金额百分比
4.每笔订单与总平均金额差异

SELECT orderid, custid, val,
			 CAST(100. * val / SUM(val) OVER (PARTITION BY custid) as NUMERIC(5,2)) as pctcust,
			 val - AVG(val) OVER (PARTITION by custid) as diffcust,
			 CAST(100. * val / SUM(val) OVER() as NUMERIC(5,2)) as pctall,
			 val - AVG(val) OVER () as diffall 
FROM Sales.OrderValues

SQL Sever 如果发现不同的函数使用相同的窗口,只会对窗口内的数据访问一次。
还有一点事窗口函数的优点:添加限制前的初始窗口是查询的结果集。比如上面的查询想查看2007年,只须在查询中
增加一个筛选器。因为窗口函数的起始点是应用了过滤项之后的结果集,但是如果使用了子查询就需要在所有子查询中
重复使用筛选器。but,也可以使用一个CTE,该表应用了筛选器的查询,外部查询和子查询都指向这个CTE。

1.2 数据岛问题

SET nocount ON; --使返回的结果中不包含有关受 Transact-SQL 语句影响的行数的信息 
USE TSQL2012;

IF OBJECT_ID('dbo.T1', 'U') IS NOT NULL DROP TABLE dbo.T1;
GO

CREATE TABLE dbo.T1
(
	col1 INT NOT NULL
		CONSTRAINT PK_T1 PRIMARY KEY
);
INSERT INTO dbo.T1(col1)
		VALUES(2),(3),(11),(12),(13),(27),(33),(34),(35),(42);
GO

找到数据岛:两个方法,核心都是构建概念化的列,同一岛内grp值相同,以grp值分组。

法1:
对每个col,找到大于/等于当前值的最小col1值,并且要求这个值后面没有值。SO,一个数据岛
内的所有成员的组标识符就是这个数据岛最后一个成员的值。

SELECT col1,
	(SELECT MIN(B.col1)
	 FROM dbo.T1 as B
	 WHERE B.col1 >= A.col1
	 AND NOT EXISTS (SELECT * FROM dbo.T1 as C
									 WHERE C.col1 = B.col1 + 1)) as grp
FROM dbo.T1 as A;

SELECT MIN(col1) as start_range, MAX(col1) as end_range
FROM (SELECT col1,
				(SELECT MIN(B.col1)
				 FROM dbo.T1 as B
				 WHERE B.col1 >= A.col1
				 AND NOT EXISTS (SELECT * 
												 FROM dbo.T1 as C
												 WHERE C.col1 = B.col1 + 1)) as grp
			FROM dbo.T1 as A) as D
GROUP BY grp;
--发现可以在SELECT 里面不写分组的grp
这个查询问题在于逻辑负责,扫库两次,数据量大就歇菜


法2:窗口函数计算组标识符
SELECT col1, ROW_NUMBER() OVER(ORDER BY col1) as rownum 
FROM dbo.T1;

这里发现col1不连贯, rownum是连贯的。在数据岛,两个序列都以固定的间隔在增长,因此二者的
差异是一个常数。
SELECT col1, col1 - ROW_NUMBER() OVER(ORDER BY col1) as diff
FROM dbo.T1;

这个差异可以当做组标识符用
SELECT MIN(col1) as start_range, MAX(col1) as end_range
FROM (SELECT col1, col1 - ROW_NUMBER() OVER(ORDER BY col1) as diff
			FROM dbo.T1) as A
GROUP BY diff

--简单明了,仅包含一个在col1上的排序索引扫描和一个持续递增计数器的迭代器

1.3 窗口函数中的元素

分区 排序 框架

单独看下框架
计算每个员工每个订单月的销售数量累计总计:
SELECT empid, ordermonth, qty,
			 SUM(qty) OVER(PARTITION BY empid ORDER BY ordermonth 
										 ROWS BETWEEN unbounded preceding AND CURRENT row) as runqty
FROM Sales.EmpOrders;

在分区内,按照给定的排序,设定框架为当前行之前的所有行(没有下边界点)。换句话说,结果合计的框架是当前行
(包含)之前的所有行


1.4 支持窗口函数的查询元素
一张图,二义性
不允许窗口函数判断遭遇SELECT

1.6 窗口定义的重复使用
SQL sever 不支持WINDOW

猜你喜欢

转载自blog.csdn.net/dufemt/article/details/81084437