加入 100 个表
Posted
技术标签:
【中文标题】加入 100 个表【英文标题】:Joining 100 tables 【发布时间】:2013-01-24 16:12:54 【问题描述】:假设我有一个主表,其中有 100 列引用(作为外键)大约 100 个表(包含主键)。
整个信息包需要加入这 100 个表。而且加入这么多表肯定是性能问题。希望我们可以预期任何用户都希望在查询中请求一组包含不超过 5-7 个表(在这 100 个表中)的值的数据,这些表将条件(在查询的 WHERE 部分)置于大约 3-4 张桌子(在这 100 张桌子中)。不同的查询具有不同的表组合,用于生成查询的“SELECT”部分并将条件放在“WHERE”中。但是,同样,每个 SELECT 将需要大约 5-7 个表,每个 WHERE 将需要大约 3-4 个表(当然,用于生成 SELECT 的表列表可能与用于将条件放入 WHERE 的表列表重叠)。
我可以编写一个视图,其中包含连接所有这 100 个表的基础代码。然后我可以将上面提到的 SQL 查询写入这个 VIEW。但是在这种情况下,如何指示 SQL Server(尽管代码中明确说明要连接所有这 100 个表)应该只连接大约 11 个表(11 个表足以连接以产生 SELECT结果并考虑 WHERE 条件)。
另一种方法可能是创建一个转换以下“假”代码的“功能”
SELECT field1, field2, field3 FROM TheFakeTable WHERE field1=12 and field4=5
进入以下“真实”代码:
SELECT T1.field1, T2.field2, T3.field3 FROM TheRealMainTable
join T1 on ....
join T2 on ....
join T3 on ....
join T4 on ....
WHERE T1.field1=12 and T4.field4=5
从语法的角度来看,即使允许这种“TheFakeTable-mechanism”与真实表格和结构的任何混合组合也不是问题。这里真正的问题是如何在技术上实现这个“特性”。我可以创建一个将“假”代码作为输入并生成“真实”代码的函数。但这并不方便,因为它需要在出现“TheFakeTable-mechanism”的任何地方使用动态 SQL 工具。一个梦幻般的解决方案是在我的 Management Studio 中扩展 SQL 语言的语法,以允许编写这样的假代码,然后在发送到服务器之前自动将此代码转换为真实代码。
我的问题是:
-
是否可以指示 SQl Server 仅加入 11 个表而不是上述 VIEW 中的 100 个表?
如果我决定创建这个“TheFakeTable-mechanism”功能,那么在技术上实现该功能的最佳形式是什么?
感谢大家的每一条评论!
PS 具有 100 个表的结构源于我在这里提出的以下问题: Normalizing an extremely big table
【问题讨论】:
其他人可能会和它说话,但我不知道 SQL Server 是否会像 postgres、oracle 等那样加入删除 - rhaas.blogspot.com/2010/06/why-join-removal-is-cool.html 因此,实际上,用户可能不想查看包含数百列的报告,对吗?您可以改为识别此数据的特定用例,并为每个用例进行单独查询,希望这些用例需要更少的连接...... 您为什么要在一个视图中完成所有操作?我希望使用 100 个表,我将拥有数百个视图和/或存储过程,以仅获取特定情况所需的数据。如果客户想要的定义每次都不同,则基于 where 子句动态 SQL 运行查询是最好的方法。 我们谈论的是什么类型的用户以及他们使用什么来构建查询? 您最好的方法是动态 SQL,您只需加入您需要的部分。见:The Curse and Blessings of Dynamic SQL 【参考方案1】:SQL Server 优化器确实包含删除冗余连接的逻辑,但有一些限制,连接必须是provably redundant。总而言之,连接可以产生四种效果:
-
它可以添加额外的列(来自连接表)
它可以添加额外的行(连接的表可能会多次匹配一个源行)
它可以删除行(连接的表可能没有匹配)
可以引入
NULL
s(对于RIGHT
或FULL JOIN
)
要成功删除冗余连接,查询(或视图)必须考虑所有四种可能性。正确完成此操作后,效果可能会令人惊讶。例如:
USE AdventureWorks2012;
GO
CREATE VIEW dbo.ComplexView
AS
SELECT
pc.ProductCategoryID, pc.Name AS CatName,
ps.ProductSubcategoryID, ps.Name AS SubCatName,
p.ProductID, p.Name AS ProductName,
p.Color, p.ListPrice, p.ReorderPoint,
pm.Name AS ModelName, pm.ModifiedDate
FROM Production.ProductCategory AS pc
FULL JOIN Production.ProductSubcategory AS ps ON
ps.ProductCategoryID = pc.ProductCategoryID
FULL JOIN Production.Product AS p ON
p.ProductSubcategoryID = ps.ProductSubcategoryID
FULL JOIN Production.ProductModel AS pm ON
pm.ProductModelID = p.ProductModelID
优化器可以成功简化以下查询:
SELECT
c.ProductID,
c.ProductName
FROM dbo.ComplexView AS c
WHERE
c.ProductName LIKE N'G%';
收件人:
Rob Farley 在original MVP Deep Dives book 中深入探讨了这些想法,SQLBits 上也有一个recording of him presenting on the topic。
主要限制是外键关系must be based on a single key 有助于简化过程,并且针对此类视图的查询的编译时间可能会变得很长,特别是随着连接数量的增加。编写一个包含 100 个表且所有语义都完全正确的视图可能是一个相当大的挑战。我倾向于寻找替代解决方案,也许使用dynamic SQL。
也就是说,非规范化表的特殊特性可能意味着视图非常易于组装,只需要强制执行 FOREIGN KEYs
非NULL
able 引用列和适当的 UNIQUE
约束,以使此解决方案作为您会希望,计划中没有 100 个物理连接运算符的开销。
示例
使用十个表而不是一百个:
-- Referenced tables
CREATE TABLE dbo.Ref01 (col01 tinyint PRIMARY KEY, item varchar(50) NOT NULL UNIQUE);
CREATE TABLE dbo.Ref02 (col02 tinyint PRIMARY KEY, item varchar(50) NOT NULL UNIQUE);
CREATE TABLE dbo.Ref03 (col03 tinyint PRIMARY KEY, item varchar(50) NOT NULL UNIQUE);
CREATE TABLE dbo.Ref04 (col04 tinyint PRIMARY KEY, item varchar(50) NOT NULL UNIQUE);
CREATE TABLE dbo.Ref05 (col05 tinyint PRIMARY KEY, item varchar(50) NOT NULL UNIQUE);
CREATE TABLE dbo.Ref06 (col06 tinyint PRIMARY KEY, item varchar(50) NOT NULL UNIQUE);
CREATE TABLE dbo.Ref07 (col07 tinyint PRIMARY KEY, item varchar(50) NOT NULL UNIQUE);
CREATE TABLE dbo.Ref08 (col08 tinyint PRIMARY KEY, item varchar(50) NOT NULL UNIQUE);
CREATE TABLE dbo.Ref09 (col09 tinyint PRIMARY KEY, item varchar(50) NOT NULL UNIQUE);
CREATE TABLE dbo.Ref10 (col10 tinyint PRIMARY KEY, item varchar(50) NOT NULL UNIQUE);
父表定义(带页面压缩):
CREATE TABLE dbo.Normalized
(
pk integer IDENTITY NOT NULL,
col01 tinyint NOT NULL REFERENCES dbo.Ref01,
col02 tinyint NOT NULL REFERENCES dbo.Ref02,
col03 tinyint NOT NULL REFERENCES dbo.Ref03,
col04 tinyint NOT NULL REFERENCES dbo.Ref04,
col05 tinyint NOT NULL REFERENCES dbo.Ref05,
col06 tinyint NOT NULL REFERENCES dbo.Ref06,
col07 tinyint NOT NULL REFERENCES dbo.Ref07,
col08 tinyint NOT NULL REFERENCES dbo.Ref08,
col09 tinyint NOT NULL REFERENCES dbo.Ref09,
col10 tinyint NOT NULL REFERENCES dbo.Ref10,
CONSTRAINT PK_Normalized
PRIMARY KEY CLUSTERED (pk)
WITH (DATA_COMPRESSION = PAGE)
);
观点:
CREATE VIEW dbo.Denormalized
WITH SCHEMABINDING AS
SELECT
item01 = r01.item,
item02 = r02.item,
item03 = r03.item,
item04 = r04.item,
item05 = r05.item,
item06 = r06.item,
item07 = r07.item,
item08 = r08.item,
item09 = r09.item,
item10 = r10.item
FROM dbo.Normalized AS n
JOIN dbo.Ref01 AS r01 ON r01.col01 = n.col01
JOIN dbo.Ref02 AS r02 ON r02.col02 = n.col02
JOIN dbo.Ref03 AS r03 ON r03.col03 = n.col03
JOIN dbo.Ref04 AS r04 ON r04.col04 = n.col04
JOIN dbo.Ref05 AS r05 ON r05.col05 = n.col05
JOIN dbo.Ref06 AS r06 ON r06.col06 = n.col06
JOIN dbo.Ref07 AS r07 ON r07.col07 = n.col07
JOIN dbo.Ref08 AS r08 ON r08.col08 = n.col08
JOIN dbo.Ref09 AS r09 ON r09.col09 = n.col09
JOIN dbo.Ref10 AS r10 ON r10.col10 = n.col10;
修改统计信息让优化器认为表非常大:
UPDATE STATISTICS dbo.Normalized WITH ROWCOUNT = 100000000, PAGECOUNT = 5000000;
用户查询示例:
SELECT
d.item06,
d.item07
FROM dbo.Denormalized AS d
WHERE
d.item08 = 'Banana'
AND d.item01 = 'Green';
给我们这个执行计划:
规范化表的扫描看起来很糟糕,但存储引擎在扫描期间应用了两个 Bloom-filter 位图(因此无法匹配的行甚至不会出现在查询处理器中)。这可能足以在您的情况下提供可接受的性能,并且肯定比扫描具有溢出列的原始表更好。
如果您能够在某个阶段升级到 SQL Server 2012 Enterprise,您还有另一个选择:在规范化表上创建列存储索引:
CREATE NONCLUSTERED COLUMNSTORE INDEX cs
ON dbo.Normalized (col01,col02,col03,col04,col05,col06,col07,col08,col09,col10);
执行计划是:
这对您来说可能看起来更糟,但列存储提供了出色的压缩,并且整个执行计划在批处理模式下运行,并为所有贡献的列提供过滤器。如果服务器有足够的可用线程和内存,这个替代方案真的可行。
最终,考虑到表的数量以及执行计划不佳或需要过多编译时间的可能性,我不确定这种规范化是正确的方法。我可能会首先更正非规范化表的架构(正确的数据类型等),可能会应用数据压缩......通常的事情。
如果数据确实属于星型模式,那么它可能需要更多的设计工作,而不仅仅是将重复的数据元素拆分到单独的表中。
【讨论】:
。 .如果查询写成嵌套的SELECT
s (select t.*, (select top 1 val from ref1 where ref1.ref1id = t.ref1id), . . .
),那会帮助编译器识别这种情况吗?
。 .还有一个问题。我的理解是视图的执行计划是在第一次使用视图时创建的(而不是在创建视图时)。在这种情况下,以后使用未使用的列会有问题吗?视图会得到一个新的执行计划吗?还是我对这个案例的初步理解是错误的?
@GordonLinoff 我读过一些人使用该策略优化了重连接查询。如果您的数据库需要多次连接并且无法解决,我相信这可能是一个可行的选择。【参考方案2】:
您为什么认为加入 100 个表会成为性能问题?
如果所有的键都是主键,那么所有的连接都将使用索引。那么,唯一的问题是索引是否适合内存。如果它们适合内存,性能可能根本不是问题。
在做出这样的陈述之前,您应该尝试使用 100 个连接的查询。
此外,根据原始问题,参考表中只有几个值。表格本身适合单页,加上索引的另一页。这是 200 个页面,最多会占用几兆字节的页面缓存。不要担心优化,创建视图,如果您有性能问题,请考虑下一步。不要预设性能问题。
详细说明:
这已经收到了很多cmets。让我解释一下为什么这个想法可能不像听起来那么疯狂。
首先,我假设所有的连接都是通过主键索引完成的,并且索引适合内存。
页面上的 100 个键占用 400 个字节。假设原始字符串平均每个有 40 个字节。这些将占用页面上的 4,000 字节,所以我们有一个节省。事实上,在之前的方案中,大约 2 条记录可以放在一个页面上。大约 20 个适合带有键的页面。
因此,使用键读取记录比读取原始记录要快 10 倍在 I/O 方面。在对少量值的假设下,索引和原始数据可以放入内存。
读取 20 条记录需要多长时间?旧方法需要阅读 10 页。使用这些键,可以读取一页和 100*20 索引查找(可能需要额外查找来获取值)。根据系统的不同,2,000 个索引查找可能比额外的 9 页 I/O 更快——甚至快得多。我想说的是,这是一个合理的情况。它可能会或可能不会发生在特定系统上,但这并不疯狂。
这有点过于简单了。 SQL Server 实际上并不一次读取一个页面。我认为它们以 4 组为一组读取(并且在进行全表扫描时可能会有前瞻读取)。但另一方面,在大多数情况下,表扫描查询的 I/O 绑定比处理器绑定更多,因此有空闲的处理器周期用于在引用表中查找值。
事实上,使用键可能会比不使用键更快读取表,因为备用处理周期将用于查找(“备用”是指处理能力是阅读时可用)。事实上,带有键的表可能足够小,可以放入可用的缓存中,从而大大提高了更复杂查询的性能。
实际性能取决于很多因素,例如字符串的长度、原始表(是否大于可用缓存?)、底层硬件同时进行 I/O 读取和处理的能力时间,以及对查询优化器正确执行连接的依赖。
我最初的观点是假设 先验 100 个连接是一件坏事是不正确的。该假设需要进行测试,使用密钥甚至可能会提高性能。
【讨论】:
我没有投票过任何一种方式,但我记得在 Cesar Galindo-Legaria(来自 SQL Server 查询优化器团队)的“SQL server 2005 实用故障排除”一书中读到了一些关于 60 个表连接和说这真的是在搜索连接顺序和类型的空间方面超越了合理的界限。 我认为创建一个巨大的视图不是答案。你能想象这个调试有多难,写多长时间吗?我从来没有见过这样的东西。虽然我见过很多东西...... @Gordon Linoff - 我没有什么要补充的,您的回答或其他人也没有问题。我不会做诸如创建 100 个表的连接或视图之类的事情,因为我喜欢保持简单。至少在我工作的公司中,我没有看到这样的事情。也许一些数据仓库。公司有这样的结构......我不知道。我对我的 cmets 并不认真,只是取笑,好心...)还有一件事,即使我不同意,我也从不反对任何人。与许多其他人不同,我要求或保留给自己。 其实我已经找到了我现在想的那一段。并更正我之前提到 60 个表格的评论。If you are joining over 20 tables, chances are the optimizer is not reviewing the entire search space but relying more on heuristics .... we have seen applications that run regular queries dealing with over 100 tables. While it is possible to run such very large queries, you really are stretching the system in these cases and should be very careful going this far
我同意这个答案。创建视图,然后查看性能问题。根据最初的问题,这 100 个表是简单的查找,但只有几个值。当所有其他列也是简单数据类型而不是文本时,我看不出你有问题。【参考方案3】:
如果您的数据没有太大变化,您可能会受益于创建一个Indexed View,它基本上实现了视图。
如果数据经常更改,这可能不是一个好的选择,因为服务器必须为视图基础表中的每次更改维护索引视图。
这是一个很好的blog post,描述得更好。
来自博客:
CREATE VIEW dbo.vw_SalesByProduct_Indexed
WITH SCHEMABINDING
AS
SELECT
Product,
COUNT_BIG(*) AS ProductCount,
SUM(ISNULL(SalePrice,0)) AS TotalSales
FROM dbo.SalesHistory
GROUP BY Product
GO
下面的脚本在我们的视图上创建索引:
CREATE UNIQUE CLUSTERED INDEX idx_SalesView ON vw_SalesByProduct_Indexed(Product)
显示已在视图上创建索引并且确实创建了索引 占用数据库空间,运行以下脚本找出 聚集索引中有多少行以及视图有多少空间 占用。
EXECUTE sp_spaceused 'vw_SalesByProduct_Indexed'
下面的 SELECT 语句与之前的语句相同,除了 这次它执行聚集索引查找,这通常是非常 快。
SELECT
Product, TotalSales, ProductCount
FROM vw_SalesByProduct_Indexed
WHERE Product = 'Computer'
【讨论】:
具有讽刺意味的是,数据开始是完全非规范化的。最初的问题是关于对其进行规范化,从而产生了大量的参考表。创建物化视图最终会返回原始视图。以上是关于加入 100 个表的主要内容,如果未能解决你的问题,请参考以下文章