在SQL Server 2012中,让我们有三个表:使用以下SQL创建的Foos,Lookup1和Lookup2:
CREATE TABLE Foos (
Id int NOT NULL,
L1 int NOT NULL,
L2 int NOT NULL,
Value int NOT NULL,
CONSTRAINT PK_Foos PRIMARY KEY CLUSTERED (Id ASC)
);
CREATE TABLE Lookup1 (
Id int NOT NULL,
Name nvarchar(50) NOT NULL,
CONSTRAINT PK_Lookup1 PRIMARY KEY CLUSTERED (Id ASC),
CONSTRAINT IX_Lookup1 UNIQUE NONCLUSTERED (Name ASC)
);
CREATE TABLE Lookup2 (
Id int NOT NULL,
Name nvarchar(50) NOT NULL,
CONSTRAINT PK_Lookup2 PRIMARY KEY CLUSTERED (Id ASC),
CONSTRAINT IX_Lookup2 UNIQUE NONCLUSTERED (Name ASC)
);
CREATE NONCLUSTERED INDEX IX_Foos ON Foos (
L1 ASC,
L2 ASC,
Value ASC
);
ALTER TABLE Foos WITH CHECK ADD CONSTRAINT FK_Foos_Lookup1
FOREIGN KEY(L2) REFERENCES Lookup1 (Id);
ALTER TABLE Foos CHECK CONSTRAINT FK_Foos_Lookup1;
ALTER TABLE Foos WITH CHECK ADD CONSTRAINT FK_Foos_Lookup2
FOREIGN KEY(L1) REFERENCES Lookup2 (Id);
ALTER TABLE Foos CHECK CONSTRAINT FK_Foos_Lookup2;
BAD PLAN :
以下SQL查询通过查找表获取Foos:
select top(1) f.* from Foos f
join Lookup1 l1 on f.L1 = l1.Id
join Lookup2 l2 on f.L2 = l2.Id
where l1.Name = 'a' and l2.Name = 'b'
order by f.Value
未充分利用IX_Foos
索引,请参阅http://sqlfiddle.com/#!6/cd5c1/1/0和the plan with data。
(它只选择一个查找表。)
好计划:
但是,如果我重写查询:
declare @l1Id int = (select Id from Lookup1 where Name = 'a');
declare @l2Id int = (select Id from Lookup2 where Name = 'b');
select top(1) f.* from Foos f
where f.L1 = @l1Id and f.L2 = @l2Id
order by f.Value
它按预期工作。它首先查找两个查找表,然后用于查找IX_Foos
索引。
是否可以使用提示在第一个查询(带连接)中强制SQL Server首先查找ID,然后将其用于IX_Foos
?
因为如果Foos
表非常大,第一个查询(带连接)会锁定整个表:(
注意:内部联接查询来自LINQ。或者是否可以强制实体框架中的LINQ使用declare
重写查询。由于在多个请求中进行查找,因此在更复杂的查询中可能会有更长的往返延迟。
注意2:在Oracle中它运行正常,这似乎是SQL Server的一个问题。
注意3:将TOP(1)
添加到select f.* from Foos ...
时,锁定问题会更明显。 (例如,您只需要获取最小值或最大值。)
更新 根据@Hoots的提示,我更改了IX_Lookup1和IX_Lookup2:
CONSTRAINT IX_Lookup1 UNIQUE NONCLUSTERED (Name ASC, Id ASC)
CONSTRAINT IX_Lookup2 UNIQUE NONCLUSTERED (Name ASC, Id ASC)
它有所帮助,但它仍在排序所有结果:
为什么从Foos
获取匹配f.L1
和f.L2
的所有 10,000 行,而不是仅占用第一行。 (IX_Foos
包含Value ASC
,因此它可以找到第一行而不处理所有10,000行并对它们进行排序。)之前使用声明变量的计划正在使用{{1} },所以它没有进行排序。
答案 0 :(得分:2)
查看查询计划,SQL Server在您已经放下的两个版本的SQL中使用相同的索引,它只是在sql的第二个版本中执行3个单独的SQL而不是1,因此在不同时间评估索引。
我已经检查过,我认为解决方法是更改索引,如下所示......
CONSTRAINT IX_Lookup1 UNIQUE NONCLUSTERED (Name ASC, ID ASC)
和
CONSTRAINT IX_Lookup2 UNIQUE NONCLUSTERED (Name ASC, ID ASC)
当它评估索引时,它不会消失并且需要从表数据中获取ID,因为它将在索引中获得它。这会改变你想要的计划,希望能阻止你所看到的锁定,但我不能保证它的那一面不是锁定不是我能够做到的事情。再现。
更新:我现在看到问题......
第二部分SQL实际上不使用基于集合的操作。简化你已经完成的工作......
select f.*
from Foos f
where f.L1 = 1
and f.L2 = 1
order by f.Value desc
只需寻找一个简单的索引即可获得已经订购的结果。
在SQL的第一位(如下所示)中,您将组合仅在各个表项上具有索引的不同数据集。 SQL的下两位使用相同的查询计划做同样的事情......
select f.* -- cost 0.7099
from Foos f
join Lookup1 l1 on f.L1 = l1.Id
join Lookup2 l2 on f.L2 = l2.Id
where l1.Name = 'a' and l2.Name = 'b'
order by f.Value
select f.* -- cost 0.7099
from Foos f
inner join (SELECT l1.id l1Id, l2.id l2Id
from Lookup1 l1, Lookup2 l2
where l1.Name = 'a' and l2.Name='b') lookups on (f.L1 = lookups.l1Id and f.L2=lookups.l2Id)
order by f.Value desc
我之所以放下这两个原因,是因为你可以很容易地在第二个版本中提示它没有基于设置但是单数并将其写下来......
select f.* -- cost 0.095
from Foos f
inner join (SELECT TOP 1 l1.id l1Id, l2.id l2Id
from Lookup1 l1, Lookup2 l2
where l1.Name = 'a' and l2.Name='b') lookups on (f.L1 = lookups.l1Id and f.L2=lookups.l2Id)
order by f.Value desc
当然,您只能知道子查询将带回单个记录,无论是否提及前1。然后将成本从0.7099降至0.095。我只能总结一下,既然有明确的单一记录输入,优化者现在知道事物的顺序可以由索引来处理,而不是必须手动'订购它们。
注意:0.7099对于单独运行的查询来说并不是非常大,即您几乎没有注意到,但如果它是更大一组执行的一部分,那么如果您愿意,可以降低成本。我怀疑这个问题更多的是为什么,我认为这是为了设置基于单一搜寻的操作。
答案 1 :(得分:0)
尝试像这样使用CTE
with cte as
(select min(Value) as Value from Foos f
join Lookup1 l1 on f.L1 = l1.Id
join Lookup2 l2 on f.L2 = l2.Id
where l1.Name = 'a' and l2.Name = 'b')
select top(1) * from Foos where exists (select * from cte where cte.Value=Foos.Value)
option (recompile)
这将两次减少Foos表和执行时间的逻辑读取。
set statistics io,time on
1)你的第一个带有@Hoots索引的查询 估计的子树成本= 0.888 表'Foos'。扫描计数1,逻辑读取59 CPU时间= 15 ms,经过时间= 151 ms。
2)此cte查询具有相同的索引 估计的子树成本= 0.397 表'Foos'。扫描计数2,逻辑读取34 CPU时间= 15 ms,经过时间= 66 ms。
但是,对于Foos中数十亿行的这种技术,只要我们触摸此表两次而不是您的第一次查询,就会非常慢。