【问题标题】:SQL scalar subquery checking a row was found找到检查行的 SQL 标量子查询
【发布时间】:2015-05-07 03:32:49
【问题描述】:

简介

有时,您可以故意使用标量子查询来检查是否找到了不超过一行,而不是连接。例如,您可能有此查询来查找某些 person 行的国籍。

select p.name, c.iso from person p join person_country_map pcm on p.id = pcm.person join country c on pcm.country = c.id where p.id in (1, 2, 3)

现在,假设person_country_map 不是函数映射。一个给定的人可能会映射到多个国家 - 因此连接可能会找到不止一行。或者实际上,一个人可能根本不在映射表中,至少就任何数据库约束而言。

但是对于这个特定的查询,我碰巧知道我要查询的人将只有一个国家。这是我基于我的代码的假设。但我想在可能的情况下检查该假设 - 这样如果出现问题并且我最终尝试为一个拥有多个国家或没有国家映射行的人执行此查询,它就会死掉。

最多为一行添加安全检查

要检查多行,可以将连接重写为标量子查询:

select p.name, ( select c.iso from person_country_map pcm join country c on pc.country = c.id where pcm.person = p.id ) as iso from person p where p.id in (1, 2, 3)

现在,如果有人查询到两个或多个国家/地区的地图,DBMS 会出错。它不会像直接连接那样为同一个人返回多个条目。因此,即使在任何行返回到应用程序之前,我也可以更轻松地知道这个错误案例正在被检查。作为一个细心的程序员,我当然也可以检查应用程序。

是否可以对 没有 找到行进行安全检查?

但是如果 person_country_map 中没有一个人的行怎么办?在这种情况下,标量子查询将返回 null,使其大致相当于左连接。

(为了论证起见,假设从 person_country_map.country 到 country.id 的外键和 country.id 上的唯一索引,这样特定的连接将始终成功并准确找到一个国家行。)

我的问题

有没有什么方法可以在 SQL 中表达我想要一个并且完全是一个结果?一个普通的标量子查询是“零或一”。我想说的是

select 42, (select exactly one x from t where id = 55)

如果子查询不返回行,则查询在运行时失败。当然,上面的语法是虚构的,我相信它不会那么容易。

我使用的是 MSSQL 2008 R2,实际上这段代码是在一个存储过程中,所以如果需要我可以使用 TSQL。 (显然,普通的声明性 SQL 更可取,因为它也可以在视图定义中使用。)当然,我可以做一个 exists 检查,或者我可以选择一个 TSQL 变量的值,然后显式检查它是否为空,等等在。我什至可以将结果选择到一个临时表中,然后在该表上构建唯一索引作为检查。但是,是否没有更易读和更优雅的方式来标记我的假设,即子查询只返回一行,并让 DBMS 检查该假设?

【问题讨论】:

  • 你有没有考虑过一个用户定义的函数,如果没有一个结果就抛出异常?
  • @HABO,你的意思是一个通用的 check_not_null(x) 函数,如果 x 不为 null,则返回 x,否则返回 barfs(比如触发除以零)?
  • 我在想GetCountryForUser( UserId )。它要么返回指定用户的唯一CountryId,要么抱怨。不幸的是,UDF 中不允许使用 RaIsErrorThrow,因此您会得到类似 this 的内容。

标签: sql-server-2008 tsql join cardinality scalar-subquery


【解决方案1】:

你做的比它需要的更难

当然,您需要 person.id 到 person_country_map.person 的 FK 关系

您要么对 person_country_map.person 有唯一限制,要么没有?
如果您没有唯一约束,那么是的,您可以为同一个 person_country_map.person 拥有多个记录。

如果你想知道你是否有任何重复,那么

select pcm.person 
from person_country_map pcm
group by  pcm.person  
having count(*) > 1

如果不止一个,那么你只需要确定哪一个

select p.name,
       min(c.iso)
from person p
join person_country_map pcm
  on p.id = pcm.person
join country c
  on pcm.country = c.id 
where p.id in (1, 2, 3)
group by p.name

【讨论】:

  • 对 person_country_map.person 没有唯一限制,因为一般而言,一个人可以属于多个国家/地区。但是对于这个特定的应用程序,我知道我只是在查询属于一个的人。发明示例模式和查询是为了说明要检查标量子查询恰好返回一行的一般意义,因为数据库约束不能保证它,尽管我作为程序员相信它总是如此。
  • 这不是我的问题的答案,它是如何创建一个 SQL 标量子查询来检查是否返回了一个结果。 person 模式示例只是我为了说明这个问题而发明的。返回min(c.iso) 的查询是我过去使用过的一种技术,但如果发现多行,我不想确定是哪一种;我希望查询因一个大错误而失败,以便找出我的假设有什么问题。
  • 对不起,我应该更明确地说,模式只是为了说明一般观点而发明的。我已经编辑了问题文本。
【解决方案2】:

在 MSSQL 中,isnull 似乎仅在第一个参数为空时才计算其第二个参数。所以总的来说你可以说

select isnull(x, 0/0)

给出一个查询,如果非空则返回x,如果返回空则终止。将此应用于标量子查询,

select 42, isnull((select x from t where id = 55), 0/0)

将保证select x 子查询恰好找到一行。如果超过一个,DBMS本身就会产生错误;如果没有行,则触发除以零。

将此应用于原始示例会导致代码

select p.name, -- Get the unique country code of this person. -- Although the database constraints do not guarantee it in general, -- for this particular query we expect exactly one row. Check that. -- isnull(( select c.iso from person_country_map pcm join country c on pc.country = c.id where pcm.person = p.id ), 0/0) as iso from person p where p.id in (1, 2, 3)

为了获得更好的错误消息,您可以使用转换失败而不是除以零:

select 42, isnull((select x from t where id = 55), convert(int, 'No row found'))

虽然如果您从子查询中获取的值本身不是int,则需要进一步的convert 恶作剧。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2023-03-16
    • 1970-01-01
    • 2018-05-31
    • 1970-01-01
    • 1970-01-01
    • 2021-06-19
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多