子查询中的错误命名字段导致加入

Posted

技术标签:

【中文标题】子查询中的错误命名字段导致加入【英文标题】:Misnamed field in subquery leads to join 【发布时间】:2015-07-07 09:58:46 【问题描述】:

我遇到了由错误查询引起的数据丢失问题。 数据已恢复,但现在我想了解一下问题所在。

我在 SQL Server 2014 上遇到了这个问题,但我在 SQL Server 2000 和 PostgreSQL 上复制了它。具体来说,有一个 DELETE。在以下场景中,我使用 SELECT。

为 sql server 2014 创建表:

CREATE TABLE [dbo].[tmp_color](
    [color_id] [int] NOT NULL,
    [color_name] [nvarchar](50) NOT NULL,
    [color_cat] [int] NOT NULL,
 CONSTRAINT [PK_tmp_color] PRIMARY KEY CLUSTERED (
    [color_id] ASC
) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF
      , ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

CREATE TABLE [dbo].[tmp_color_cat](
    [catid] [int] NOT NULL,
    [catname] [nvarchar](50) NOT NULL,
 CONSTRAINT [PK_tmp_color_cat] PRIMARY KEY CLUSTERED (
    [catid] ASC
) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF
      , ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

还有 Postgres 版本:

CREATE TABLE tmp_color (
  color_id integer NOT NULL,
  color_name text,
  color_cat integer,
  CONSTRAINT tmp_color_pkey PRIMARY KEY (color_id)
);

CREATE TABLE tmp_color_cat (
  catid integer NOT NULL,
  catname text,
  CONSTRAINT tmp_color_cat_pkey PRIMARY KEY (catid)
);

数据填充(适用于两个 RDBMS):

INSERT INTO tmp_color_cat (catid, catname) VALUES (1, 'magic color');
INSERT INTO tmp_color_cat (catid, catname) VALUES (2, 'normal color');

INSERT INTO tmp_color (color_id, color_name, color_cat) VALUES (1, 'red', 1);
INSERT INTO tmp_color (color_id, color_name, color_cat) VALUES (2, 'green', 2);
INSERT INTO tmp_color (color_id, color_name, color_cat) VALUES (3, 'black', 1);

以下 SELECT 错误

SELECT color_cat
FROM tmp_color_cat;

因为color_cat 不存在于tmp_color_cat。 但是,当您在子查询中使用它时:

SELECT * FROM tmp_color
WHERE color_cat IN(
    SELECT color_cat
    FROM tmp_color_cat
    WHERE catname = 'magic color'
);

返回来自tmp_color 的每条记录。 脚本中的逻辑错误很明显:开发人员写了错误的列来识别类别。如果您要删除记录而不是选择它们,您将删除整个表格。不好。

这是期望的行为吗?还是子查询设计的结果?

通过观察SQL Server的执行计划,逻辑操作是Left Semi Join。

我找到了几个帖子,一个 for PostgreSQL 和一个 for SQL Server。有没有什么好的文档可以发送给开发者组,解释为什么这不是错误?

如何避免此类问题?我的第一个想法是使用别名。别名很好。

【问题讨论】:

您的第二个链接似乎正是您所描述的,所以我不太明白您的问题。我建议始终在涉及多个表的情况下使用别名,即使看起来没有必要,这是一种很好的编程习惯(如您的事件所示) 子选择中的 color_cat 引用了外部查询中的列,并且实质上将查询更改为 where color_cat = colo_cat。这就是 SQL 标准定义可见性规则的方式(如您的第一个链接中所述)。最好的方法(正如您已经注意到的)是始终使用别名和完全限定的列名 (alias.column),然后这个错误就会变得很明显。 总是。采用。别名。 :) 即使是从单个表查询 - 稍后您可能会使用另一个表扩展此查询,然后您可能会再次遇到问题。 是的,@a_horse_with_no_name,问题很清楚。我的环境中的问题出在 SQL Server 中。第二个链接是发送给开发人员的好资源吗? 我们可以假设当前的 Postgres 版本是 9.4? 【参考方案1】:

Postgres 的权威报价

子查询的范围包括外部查询的所有可见列。不合格的名称首先解析为内部查询,然后向外扩展搜索。 分配表别名并使用这些别名来限定列名以消除任何歧义 - 正如您已经暗示过的那样。

这是example in the Postgres manual with a definitive statement explaining the scope:

SELECT ... FROM fdt WHERE c1 IN (SELECT c3 FROM t2 WHERE c2 = fdt.c1 + 10)

[...]

仅当c1 也是 子查询的派生输入表中的列。但排位赛 列名即使在不需要时也能增加清晰度。这个例子 显示了外部查询的列命名范围如何扩展到其内部查询。

我的大胆强调。

手册同一章节的示例列表中还有一个带有EXISTS 半连接的示例。这通常是WHERE x IN (subquery)更好的替代方案。但在这种特殊情况下,您也不需要。见下文。

一个例子:

sql query to extract new records

数据库设计、命名约定

这场灾难是由于列名混淆而发生的。在您的表定义中清晰一致的命名约定将大大降低发生这种情况的可能性。 任何 RDBMS 都是如此。使它们尽可能长以清晰,否则尽可能短。无论您的政策是什么,都要保持一致。

对于 Postgres,我建议:

CREATE TABLE colorcat (
  colorcat_id integer NOT NULL PRIMARY KEY,
  colorcat    text UNIQUE NOT NULL
);

CREATE TABLE color (
  color_id    integer NOT NULL PRIMARY KEY,
  color       text NOT NULL,
  colorcat_id integer REFERENCES colorcat   -- assuming an FK
);

您已经有了合法的、小写的、不带引号的标识符。这很好

使用一致政策。不一致的政策比糟糕的政策更糟糕。不是color_name(带下划线)与catname

我很少在标识符中使用“名称”。它不会添加信息,只会使它们更长。所有标识符都是名称。你选择了cat_name,去掉了实际携带信息的color,添加了不携带信息的name。如果您的数据库中有其他“类别”,这很常见,您将拥有多个cat_name,它们很容易在更大的查询中发生冲突。我宁愿使用colorcat(就像表名一样)。

让名称表明列中的内容。对于颜色类别的 ID,colorcat_id 是一个不错的选择。 id 不是描述性的,colorcat 会产生误导。

FK 列colorcat_id 可以与引用的列同名。两者都有完全相同的内容。还允许在连接中使用 USING 的短语法。

更多细节的相关答案:

How to implement a many-to-many relationship in PostgreSQL?

更好的查询

基于我设想的设计:

SELECT c.*
FROM   colorcat cc
JOIN   color c USING (colorcat_id)
WHERE  cc.colorcat = 'magic color';

这是假设 colorcatcolor 之间的 1:n 关系(您没有指定,但似乎很可能)。

鲜为人知(因为语法在 SQL Server 等其他 RDBMS 中有所不同),您也可以join in additional tables in a DELETE

DELETE FROM color c
USING  colorcat cc
WHERE  cc.colorcat = 'magic color'
AND    cc.colorcat_id = c.colorcat_id;

【讨论】:

这很好。感谢您的提示,我将与开发人员分享。【参考方案2】:

这是 SQL Server 的已知行为。使用别名可以防止这种情况发生

SELECT * FROM tmp_color
WHERE color_cat IN(
    SELECT A.color_cat
    FROM tmp_color_cat As A
    WHERE A.catname = 'magic color'
);

上面的查询会报错

Msg 207, Level 16, State 1, Line 3
Invalid column name 'color_cat'.

【讨论】:

【参考方案3】:

在您的情况下,别名可以解决问题,因为它的编写方式只是引用外部查询 tmp_color 中的表,从而返回所有内容。

所以你会按照你的建议重写这个:

SELECT * FROM tmp_color t1
WHERE t1.color_cat IN(
    SELECT t2.color_cat
    FROM tmp_color_cat t2
    WHERE t2.catname = 'magic color'
);

这表明你的逻辑有错误:

列名无效

另一种安全的编写方法是使用JOIN。请注意,由于JOIN 规范没有任何列冲突,因此我省略了下面的别名。如果表中的任何列名相同,那么您将收到 Ambiguous column 错误。为清楚起见,最佳做法是始终使用别名。

SELECT * 
FROM #tmp_color
INNER JOIN #tmp_color_cat ON color_cat = catid
WHERE catname = 'magic color'

等价的DELETE 是:

DELETE t1
FROM #tmp_color t1
INNER JOIN #tmp_color_cat ON color_cat = catid
Where catname = 'magic color'

完整的可运行示例:

CREATE TABLE #tmp_color
    (
      color_id INT ,
      color_name NVARCHAR(50) ,
      color_cat INT
    )

CREATE TABLE #tmp_color_cat
    (
      catid INT ,
      catname NVARCHAR(50) NOT NULL,
    )

INSERT INTO #tmp_color_cat (catid, catname) VALUES (1, 'magic color');
INSERT INTO #tmp_color_cat (catid, catname) VALUES (2, 'normal color');

INSERT INTO #tmp_color (color_id, color_name, color_cat) VALUES (1, 'red', 1);
INSERT INTO #tmp_color (color_id, color_name, color_cat) VALUES (2, 'green', 2);
INSERT INTO #tmp_color (color_id, color_name, color_cat) VALUES (3, 'black', 1);

DELETE t1
FROM #tmp_color t1
INNER JOIN #tmp_color_cat ON color_cat = catid
Where catname = 'magic color'

SELECT * 
FROM #tmp_color

DROP TABLE #tmp_color
DROP TABLE #tmp_color_cat

产生剩余的行:

color_id    color_name  color_cat
2           green       2

【讨论】:

【参考方案4】:

服务器试图找出在您的 SQL 语句范围内的任何表/视图/子查询中是否存在提到的列名。

其实最好使用别名来避免这样的错误和误解:

  SELECT * FROM tmp_color tc
  WHERE color_cat IN(
      SELECT tcc.catid
      FROM tmp_color_cat tcc
      WHERE catname = 'magic color'
  );

所以,如果你尝试使用这样的构造:

SELECT * FROM tmp_color tc
WHERE color_cat IN(
    SELECT tcc.color_cat
    FROM tmp_color_cat tcc
    WHERE catname = 'magic color'
);

您将收到一条错误消息:

消息 207,第 16 级,状态 1,第 3 行 列名“color_cat”无效。

【讨论】:

以上是关于子查询中的错误命名字段导致加入的主要内容,如果未能解决你的问题,请参考以下文章

无法深入访问字段多个子查询

Postgresql中的Inner Join子查询导致错误

SQL错误使用多个子查询的字段列表中的未知列

加入两个包含 SUM() 函数的子查询时出现无效操作错误

访问:使用子查询中的计数更新查询 - 错误或所有结果

如何加入/子查询第二个表