单行设置表:联接与标量子查询的优缺点

Liz*_*eir 3 sql-server best-practices query-performance

我使用的应用程序使用 SQL Server 数据库,其中包含许多保存单行配置数据的表,有时在针对更传统的多行表的查询中需要这些表。我见过的大多数代码在处理单个查询时都通过联接访问这些表,但在最近的代码审查中,我看到了一种使用标量子查询的方法,大致如下:

Select T.Id
From dbo.SomeTable T
Where T.SomeValue > (Select Tolerance From dbo.Settings)
Run Code Online (Sandbox Code Playgroud)

虽然它显然有效,但我最初的反应是假设这是对我们标准实践的冒险违反,但我对表单进行了一些实验,发现“子查询返回超过 1 个值。当子查询遵循时,这是不允许的=、!=、<、<=、>、>= 或当子查询用作表达式时”错误。这使得这看起来可以避免意外 1:n 连接带来的不良行为的风险。(在实践中,这不应该是这些单行表的问题,它们相当强大,但我已经看到它出现在系统的其他地方。)

除了(可能非常便宜的)流聚合和断言之外,我的简单测试用例的执行计划看起来非常相似,我认为它们负责查询引擎在多行情况下识别和抛出错误的能力。

使用这种表是否有普遍接受的最佳实践?选择方法时我应该注意哪些主要优点和缺点?

(我知道使用变量来保存数据也是一种选择,但在我们的某些代码中这样做并不总是可行,因此我想重点关注这两种方法和/或任何其他方式的比较将其折叠到单个查询中。)

Jos*_*ell 6

我创建了一组演示表来看看这在实践中是如何运作的。

USE tempdb;
GO

DROP TABLE IF EXISTS dbo.Settings;
CREATE TABLE dbo.Settings
(
    Id int IDENTITY(1,1) NOT NULL,
    Tolerance int NOT NULL,

    CONSTRAINT PK_Settings PRIMARY KEY (Id)
);
GO

INSERT INTO dbo.Settings (Tolerance) VALUES (1073741823);

CREATE TABLE dbo.SomeTable
(
    Id int IDENTITY(1,1) NOT NULL,
    SomeValue int NOT NULL,

    CONSTRAINT PK_SomeTable PRIMARY KEY (Id)
);
CREATE NONCLUSTERED INDEX IX_SomeValue
ON dbo.SomeTable (SomeValue);
GO

INSERT INTO dbo.SomeTable
    (SomeValue)
SELECT
    SomeValue = v1.number
FROM master.dbo.spt_values v1
CROSS JOIN master.dbo.spt_values v2;
GO
Run Code Online (Sandbox Code Playgroud)

这会将约 6,000,000 行放入 SomeTable 表中,其中约 15,000 行比 Settings 表中的行 (1073741823) 大。

OP 中的标量子查询版本

SELECT st.Id
FROM dbo.SomeTable st
WHERE st.SomeValue > (SELECT s.Tolerance FROM dbo.Settings s);
Run Code Online (Sandbox Code Playgroud)

SSMS 中执行计划的屏幕截图显示并行嵌套循环连接的估计值非常糟糕

这会导致一种奇怪的查询计划。估计值相差几个数量级。这导致选择并行计划。然而,由于连接的上部输入上只有一行,因此来自下部输入的所有行最终都在一个线程上 - 导致并行性严重倾斜(并且完全无用)。

我没有很好地解释为什么这里的估计如此糟糕,但这似乎是这种方法的一个坏兆头,具体取决于您的表大小和数据分布。

内连接版本

SELECT st.Id
FROM dbo.SomeTable st
INNER JOIN dbo.Settings s
    ON st.SomeValue > s.Tolerance;
Run Code Online (Sandbox Code Playgroud)

SSMS 中执行计划的屏幕截图显示了具有良好估计的串行嵌套循环连接

这得到了更好的估计(仅下降了 2 倍),并且不保证并行性。

实际标量子查询

TOP (1)我们可以通过添加到内部查询来强制子查询为标量:

SELECT st.Id
FROM dbo.SomeTable st
WHERE st.SomeValue > (SELECT TOP (1) s.Tolerance FROM dbo.Settings s);
Run Code Online (Sandbox Code Playgroud)

SSMS 中执行计划的屏幕截图显示了串行嵌套循环与上部输入上的 top 1 的连接,具有良好的估计

这会产生与 INNER JOIN 版本非常相似的查询计划,其中 TOP 强制我们只从 Settings 表中获取一行。

使用参数代替

你提到要避免这种方法,但我很好奇。如果您的应用程序代码可以缓存/检索该值,并将查询传递给它(可能在存储过程中,我在这里使用 sp_executesql):

EXEC sys.sp_executesql 
    N'SELECT st.Id FROM dbo.SomeTable st WHERE st.SomeValue > @SomeValue',
    N'@SomeValue int',
    @SomeValue = 1073741823;
Run Code Online (Sandbox Code Playgroud)

SSMS 中执行计划的屏幕截图,显示通过 IX_SomeValue 索引对 SomeTable 表进行索引查找

这会产生良好的估计,并且通常会产生相当有效的执行计划,并且不必触及“设置”表。


注意:以上所有项目都取决于 SomeTable 表的 SomeValue 列上是否有索引。


基于所有这些,我同意京东的回答,即该INNER JOIN方法可能更可靠。如果您被迫使用子查询方法,请查看添加是否TOP (1)有帮助。

我想补充一点,如果你可以使用参数,那就更好了,但听起来这不可行。