【问题标题】:Can joining with an iTVF be as fast as joining with a temp table?加入 iTVF 可以像加入临时表一样快吗?
【发布时间】:2026-01-07 09:10:01
【问题描述】:

场景

快速背景知识:我正在尝试优化内联表值函数uf_GetVisibleCustomers(@cUserId) 的使用。 iTVF 包装了一个视图CustomerView 并过滤掉所有包含客户数据的行,这些客户不允许所提供的请求用户查看。这样,如果将来某些用户类型的选择标准发生变化,我们就不必在整个 SQL 代码库中实现该新条件一百次(夸张)。

然而,性能不是很好,所以我想在鼓励使用 iTVF 之前解决这个问题。在这里更改了数据库对象名称,以便更容易演示(希望如此)。

查询

在尝试优化我们的 iTVF uf_GetVisibleCustomers 时,我注意到以下 SQL ...

CREATE TABLE #tC ( idCustomer INT )

INSERT #tC
SELECT idCustomer
FROM [dbo].[uf_GetVisibleCustomers]('requester')

SELECT T.fAmount
FROM [Transactions] T
JOIN #tC C ON C.idCustomer = T.idCustomer

...比我原来的 SQL(IMO 更具可读性,可能会被使用)快几个数量级...

SELECT T.fAmount
FROM [Transactions] T
JOIN [dbo].[uf_GetVisibleCustomers]('requester') C ON C.idCustomer = T.idCustomer

我不明白这是为什么。前者(SQL 的顶部块)在相当适中的开发服务器上在 17 秒内返回约 700k 行。当服务器上没有其他用户活动时,后者(第二个 SQL 块)在大约十分钟内返回相同数量的行。也许值得注意的是有一个 WHERE 子句,但是为了简单起见,我在这里省略了它;两个查询都是一样的。

执行计划

下面是第一个的执行计划。如前所述,它享有自动并行性,而后一个查询不值得在此处显示,因为它非常庞大(扩展了整个 iTVF 和底层视图、子查询)。无论如何,后者在任何程度上也不会并行执行(AFAIK)。

我的问题

  1. 是否可以在没有临时表的情况下实现与第一个块相当的性能?
    • 也就是说,具有较慢 SQL 的相对简单性和人类可读性。
  2. 为什么加入临时表比加入 iTVF 更快?
  3. 为什么使用临时表比使用相同方式填充的内存表更快?

除了这些明确的问题之外,如果有人能指出我正确的方向以更好地理解这一点,那么我将非常感激。

【问题讨论】:

  • 在某种程度上取决于您的 sql server 版本,但临时表的平均统计数据比 TVF 单行的估计值要好...
  • 我认为问题不在于“内存表”——它的执行速度与临时表一样快。问题在于用户定义的函数——它们是逐行执行的,这比标准的 SQL 集合操作要慢得多。仅当该任务没有其他选项时,我才会使用 iTVF。您的 SQL 代码的可读性是一个主观因素 - 您可以通过使用更有意义/直观的名称(即“#tC”? - 我敢打赌这个表有一个更好的名称 :)
  • 贴出你的函数代码可能会有所帮助...

标签: sql-server tsql


【解决方案1】:

没有看到您的内联函数的 DDL - 很难说出问题所在。查看两个查询的实际执行计划也将有所帮助(也许您可以尝试:https://www.brentozar.com/pastetheplan/)。也就是说,我可以提供一些思考的食物。

正如您所提到的,iTVF 访问基础表、视图和相关索引。如果您的统计信息不是最新的,您可能会得到一个糟糕的计划,您的临时表不会发生这种情况。在那张纸条上,填充该临时表需要多长时间?

另一件需要注意的事情(这也是 DDL 有用的原因)是:Transactions.idCustomer 和 #TC.idCustomer 的数据类型是否相同?我在您发布的计划中看到哈希匹配,这对于两个 ID 之间的连接似乎很糟糕(嵌套循环或合并连接会更好)。这可能会减慢两个查询的速度,但似乎会对利用您的 iTVF 的查询产生更大的影响。

再次,这^^^是根据我的经验推测的。一些快速的尝试(不是作为烫发修复,而是用于故障排除): 1. 检查在使用 iTVF 时重新编译查询是否会加快速度(这可能是一个糟糕的统计数据或一个糟糕的执行计划被缓存和重用的标志) 2. 尝试为 iTVF 查询强制执行并行计划。您可以通过使用 Adam Machanic 的 make_parallel() 将 OPTION (QUERYTRACEON 8649) 添加到查询的末尾来做到这一点。

【讨论】: