为什么这个子查询似乎不起作用?

时间:2012-03-23 19:38:48

标签: sql-server-2008 tsql

在此之前,我不是在寻找重写。这是呈现给我的,我似乎无法弄清楚这是否是一般的错误或由于脚本的特殊性而发生的某种句法疯狂。好的,用设置说:

  • Microsoft SQL Server标准版(64位)

  • 版本10.50.2500.0

在位于通用数据库中的表上,定义为:

CREATE TABLE [dbo].[Regions](
    [RegionID] [int] NOT NULL,
    [RegionGroupID] [int] NOT NULL,
    [IsDefault] [bit] NOT NULL,
 CONSTRAINT [PK_Regions] PRIMARY KEY CLUSTERED
(
    [RegionID] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]

插入一些值:

INSERT INTO [dbo].[Regions]
([RegionID],[RegionGroupID],[IsDefault])
VALUES
(0,1,0),
(1,1,0),
(2,1,0),
(3,2,0),
(4,2,0),
(5,2,0),
(6,3,0),
(7,3,0),
(8,3,0)

现在运行查询(从每个组中选择一个,请记住没有重写建议!):

SELECT RXXID FROM (
   SELECT
       RXX.RegionID as RXXID,
       ROW_NUMBER() OVER (PARTITION BY RXX.RegionGroupID ORDER BY RXX.RegionGroupID) AS RXXNUM
   FROM Regions as RXX
) AS tmp
WHERE tmp.RXXNUM = 1

你应该得到:

RXXID
-----------
0
3
6

现在坚持更新声明(预设为0,之后全部选择):

UPDATE Regions SET IsDefault = 0

UPDATE Regions
SET IsDefault = 1
WHERE RegionID IN (
    SELECT RXXID FROM (
       SELECT
           RXX.RegionID as RXXID,
           ROW_NUMBER() OVER (PARTITION BY RXX.RegionGroupID ORDER BY RXX.RegionGroupID) AS RXXNUM
       FROM Regions as RXX
    ) AS tmp
    WHERE tmp.RXXNUM = 1
)


SELECT * FROM Regions
ORDER BY RegionGroupID

并获得此结果:

RegionID    RegionGroupID IsDefault
----------- ------------- ---------
0           1             1
1           1             1
2           1             1
3           2             1
4           2             1
5           2             1
6           3             1
7           3             1
8           3             1

zomg wtf lamaz?

虽然我并不自称是SQL大师,但这似乎既不合适也不正确。为了让事情变得更加疯狂,如果你放下主键,它似乎有效:

删除主键:

IF  EXISTS (SELECT * FROM sys.indexes WHERE object_id = OBJECT_ID(N'[dbo].[Regions]') AND name = N'PK_Regions')
ALTER TABLE [dbo].[Regions] DROP CONSTRAINT [PK_Regions]

并重新运行更新语句集,结果:

RegionID    RegionGroupID IsDefault
----------- ------------- ---------
0           1             1
1           1             0
2           1             0
3           2             1
4           2             0
5           2             0
6           3             1
7           3             0
8           3             0

这不是b?

有没有人知道这里发生了什么?我的猜测是某种子查询缓存,这是一个错误吗?这肯定不像SQL 应该做什么?

2 个答案:

答案 0 :(得分:9)

直接更新为CTE:

WITH tmp AS (
SELECT 
       RegionID as RXXID,
       RegionGroupID,
       IsDefault,
       ROW_NUMBER() OVER (PARTITION BY RegionGroupID ORDER BY RegionID) AS RXXNUM
   FROM Regions

) 
UPDATE tmp SET IsDefault = 1 WHERE RXXNUM = 1
select * from Regions

添加了更多列来说明。您可以在http://sqlfiddle.com/#!3/03913/9

上看到此信息

不是100%确定您的示例中发生了什么,但由于您按同一列进行分区和排序,因此您并不确定会获得相同的订单,因为它们都是并列的。你不应该按区域ID或其他列订购,就像我在sqlfiddle上做的那样?


回到你的问题:

如果将UPDATE(使用聚簇索引)更改为SELECT,则将返回所有9行。 如果删除PK并执行SELECT,则只能获得3行。返回更新声明。检查执行计划表明它们略有不同:

First (PK) Execution plan Second (No PK) Execution plan

您可以在这里看到的是,在第一个(使用PK)查询中,您将扫描聚集索引以获取外部引用,请注意它没有别名RXX。然后对于顶部的每一行,查找RXX。是的,由于您的行号排序,每个RegionGroupID的每个RegionID可以是row_number()1。我想,SQL Server会根据你的PK知道这个,并且可以说对于每个RegionID,这个RegionID可以是第1行。因此该语句相当有效。

在第二个查询中,没有索引,并且您在Regions上获得表扫描,然后使用RXX构建探测表,并以不同方式连接(单通道,ROW_NUMBER()每个行只能为1行现在是regiongroupid)。这种方式在该扫描中,每个RegionID只有一个ROW_NUMBER(),但你不能100%确定它每次都是相同的。

这意味着: 使用对每次执行都没有确定性顺序的子查询,应该避免使用多次传递(NESTED LOOP)连接类型,而是使用单次传递(MERGE OR HASH)连接。

要在不更改查询结构的情况下解决此问题,请将OPTION(HASH JOIN)或OPTION(MERGE JOIN)添加到第一个UPDATE:

因此,您需要以下更新语句(当您拥有PK时):

UPDATE Regions SET IsDefault = 0

UPDATE Regions 
SET IsDefault = 1 
WHERE RegionID IN (
    SELECT RXXID FROM (
       SELECT 
           RXX.RegionID as RXXID,
           ROW_NUMBER() OVER (PARTITION BY RXX.RegionGroupID ORDER BY RXX.RegionGroupID) AS RXXNUM
       FROM Regions as RXX
    ) AS tmp
    WHERE tmp.RXXNUM = 1 
)
OPTION (HASH JOIN)

SELECT * FROM Regions
ORDER BY RegionGroupID

以下是使用这两种连接类型的执行计划(注意实际行数:属性中的3):

Using MERGE JOIN Using HASH JOIN

答案 1 :(得分:3)

您用简单语言查询的内容如下:
对于Regions中的每一行,检查某个子查询中是否存在RegionID。意味着对Regions中的每一行执行子查询。 (我知道情况并非如此,但它是查询的语义)。

由于您使用RegionGroupID作为订单和分区,因此您实际上不知道将返回RegionID,因此每次检查子查询时,它很可能是新ID。

<强>更新

使用加入对导出的表执行更新,而不是使用 更改查询的语义,并且还更改了结果。

这可以按预期工作:

UPDATE R 
SET IsDefault = 1
FROM Regions as R
  inner join 
      (
        SELECT RXXID FROM (
           SELECT 
               RXX.RegionID as RXXID,
               ROW_NUMBER() OVER (PARTITION BY RXX.RegionGroupID ORDER BY RXX.RegionGroupID) AS RXXNUM
           FROM Regions as RXX
        ) AS tmp
        WHERE tmp.RXXNUM = 1 
      ) as C
    on R.RegionID = C.RXXID