在 where 子句中同时使用“包含”和“=”时查询速度很慢

Hoo*_*igi 8 sql-server full-text-search sql-server-2008-r2

以下查询需要大约 10 秒才能在具有 12k 条记录的表上完成

select top (5) *
from "Physician"
where "id" = 1 or contains("lastName", '"a*"')
Run Code Online (Sandbox Code Playgroud)

但是,如果我将 where 子句更改为

where "id" = 1
Run Code Online (Sandbox Code Playgroud)

或者

where contains("lastName", '"a*"')
Run Code Online (Sandbox Code Playgroud)

它会立即返回。

两列都被索引,lastName 列也被全文索引。

CREATE TABLE Physician
(
   id         int identity    NOT NULL,
   firstName  nvarchar(100)   NOT NULL,
   lastName   nvarchar(100)   NOT NULL
);

ALTER TABLE Physician
  ADD CONSTRAINT Physician_PK
  PRIMARY KEY CLUSTERED (id);

CREATE NONCLUSTERED INDEX Physician_IX2
   ON Physician (firstName ASC);

CREATE NONCLUSTERED INDEX Physician_IX3
   ON Physician (lastName ASC);

CREATE FULLTEXT INDEX
    ON "Physician" ("firstName" LANGUAGE 0x0, "lastName" LANGUAGE 0x0)
    KEY INDEX "Physician_PK"
    ON "the_catalog"
    WITH stoplist = off;
Run Code Online (Sandbox Code Playgroud)

这是执行计划

可能是什么问题呢?

Ran*_*gen 12

你的执行计划

查看查询计划时,我们可以看到触及一个索引来服务两个过滤操作。

在此处输入图片说明

非常简单地说,由于 TOP 运算符,设置了行目标。可以在此处找到有关行目标的更多信息和先决条件

来自同一来源:

行目标策略通常意味着优先于非阻塞导航操作(例如,嵌套循环连接、索引查找和查找)而不是阻塞、基于集合的操作(如排序和散列)。每当客户端可以从快速启动和稳定的行流中受益(可能有更长的整体执行时间 - 请参阅上面的 Rob Farley 的帖子)时,这会很有用。还有更明显和传统的用途,例如一次显示一个页面的结果。

使用设置了行目标的左半连接将整个表探测到过滤器中,希望尽可能快速有效地返回 5 行。

这不会发生,导致对 .Fulltextmatch TVF 进行多次迭代。

在此处输入图片说明


再创造

根据您的计划,我能够在一定程度上重现您的问题:

CREATE TABLE dbo.Person(id int not null,lastname varchar(max));

CREATE UNIQUE INDEX ui_id ON  dbo.Person(id)
CREATE FULLTEXT CATALOG ft AS DEFAULT;  
CREATE FULLTEXT INDEX ON dbo.Person(lastname)   
   KEY INDEX ui_id   
   WITH STOPLIST = SYSTEM;  
GO  

INSERT INTO dbo.Person(id,lastname)
SELECT top(12000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)),
REPLICATE(CAST('A' as nvarchar(max)),80000)+ CAST(ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) as varchar(10))
FROM master..spt_values spt1
CROSS APPLY master..spt_values spt2;
CREATE CLUSTERED INDEX cx_Id on dbo.Person(id);
Run Code Online (Sandbox Code Playgroud)

运行查询

SELECT TOP (5) *
FROM dbo.Person
WHERE "id" = 1 OR contains("lastName", '"B*"');
Run Code Online (Sandbox Code Playgroud)

结果与您的查询计划相当:

在此处输入图片说明

在上面的例子中,B 不存在于全文索引中。因此,它取决于参数和数据查询计划的效率。

对此的更好解释可以在Paul White 的Row Goals, Part 2: Semi Joins 中找到

...换句话说,在应用程序的每次迭代中,只要找到第一个匹配项,我们就可以停止查看输入 B,使用下推连接谓词。这正是行目标的优点:生成优化计划的一部分以快速返回前 n 个匹配行(此处 n = 1)。

例如,更改谓词以便更快地找到结果(在扫描开始时)。

select top (5) *
from dbo.Person
where "id" = 124 
or contains("lastName", '"A*"');
Run Code Online (Sandbox Code Playgroud)

在此处输入图片说明

where "id" = 124由于全文索引谓词已经返回 5 行,满足TOP()谓词,因此被消除。

结果也显示了这一点

id lastname 
1  'AAA...'   
2  'AAA...'
3  'AAA...'
4  'AAA...'
5  'AAA...'
Run Code Online (Sandbox Code Playgroud)

和 TVF 处决:

在此处输入图片说明

插入一些新行

INSERT INTO dbo.Person
SELECT 12001, REPLICATE(CAST('B' as nvarchar(max)),80000);
INSERT INTO dbo.Person
SELECT 12002, REPLICATE(CAST('B' as nvarchar(max)),80000);
Run Code Online (Sandbox Code Playgroud)

运行查询以查找这些先前插入的行

SELECT TOP (2) *
from dbo.Person
where "id" = 1
or contains("lastName", '"B*"');
Run Code Online (Sandbox Code Playgroud)

这再次导致几乎所有行的迭代次数过多,无法返回找到的最后一个值。

在此处输入图片说明

在此处输入图片说明

id   lastname
1     'AAA...'
12001 'BBB...'
Run Code Online (Sandbox Code Playgroud)

解决

使用跟踪标志 4138 删除行目标时

SELECT TOP (5) *
FROM dbo.Person
WHERE "id" = 124 
OR contains("lastName", '"B*"')
OPTION(QUERYTRACEON 4138 );
Run Code Online (Sandbox Code Playgroud)

优化器使用更接近于实现 a 的连接模式UNION,在我们的例子中这是有利的,因为它将谓词下推到它们各自的聚集索引查找,并且不使用行目标左半连接运算符。

在此处输入图片说明

另一种写法,不使用上面提到的跟踪标志:

SELECT top (5) *
FROM
(
SELECT * 
FROM dbo.Person
WHERE "id" = 1 
UNION
SELECT * 
FROM dbo.Person
WHERE contains("lastName", '"B*"')
 ) as A;
Run Code Online (Sandbox Code Playgroud)

使用结果查询计划:

在此处输入图片说明

直接应用全文功能的地方

在此处输入图片说明

作为旁注,对于 op,查询优化器修补程序跟踪标志 4199 解决了他的问题。他通过添加OPTION(QUERYTRACEON(4199))到查询来实现这一点。我最终无法重现该行为。此修补程序确实包含半连接优化:

跟踪标志:4102 功能:SQL 9 - 如果查询的执行计划包含半连接运算符,则查询性能会降低 通常,当查询包含 IN 关键字或 EXISTS 关键字时,会生成半连接运算符。启用标志 4102 和 4118 来克服这个问题。

来源


额外的

在基于成本的优化期间,优化器还可以向执行计划添加一个索引假脱机,由LogOp_Spool Index on fly Eager (或物理对应物)实现

它对我的数据集执行此操作,TOP(3)但不适用于TOP(2)

SELECT TOP (3) *
from dbo.Physician
where "id" = 1
or contains("lastName", '"B*"')  
Run Code Online (Sandbox Code Playgroud)

在此处输入图片说明

在第一次执行时,急切假脱机在返回 Predicate 请求的行子集之前读取并存储整个输入 后来执行从工作表中读取并返回相同或不同的行子集,而不必执行子行再次节点。

来源

将搜索谓词应用于此索引热切假脱机:

在此处输入图片说明