11

假设我有一个主表,其中有 100 列引用(作为外键)到大约 100 个表(包含主键)。

整个信息包需要加入这 100 个表。而且加入这么多表肯定是性能问题。希望我们可以预期任何用户都希望在查询中请求一组包含不超过 5-7 个表(在这 100 个表中)的值的数据,这些表将条件(在查询的 WHERE 部分)置于大约 3-4 张桌子(在这 100 张桌子中)。不同的查询具有不同的表组合,用于生成查询的“SELECT”部分并将条件放在“WHERE”中。但是,同样,每个 SELECT 将需要大约 5-7 个表,每个 WHERE 将需要大约 3-4 个表(当然,用于生成 SELECT 的表列表可能与用于将条件放入 WHERE 的表列表重叠)。

我可以编写一个带有底层代码的 VIEW 加入所有这 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 语言的语法,以允许编写这样的假代码,然后在发送到服务器之前自动将此代码转换为真实代码。

我的问题是:

  1. 是否可以指示 SQl 服务器在上述 VIEW 中仅加入 11 个表而不是 100 个表(或成为天才)?
  2. 如果我决定创建这个“TheFakeTable-mechanism”功能,那么在技术上实现这个功能的最佳形式是什么?

感谢大家的每一条评论!

PS 100 个表的结构源于我在这里提出的以下问题: Normalizing anextreme big table

4

3 回答 3

17

SQL Server 优化器确实包含删除冗余连接的逻辑,但存在一些限制,并且连接必须可证明是冗余的。总而言之,连接可以产生四种效果:

  1. 它可以添加额外的列(来自连接表)
  2. 它可以添加额外的行(连接的表可能会多次匹配源行)
  3. 它可以删除行(连接的表可能没有匹配)
  4. 它可以引入NULLs(对于 aRIGHTFULL 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 在最初的 MVP Deep Dives 书中深入地描述了这些想法,并且有一段他在 SQLBits上就该主题进行演讲的录音。

主要限制是外键关系必须基于单个键以有助于简化过程,并且针对这种视图的查询的编译时间可能会变得很长,特别是随着连接数量的增加。编写一个包含 100 个表且所有语义都完全正确的视图可能是一个相当大的挑战。我倾向于寻找替代解决方案,也许使用动态 SQL

也就是说,非规范化表的特殊特性可能意味着视图非常易于组装,只需要强制执行的FOREIGN KEYs不可NULL引用列和适当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';

给我们这个执行计划:

执行计划1

规范化表的扫描看起来很糟糕,但是存储引擎在扫描期间应用了两个 Bloom-filter 位图(因此无法匹配的行甚至不会出现在查询处理器中)。这可能足以在您的情况下提供可接受的性能,并且肯定比扫描具有溢出列的原始表更好。

如果您能够在某个阶段升级到 SQL Server 2012 Enterprise,您还有另一个选择:在规范化表上创建列存储索引:

CREATE NONCLUSTERED COLUMNSTORE INDEX cs 
ON dbo.Normalized (col01,col02,col03,col04,col05,col06,col07,col08,col09,col10);

执行计划是:

列存储计划

这对您来说可能看起来更糟,但是列存储提供了出色的压缩,并且整个执行计划在批处理模式下运行,并为所有贡献的列提供了过滤器。如果服务器有足够的线程和内存可用,这个替代方案真的可以实现。

最终,考虑到表的数量以及执行计划不佳或需要过多编译时间的可能性,我不确定这种规范化是正确的方法。我可能会首先更正非规范化表的架构(正确的数据类型等),可能会应用数据压缩......通常的事情。

如果数据确实属于星型模式,那么它可能需要更多的设计工作,而不仅仅是将重复的数据元素拆分到单独的表中。

于 2013-02-08T20:34:29.297 回答
4

为什么您认为加入 100 个表会成为性能问题?

如果所有的键都是主键,那么所有的连接都将使用索引。那么,唯一的问题是索引是否适合内存。如果它们适合内存,性能可能根本不是问题。

在做出这样的声明之前,您应该尝试使用 100 个连接进行查询。

此外,根据原始问题,参考表中只有几个值。表格本身适合单页,加上索引的另一页。这是 200 个页面,最多会占用几兆字节的页面缓存。不要担心优化,创建视图,如果您有性能问题,请考虑下一步。不要预设性能问题。

阐述:

这已经收到了很多评论。让我解释一下为什么这个想法可能不像听起来那么疯狂。

首先,我假设所有连接都是通过主键索引完成的,并且索引适合内存。

页面上的 100 个键占用 400 个字节。假设原始字符串平均每个有 40 个字节。这些将占用页面上的 4,000 字节,所以我们有一个节省。事实上,在之前的方案中,大约 2 条记录可以放在一个页面上。大约 20 个适合带有键的页面。

因此,使用键读取记录在 I/O 方面比读取原始记录快 10 倍左右。在关于少量值的假设下,索引和原始数据适合内存。

阅读20条记录需要多长时间?旧方法需要阅读 10 页。使用这些键,可以读取一页和 100*20 索引查找(可能需要额外查找来获取值)。根据系统的不同,2,000 个索引查找可能比额外的 9 页 I/O 更快——甚至快得多。我想说的是,这是一个合理的情况。它可能会或可能不会发生在特定系统上,但这并不疯狂。

这有点过于简单了。SQL Server 实际上并不一次读取一个页面。我认为它们是以 4 个一组的形式读取的(并且在进行全表扫描时可能会有前瞻读取)。但另一方面,在大多数情况下,表扫描查询的 I/O 绑定比处理器绑定更多,因此有空闲的处理器周期用于在引用表中查找值。

事实上,使用键可以比不使用它们更快地读取表,因为备用处理周期将用于查找(“备用”在读取时处理能力可用的意义上)。事实上,带有键的表可能小到足以放入可用的缓存中,从而大大提高了更复杂查询的性能。

实际性能取决于很多因素,例如字符串的长度、原始表(是否大于可用缓存?)、底层硬件同时进行 I/O 读取和处理的能力,以及对查询优化器正确执行连接的依赖。

我最初的观点是,先验地假设100 个连接是一件坏事是不正确的。该假设需要进行测试,使用密钥甚至可能会提高性能。

于 2013-02-08T19:56:18.177 回答
0

如果您的数据没有太大变化,您可能会受益于创建索引视图,它基本上实现了视图。

如果数据经常更改,这可能不是一个好的选择,因为服务器必须为视图基础表中的每次更改维护索引视图。

这是一篇很好的博客文章,可以更好地描述它。

来自博客:

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'
于 2013-02-08T20:46:04.083 回答