高级t - sql第1级的阶梯:使用交叉连接来引入高级t - sql
源于:格雷戈里·拉森,2016/02/19(首次出版:2014/12/17
翻译:刘琼滨 谢雪妮 徐雅莉 赖慧芳
链接:http://www.sqlservercentral.com/articles/Stairway+Series/119933/
正文:
系列
本文是楼梯系列的一部分:高级t - sql的阶梯
这个楼梯将包含一系列的文章,这些文章将扩展到您在前面的两个t - sql stairways中学习的t - sql基础上,以及在基础之上的t - sql DML和t - sql的阶梯。这个楼梯应该帮助读者准备通过微软认证考试70 - 461:查询微软SQL Server 2012。
这是一个新的楼梯系列的第一篇文章,它将探索Transact SQL(TSQL)的更高级特性。这个楼梯将包含一系列的文章,这些文章将扩展到您在之前的两个TSQL stairways中学习的TSQL基础:
··t - sql DML楼梯
··楼梯t - sql:除了基础知识
这个“高级Transact SQL”的楼梯将涵盖以下TSQL主题:
··使用交叉连接操作符
··使用应用操作符
··了解公共表表达式(CTE)
··创纪录水平处理使用transact - sql游标
··将使用主数据的支持
··使用透视将列进行
··订购您的数据使用排序的功能
··管理日期和时间函数
··了解在条款的变化
这个楼梯的读者应该已经很好地理解了如何从SQL Server表查询、更新、插入和删除数据。此外,他们应该有一个工作知识,这些方法可以用来控制他们的TSQL代码的流程,以及能够测试和操作数据。
这个楼梯应该帮助读者准备通过微软认证考试70 - 461:查询微软SQL Server 2012。
在这个新的楼梯系列的第一期中,我将讨论CROSS JOIN操作符。
CROSS JOIN操作符介绍
交叉连接操作符可以将一个数据集的所有记录合并到另一个数据集中的所有记录。通过使用两组记录之间的交叉连接操作符,您创建了一个称为笛卡尔乘积的东西。
这里有一个简单的例子,使用CROSS JOIN操作符来连接两个表A和B:
SELECT * FROM A CROSS JOIN B
注意,当使用交叉连接操作符时,没有连接子句连接两个表,就像在两个表之间执行内部和外部连接操作时使用的连接子句。
需要注意的是,使用交叉连接可以生成一个大的记录集。为了研究这种行为,我们来看看两个不同的例子,说明这个结果集的大小将来自于交叉连接操作。对于第一个示例,假设您是交叉连接两个表,其中表A有10行,表B有3行。一个交叉连接的结果集将是10乘以3或30行。对于第二个示例,假设表A有1000万行,表B有300万行。在表a和B之间的交叉连接结果中有多少行?那将是一个巨大的30000亿行。这是大量的行,需要大量的时间和大量的资源来创建这个结果集。因此,在大型记录集上使用交叉连接操作符时需要非常小心。
让我们仔细研究一下使用CROSS JOIN操作符的一些例子。
使用交叉连接的基本示例
在前面的几个例子中,我们将会连接两个示例表。清单1中的代码将用于创建这两个示例表。确保在用户数据数据库中运行这些脚本,而不是在master中。
CREATE TABLE Product (ID int,
ProductName varchar(100),
Cost money);CREATE TABLE SalesItem (ID int,
SalesDate datetime,
ProductID int,
Qty int,
TotalSalesAmt money);INSERT INTO Product
VALUES (1,'Widget',21.99),
(2,'Thingamajig',5.38),
(3,'Watchamacallit',1.96);INSERT INTO SalesItem
VALUES (1,'2014-10-1',1,1,21.99),
(2,'2014-10-2',3,1,1.96),
(3,'2014-10-3',3,10,19.60),
(4,'2014-10-3',1,2,43.98),
(5,'2014-10-3',1,2,43.98);
列表1:交叉连接的示例表
对于第一个交叉连接示例,我将运行列表2中的代码。
SELECT * FROM
Product CROSS JOIN SalesItem;
列表2:简单的交叉连接示例
当我在一个SQL Server Management Studio窗口中运行列表2中的代码时,通过我的会话设置输出结果的文本,我得到了报告1中的输出:
ID ProductName Cost ID SalesDate ProductID Qty TotalSalesAmt
--- --------------------- -------- ---- ----------------------- --------- ---- ---------------
1 Widget 21.99 1 2014-10-01 00:00:00.000 1 1 21.99
1 Widget 21.99 2 2014-10-02 00:00:00.000 3 1 1.96
1 Widget 21.99 3 2014-10-03 00:00:00.000 3 10 19.60
1 Widget 21.99 4 2014-10-03 00:00:00.000 1 2 43.98
1 Widget 21.99 5 2014-10-03 00:00:00.000 1 2 43.98
2 Thingamajig 5.38 1 2014-10-01 00:00:00.000 1 1 21.99
2 Thingamajig 5.38 2 2014-10-02 00:00:00.000 3 1 1.96
2 Thingamajig 5.38 3 2014-10-03 00:00:00.000 3 10 19.60
2 Thingamajig 5.38 4 2014-10-03 00:00:00.000 1 2 43.98
2 Thingamajig 5.38 5 2014-10-03 00:00:00.000 1 2 43.98
3 Watchamacallit 1.96 1 2014-10-01 00:00:00.000 1 1 21.99
3 Watchamacallit 1.96 2 2014-10-02 00:00:00.000 3 1 1.96
3 Watchamacallit 1.96 3 2014-10-03 00:00:00.000 3 10 19.60
3 Watchamacallit 1.96 4 2014-10-03 00:00:00.000 1 2 43.98
3 Watchamacallit 1.96 5 2014-10-03 00:00:00.000 1 2 43.98
报告1:运行列表2的结果
如果你回顾报告1的结果,你可以看到有15个不同的记录。这些前5个记录包含从产品表的第一行与SalesItem表中5个不同的行连接的列值。同样适用于产品表的2秒和3行。返回的行数是Product表中的行数乘以SalesItem表中的行数,即15行。
创建Cartesian产品可能有用的一个原因是生成测试数据。假设我想在我的产品和SalesItem表中使用日期生成一些不同的产品。我可以使用一个交叉连接来实现,如列表3所示:
SELECT ROW_NUMBER() OVER(ORDER BY ProductName DESC) AS ID,
Product.ProductName
+ CAST(SalesItem.ID as varchar(2)) AS ProductName,
(Product.Cost / SalesItem.ID) * 100 AS CostFROM Product CROSS JOIN SalesItem;
列表3:简单的交叉连接示例
当我运行列表3中的代码时,我得到了报告2中的输出。
ID ProductName Cost
----- ----------------------------------------------------------- ---------------------
1 Widget1 2199.00
2 Widget2 1099.50
3 Widget3 733.00
4 Widget4 549.75
5 Widget5 439.80
6 Watchamacallit1 196.00
7 Watchamacallit2 98.00
8 Watchamacallit3 65.33
9 Watchamacallit4 49.00
10 Watchamacallit5 39.20
11 Thingamajig1 538.00
12 Thingamajig2 269.00
13 Thingamajig3 179.33
14 Thingamajig4 134.50
15 Thingamajig5 107.60
报告2:运行列表3的结果
通过查看列表3中的代码,您可以看到,我生成了一些列,其中包含与产品表中的数据类似的数据。通过使用ROW_NUMBER函数,我可以在每行上生成唯一的ID列。此外,我使用SalesItem表中的ID列创建惟一的ProductName和成本列值。产生的行数等于产品表中的行数乘以SalesItem表中的行数。
到目前为止,本节中的示例只执行了跨两个表的交叉连接。可以使用CROSS JOIN操作符跨多个表执行交叉连接操作。列表4中的示例在三个表中创建了一个Cartesian产品。
SELECT * FROM sys.tables CROSS JOIN sys.objectsCROSS JOIN sys.sysusers;
列表4:使用CROSS JOIN操作符创建三个表的Cartesian产品
运行列表4的输出有两个不同的CROSS_JOIN操作。由该代码创建的Cartesian产品将产生一个结果集,其总行数等于sys中的行数。表乘以sys中的行数。对象乘以sysusers中的行数。
当交叉连接执行类似于内部连接时
在前面的部分中,我提到过,当使用交叉连接运算符时,它会产生一个笛卡尔积。这不是真的。当您使用WHERE子句约束连接到跨连接操作SQL Server的表时,不会创建笛卡尔产品。相反,它的功能类似于普通的连接操作。为了演示这种行为,请查看列表5中的代码。
SELECT * FROM Product P CROSS JOIN SalesItem SWHERE P.ID = S.ProductID;
SELECT * FROM Product P INNER JOIN SalesItem SON P.ID = S.ProductID;
列表5:两个等价的SELECT语句。
列表5中的代码包含两个SELECT语句。第一个SELECT语句使用CROSS JOIN操作符,然后使用WHERE子句定义如何连接到交叉连接操作中的两个表。第二个SELECT语句使用一个正常的内部连接操作符,并使用一个ON子句来连接这两个表。SQL Server的查询优化器足够聪明,可以知道列表5中的第一个SELECT语句可以作为内部连接重新编写。优化器知道,当使用交叉连接操作时,它可以重新编写查询,与在交叉连接中涉及的两个表之间提供连接谓词的WHERE子句一起使用。因此,SQL Server引擎为列表5中的SELECT语句生成相同的执行计划。当您不提供一个约束SQL服务器不知道如何连接跨连接操作的两个表时,它会在与交叉连接操作相关联的两个集合之间创建一个Cartesian产品。
使用交叉连接查找未销售的产品
在前面的小节中找到的示例是为了帮助您理解CROSS JOIN操作符以及如何使用它。使用CROSS JOIN操作符的一个功能是使用它来帮助在一个表中查找与另一个表中没有匹配记录的项。例如,假设我想要在我的产品表中每一个产品被售出的每一个日期,报告我的产品表中每个产品名称的总数量和总销售额。因为在我的例子中,每一个产品的名字都不是每天都有销售,我的报告要求是我需要显示一个0的数量和总的销售额的0美元,因为这些产品在某一天没有销售。这是交叉连接操作符与左外JOIN操作的结合,它将帮助我识别那些在给定的一天中没有被出售的项目。满足这些报告需求的代码如列表6所示:
SELECT S1.SalesDate, ProductName
, ISNULL(Sum(S2.Qty),0) AS TotalQty
, ISNULL(SUM(S2.TotalSalesAmt),0) AS TotalSalesFROM Product PCROSS JOIN (SELECT DISTINCT SalesDate FROM SalesItem
) S1LEFT OUTER JOIN
SalesItem S2ON P.ID = S2.ProductIDAND S1.SalesDate = S2.SalesDateGROUP BY S1.SalesDate, P.ProductNameORDER BY S1.SalesDate;
列表6:查找不使用交叉连接销售的产品
让我带你走过这段代码。我创建了一个子查询,它选择所有不同的SalesDate值。这个子查询提供了所有的日期,其中有一个销售。然后我将它与我的产品表连接起来。这允许我在每个销售日期和每个产品行之间创建一个Cartesian产品。从交叉连接返回的集合将具有在最终结果集中所需要的所有值,除了每个产品的Qty和TotalSalesAmt的总和。为了获得这些汇总值,我在SalesItem表上执行一个左外连接,并与通过CROSS JOIN操作创建的Cartesian产品连接。我基于ProductID和SalesDate列执行了此连接。通过使用我的Cartesian产品中的左外联接来返回,如果有一个与ProductID和SalesDate相匹配的SalesDate记录,那么Qty和TotalSalesAmt值将与相应的行相关联。这个查询的最后一件事是使用GROUP BY子句来总结基于SalesDate和ProductName的Qty和TotalSalesAmount。
性能考虑
产生笛卡尔积的交叉连接运算符有一些性能方面需要考虑。因为SQL引擎需要在一个集合中加入每一行,而在另一个集合中,结果集可以相当大。如果我做一个交叉连接一个表有1,000,000行和另一个表有100,000行那么我的结果集就会有1,000,000 X 10万行,或者说100,000,000,000行。这是一个很大的结果集,它将花很多时间来创建它。
交叉连接操作符可以是一个很好的解决方案,可以在所有可能的组合中确定一个结果集,就像所有客户的每个月的所有销售,即使在几个月的时间里,一些客户没有销售。在使用CROSS JOIN操作符时,如果希望优化性能,应该尽量减少交叉联接的大小。例如,假设我有一个表,其中包含过去两个月的销售数据。如果我想要生成一个报告,显示一个月没有销售的客户,那么确定一个月的天数的方法可以极大地改变我的查询的性能。为了证明这一点,我首先为1000名客户创造了一个为期两个月的销售记录。我将使用列表7中的代码来实现这一点。
CREATE TABLE Cust (Id int, CustName varchar(20));CREATE TABLE Sales (Id int identity
,CustID int
,SaleDate date
,SalesAmt money);SET NOCOUNT ON;DECLARE @I int = 0;DECLARE @Date date;WHILE @I < 1000BEGIN
SET @I = @I + 1;
SET @Date = DATEADD(mm, -2, '2014-11-01');
INSERT INTO Cust
VALUES (@I,
'Customer #' + right(cast(@I+100000 as varchar(6)),5));
WHILE @Date < '2014-11-01'
BEGIN
IF @I%7 > 0
INSERT INTO Sales (CustID, SaleDate, SalesAmt)
VALUES (@I, @Date, 10.00);
SET @Date = DATEADD(DD, 1, @Date);
ENDEND
列表7:TSQL为性能测试创建示例数据
列表7中的代码为1000个不同的客户创建了两个月的数据。这段代码没有为每7个客户增加销售数据。这段代码产生了1000个Cust表记录和52,338个销售表记录。
为了演示如何使用交叉连接操作符执行不同的操作,这取决于跨连接输入集中使用的集合的大小,让我来运行列表8和列表9中的代码。对于每个测试,我将记录返回结果所需的时间。
SELECT CONVERT(CHAR(6),S1.SaleDate,112) AS SalesMonth, C.CustName,
ISNULL(SUM(S2.SalesAmt),0) AS TotalSalesFROM Cust CCROSS JOIN (SELECT SaleDate FROM Sales ) AS S1LEFT OUTER JOIN
Sales S2ON C.ID = S2.CustIDAND S1.SaleDate = S2.SaleDateGROUP BY CONVERT(CHAR(6),S1.SaleDate,112),C.CustNameHAVING ISNULL(SUM(S2.SalesAmt),0) = 0ORDER BY CONVERT(CHAR(6),S1.SaleDate,112),C.CustName
列表8:与所有销售记录交叉连接
SELECT CONVERT(CHAR(6),S1.SaleDate,112) AS SalesMonth, C.CustName,
ISNULL(SUM(S2.SalesAmt),0) AS TotalSalesFROM Cust CCROSS JOIN (SELECT DISTINCT SaleDate FROM Sales ) AS S1LEFT OUTER JOIN
Sales S2ON C.ID = S2.CustIDAND S1.SaleDate = S2.SaleDateGROUP BY CONVERT(CHAR(6),S1.SaleDate,112),C.CustNameHAVING ISNULL(SUM(S2.SalesAmt),0) = 0ORDER BY CONVERT(CHAR(6),S1.SaleDate,112),C.CustName
列表9:与不同的销售日期列表交叉连接
在列表8中,CROSS JOIN操作符加入了1000个Cust记录,其中有52,338个销售记录,生成一个创纪录的52338000行的记录集,然后用来确定一个月销售为零的客户。在列表9中,我将选择标准从Sales表中更改为只返回一组不同的SalesDate值。这个独特的集合只产生了61个不同的销售日期值,因此列表9中的CROSS JOIN操作的结果只产生了61,000条记录。通过减少交叉连接操作的结果集,清单9中的查询运行不到1秒,而列表8中的代码在我的机器上运行了19秒。这种性能差异的主要原因是记录SQL Server需要处理每个查询执行的不同操作的数量。如果您查看两个清单的执行计划,您将看到计划略有不同。但是,如果您看一下嵌套循环(Inner Join)操作所生成的记录的数量,在图形化计划的右侧,您将看到列表8估计有52338000条记录,而列表9中的操作仅估计有61,000条记录。这个巨大的记录集,列表8的查询计划从交叉连接嵌套循环操作中生成,然后再传递到几个额外的操作。因为列表8中的所有操作都必须处理5200万的记录。列表8比列表9慢得多。
正如您所看到的,交叉连接操作中使用的记录数可以极大地影响查询运行的时间长度。因此,如果您可以编写您的查询来最小化交叉连接操作中涉及的记录的数量,那么您的查询将执行得更有效率。
结论
交叉连接运算符在两个记录集之间产生一个笛卡尔积。这个操作符有助于识别一个表中没有与另一个表中匹配的记录的项。应注意尽量减少与交叉连接操作符使用的记录集的大小。通过确保交叉连接的结果集尽可能小,您将确保代码尽可能快地运行。