草庐IT

【SQL 中级语法 2】自连接的用法

好大一个绿菠萝 2023-07-15 原文

SQL的连接运算根据其特征的不同,有着不同的名称,如内连接、外连接、交叉连接等。一般来说,这些连接大都是以不同的表或视图为对象进行的,但针对相同的表或相同的视图的连接也并没有被禁止。针对相同的表进行的连接被称为“自连接”(self join)。一旦熟练掌握自连接技术,我们便能快速地解决很多问题。

理解自连接能增进我们对“面向集合”这一SQL语言重要特征的理解。面向对象语言以对象的方式来描述世界,而面向集合语言SQL以集合的方式来描述世界。自连接技术充分体现了SQL面向集合的特性。

目录

1 可重排列、排列、组合

2 删除重复行

3 查找局部不一致的列

4 排序

小结

____________________________________________________________________________

1 可重排列、排列、组合

Products

name(商品名称)  price(价格)
苹果            50
橘子            100
香蕉            80

这里所说的组合其实分为两种类型。一种是有顺序的有序对(ordered pair),另一种是无顺序的无序对(unordered pair)。有序对用尖括号括起来,如<1,2>;无序对用花括号括起来,如{1, 2}。在有序对里,如果元素顺序相反,那就是不同的对,因此<1, 2>≠<2, 1>;而无序对与顺序无关,因此{1, 2}={2, 1}。用学校里学到的术语来说,这两类分别对应着“排列”和“组合”。

用SQL生成有序对非常简单。像下面这样通过交叉连接生成笛卡儿积(直积),就可以得到有序对。

    --用于获取可重排列的SQL语句
    SELECT P1.name AS name_1, P2.name AS name_2
      FROM Products P1, Products P2;

执行结果

    name_1       name_2
    ------      ------
    苹果         苹果
    苹果         橘子
    苹果         香蕉
    橘子         苹果
    橘子         橘子
    橘子         香蕉
    香蕉         苹果
    香蕉         橘子
    香蕉         香蕉

执行结果里每一行(记录)都是一个有序对。因为是可重排列,所以结果行数为3的平方=9。结果里出现了(苹果,苹果)这种由相同元素构成的对,而且(橘子,苹果)和(苹果,橘子)这种只是调换了元素顺序的对也被当作不同的对了。这是因为,该查询在生成结果集合时会区分顺序。

如何更改才能排除掉由相同元素构成的对呢?首先,为了去掉(苹果,苹果)这种由相同元素构成的对,需要像下面这样加上一个条件,然后再进行连接运算。

    --用于获取排列的SQL语句
    SELECT P1.name AS name_1, P2.name AS name_2
      FROM Products P1, Products P2
     WHERE P1.name <> P2.name;

执行结果 

    name_1       name_2
    ------       ------
    苹果         橘子
    苹果         香蕉
    橘子         苹果
    橘子         香蕉
    香蕉         苹果
    香蕉         橘子

加上WHERE P1.name <> P2.name这个条件以后,就能排除掉由相同元素构成的对,结果行数为排列=6。

 ● P1里的“苹果”行的连接对象为P2里的“橘子、香蕉”这两行

● P1里的“橘子”行的连接对象为P2里的“苹果、香蕉”这两行

● P1里的“香蕉”行的连接对象为P2里的“苹果、橘子”这两行

相同的表的自连接和不同表间的普通连接并没有什么区别,自连接里的“自”这个词也没有太大的意义。

接下来对(苹果,橘子)和(橘子,苹果)这样只是调换了元素顺序的对进行去重。

    --用于获取组合的SQL语句
    SELECT P1.name AS name_1, P2.name AS name_2
      FROM Products P1, Products P2
     WHERE P1.name > P2.name;

执行结果

    name_1       name_2
    ------       ------
    苹果          橘子
    香蕉          橘子
    香蕉          苹果

在加上“不等于”这个条件后,这条SQL语句所做的是,按字符顺序排列各商品,只与“字符顺序比自己靠前”的商品进行配对,结果行数为组合C(2,3)=3。

想要获取3个以上元素的组合时,像下面这样简单地扩展一下就可以了。这次的样本数据只有3行,所以结果应该只有1行。

    --用于获取组合的SQL语句:扩展成3列
    SELECT P1.name AS name_1, P2.name AS name_2, P3.name AS name_3
      FROM Products P1, Products P2, Products P3
     WHERE P1.name > P2.name
      AND P2.name > P3.name;
    name_1    name_2     name_3
    -------   --------   --------
    香蕉       苹果        橘子

使用等号“=”以外的比较运算符,如“<、>、<>”进行的连接称为“非等值连接”。这里将非等值连接与自连接结合使用了,因此称为“非等值自连接”。在需要获取列的组合时,会经常使用。

“, >”和“<”等比较运算符不仅可以用于比较数值大小,也可以用于比较字符串(比如按字典序进行比较)或者日期等。

2 删除重复行

在关系数据库的世界里,重复行和NULL一样,都不受欢迎。

 重复行有多少行都没有关系。通常,如果重复的列里不包含主键,就可以用主键来处理,但像这道例题一样所有的列都重复的情况,则需要使用由数据库独自实现的行ID。这里的行ID可以理解成拥有“任何表都可以使用的主键”这种特征的虚拟列。在下面的SQL语句里,我们使用的是Oracle数据库里的rowid。

    --用于删除重复行的SQL语句(1):使用极值函数
    DELETE FROM Products P1
     WHERE rowid < ( SELECT MAX(P2.rowid)
                      FROM Products P2
                      WHERE P1.name = P2. name
                        AND P1.price = P2.price ) ;

(没理解)

 这里的重点也与前面的例题一样,对于在SQL语句里被赋予不同名称的集合,我们应该将其看作完全不同的集合。这个子查询会比较两个集合P1和P2,然后返回商品名称和价格都相同的行里最大的rowid所在的行。

于是,由于苹果和香蕉没有重复行,所以返回的行是“1:苹果”“5:香蕉”,而判断条件是不等号,所以该行不会被删除。而对于“橘子”这个商品,程序返回的行是“4:橘子”,那么rowid比4小的两行——“2:橘子”和“3:橘子”都会被删除。

(还是没理解)

通过这道例题我们明白,如果从物理表的层面来理解SQL语句,抽象度是非常低的。“表”“视图”这样的名称只反映了不同的存储方法,而存储方法并不会影响到SQL语句的执行和结果,因此无需有什么顾虑(在不考虑性能的前提下)。无论表还是视图,本质上都是集合——集合是SQL能处理的唯一的数据结构。

用前面介绍过的非等值连接的方法也可以写出与这里的执行过程一样的SQL语句。

    --用于删除重复行的SQL语句(2):使用非等值连接
    DELETE FROM Products P1
     WHERE EXISTS ( SELECT *
                      FROM Products P2
                    WHERE P1.name = P2.name
                      AND P1.price = P2.price
                      AND P1.rowid < P2.rowid );

3 查找局部不一致的列

 如果家庭ID一样,住址也必须一样。因此这里需要修改一下。那么我们该如何找出像前田夫妇这样的“是同一家人但住址却不同的记录”呢?

实现办法有几种,不过如果用非等值自连接来实现,代码会非常简洁。

    --用于查找是同一家人但住址却不同的记录的SQL语句
    SELECT DISTINCT A1.name, A1.address
      FROM Addresses A1, Addresses A2
     WHERE A1.family_id = A2.family_id
      AND A1.address <> A2.address ;

 和前面的住址表那道题的结构完全一样。家庭ID→价格;住址→商品名称

    --用于查找价格相等但商品名称不同的记录的SQL语句
    SELECT DISTINCT P1.name, P1.price
      FROM Products P1, Products P2
     WHERE P1.price = P2.price
      AND P1.name <> P2.name;

执行结果

    name       price
    ------    ------
    苹果          50
    葡萄          50
    草莓         100
    橘子         100
    香蕉         100

请注意,这里与住址表那道例题不同,如果不加上DISTINCT,结果里就会出现重复行。关键在于价格相同的记录的条数。而就住址表来说,如果前田家有孩子,那么不在代码中加上DISTINCT的话,结果里才会出现重复行。不过,这道例题使用的是连接查询,如果改用关联子查询,就不需要DISTINCT了。

4 排序

按照价格从高到低的顺序,对下面这张表里的商品进行排序。我们让价格相同的商品位次也一样,而紧接着它们的商品则有两种排序方法,一种是跳过之后的位次,另一种是不跳过之后的位次。

 首先想到的肯定是窗口函数

    --排序:使用窗口函数
    SELECT name, price,
          RANK() OVER (ORDER BY price DESC) AS rank_1,
          DENSE_RANK() OVER (ORDER BY price DESC) AS rank_2
      FROM Products;

执行结果

    name         price    rank_1    rank_2
    -------    ------   -------   -------
    橘子          100          1          1
    西瓜           80          2          2
    苹果           50          3          3
    香蕉           50          3          3
    葡萄           50          3          3
    柠檬           30          6          4

但是,RANK函数还属于标准SQL中较新的功能,目前只有个别数据库实现了它,还不能用于MySQL数据库。(这我不知道现在可不可以了)

考虑不依赖具体数据库,使用非等值自连接(真的很常用)来实现:

    --排序从1开始。如果已出现相同位次,则跳过之后的位次
    SELECT P1.name,
          P1.price,
          (SELECT COUNT(P2.price)
              FROM Products P2
            WHERE P2.price > P1.price) + 1 AS rank_1
      FROM Products P1
      ORDER BY rank_1;

执行结果

    name    price     rank
    -----   ------   ------
    橘子      100         1
    西瓜       80         2
    苹果       50         3
    葡萄       50         3
    香蕉       50         3
    柠檬       30         6

去掉标量子查询后边的+1,就可以从0开始给商品排序,如果修改成COUNT(DISTINCT P2.price),那么存在相同位次的记录时,就可以不跳过之后的位次,而是连续输出(相当于DENSE_RANK函数)。由此可知,这条SQL语句可以根据不同的需求灵活地进行扩展,实现不同的排序方式。(这也不理解)

这条SQL语句会生成这样几个“同心圆状的”递归集合,然后数这些集合的元素个数。正如“同心圆状”这个词的字面意思那样,这几个集合之间存在如下包含关系。

 实际上,“通过递归集合来定义数”这个想法并不算新颖。有趣的是,它和集合论里沿用了100多年的自然数(包含0)的递归定义(recursive definition)在思想上不谋而合。研究这种思想的学者形成了几个流派,其中和这道例题的思路类型相同的是计算机之父、数学家冯·诺依曼提出的想法。冯·诺依曼首先将空集定义为0,然后按照下面的规则定义了全体自然数。

    0 = φ
    1 = {0}
    2 = {0, 1}
    3 = {0, 1, 2}
        ·
        ·
        ·

定义完0之后,用0来定义1,然后用0和1来定义2,再用0、1和2来定义3……以此类推。这种做法与上面例题里的集合S0~S3在生成方法和结构上都是一样的(正是为了便于比较,例题里的位次才从0开始)。这道题很好地直接结合了SQL和集合论,而联系二者的正是自连接。

(鼓掌)

顺便说一下,这个子查询的代码还可以像下面这样按照自连接的写法来改写。

    --排序:使用自连接
    SELECT P1.name,
          MAX(P1.price) AS price,
          COUNT(P2.name) +1 AS rank_1
      FROM Products P1 LEFT OUTER JOIN Products P2
        ON P1.price < P2.price
     GROUP BY P1.name
     ORDER BY rank_1;

(啊我没看懂!)

去掉这条SQL语句里的聚合并展开成下面这样,就可以更清楚地看出同心圆状的包含关系(为了看得更清楚,我们从表中去掉价格重复的行,只留下橘子、西瓜、葡萄和柠檬这4行)。

    --不聚合,查看集合的包含关系
    SELECT P1.name, P2.name
      FROM Products P1 LEFT OUTER JOIN Products P2
        ON P1.price < P2.price;

 集合每增大1个,元素也增多1个,通过数集合里元素的个数就可以算出位次。

这个查询里还有一个特别的技巧,也许大家已经注意到了。那就是前面的例题里用的连接都是标准的内连接,而这里用的却是外连接。如果将外连接改为内连接看一看,马上就会明白这样做的原因。

    --排序:改为内连接
    SELECT P1.name,
          MAX(P1.price) AS price,
          COUNT(P2.name) +1 AS rank_1
      FROM Products P1 INNER JOIN Products P2
        ON P1.price < P2.price
     GROUP BY P1.name
     ORDER BY rank_1;

执行结果

    --没有第1名!
    name     price     rank_1
    ------   -------   --------
    西瓜       80          2
    葡萄       50          3
    苹果       50          3
    香蕉       50          3
    柠檬       30          6

没错,第1名“橘子”竟然从结果里消失了。关于这一点大家思考一下就能理解了。没有比橘子价格更高的水果,所以它被连接条件P1.price < P2.price排除掉了。外连接就是这样一个用于将第1名也存储在结果里的小技巧(这个小技巧在1-6节还会再次发挥重要作用)。

(啊,我还是没有完全理解,感觉会又不太会!原来sql可以这名难)

小结

自连接是不亚于CASE表达式的重要技术,请一定熟练掌握。最后说一个需要注意的地方,与多表之间进行的普通连接相比,自连接的性能开销更大(特别是与非等值连接结合使用的时候),因此用于自连接的列推荐使用主键或者在相关列上建立索引。本节例题里出现的连接大多使用的是主键。

1.自连接经常和非等值连接结合起来使用。

2.自连接和GROUP BY结合使用可以生成递归集合。

3.将自连接看作不同表之间的连接更容易理解。

4.应把表看作行的集合,用面向集合的方法来思考。

5.自连接的性能开销更大,应尽量给用于连接的列建立索引。

———————————————————————————————————————————

更多内容点击主页查看~

有关【SQL 中级语法 2】自连接的用法的更多相关文章

  1. ruby - 树顶语法无限循环 - 2

    我脑子里浮现出一些关于一种新编程语言的想法,所以我想我会尝试实现它。一位friend建议我尝试使用Treetop(Rubygem)来创建一个解析器。Treetop的文档很少,我以前从未做过这种事情。我的解析器表现得好像有一个无限循环,但没有堆栈跟踪;事实证明很难追踪到。有人可以指出入门级解析/AST指南的方向吗?我真的需要一些列出规则、常见用法等的东西来使用像Treetop这样的工具。我的语法分析器在GitHub上,以防有人希望帮助我改进它。class{initialize=lambda(name){receiver.name=name}greet=lambda{IO.puts("He

  2. ruby - 续集在添加关联时访问many_to_many连接表 - 2

    我正在使用Sequel构建一个愿望list系统。我有一个wishlists和itemstable和一个items_wishlists连接表(该名称是续集选择的名称)。items_wishlists表还有一个用于facebookid的额外列(因此我可以存储opengraph操作),这是一个NOTNULL列。我还有Wishlist和Item具有续集many_to_many关联的模型已建立。Wishlist类也有:selectmany_to_many关联的选项设置为select:[:items.*,:items_wishlists__facebook_action_id].有没有一种方法可以

  3. ruby-on-rails - 使用 Sublime Text 3 突出显示 HTML 背景语法中的 ERB? - 2

    所以我在关注Railscast,我注意到在html.erb文件中,ruby代码有一个微弱的背景高亮效果,以区别于其他代码HTML文档。我知道Ryan使用TextMate。我正在使用SublimeText3。我怎样才能达到同样的效果?谢谢! 最佳答案 为SublimeText安装ERB包。假设您安装了SublimeText包管理器*,只需点击cmd+shift+P即可获得命令菜单,然后键入installpackage并选择PackageControl:InstallPackage获取包管理器菜单。在该菜单中,键入ERB并在看到包时选择

  4. ruby - 无法在 60 秒内获得稳定的 Firefox 连接 (127.0.0.1 :7055) - 2

    我使用的是Firefox版本36.0.1和Selenium-Webdrivergem版本2.45.0。我能够创建Firefox实例,但无法使用脚本继续进行进一步的操作无法在60秒内获得稳定的Firefox连接(127.0.0.1:7055)错误。有人能帮帮我吗? 最佳答案 我遇到了同样的问题。降级到firefoxv33后一切正常。您可以找到旧版本here 关于ruby-无法在60秒内获得稳定的Firefox连接(127.0.0.1:7055),我们在StackOverflow上找到一个类

  5. ruby - 覆盖相似的方法,更短的语法 - 2

    在Ruby类中,我重写了三个方法,并且在每个方法中,我基本上做同样的事情:classExampleClassdefconfirmation_required?is_allowed&&superenddefpostpone_email_change?is_allowed&&superenddefreconfirmation_required?is_allowed&&superendend有更简洁的语法吗?如何缩短代码? 最佳答案 如何使用别名?classExampleClassdefconfirmation_required?is_a

  6. ruby 语法糖 : dealing with nils - 2

    可能已经问过了,但我找不到它。这里有2个常见的情况(对我来说,在编程Rails时......)用ruby​​编写是令人沮丧的:"astring".match(/abc(.+)abc/)[1]在这种情况下,我得到一个错误,因为字符串不匹配,因此在nil上调用[]运算符。我想找到的是比以下内容更好的替代方法:temp="astring".match(/abc(.+)abc/);temp.nil??nil:temp[1]简而言之,如果不匹配,则简单地返回nil而不会出错第二种情况是这样的:var=something.very.long.and.tedious.to.writevar=some

  7. ruby - Ruby 语法糖有 "rules"吗? - 2

    我正在学习Ruby的基础知识(刚刚开始),我遇到了Hash.[]method.它被引入a=["foo",1,"bar",2]=>["foo",1,"bar",2]Hash[*a]=>{"foo"=>1,"bar"=>2}稍加思索,我发现Hash[*a]等同于Hash.[](*a)或Hash.[]*一个。我的问题是为什么会这样。是什么让您将*a放在方括号内,是否有某种规则可以在何时何地使用“it”?编辑:我的措辞似乎造成了一些困惑。我不是在问数组扩展。我明白了。我的问题基本上是:如果[]是方法名称,为什么可以将参数放在括号内?这看起来几乎——但不完全是——就像说如果你有一个方法Foo.d

  8. Hive SQL 五大经典面试题 - 2

    目录第1题连续问题分析:解法:第2题分组问题分析:解法:第3题间隔连续问题分析:解法:第4题打折日期交叉问题分析:解法:第5题同时在线问题分析:解法:第1题连续问题如下数据为蚂蚁森林中用户领取的减少碳排放量iddtlowcarbon10012021-12-1212310022021-12-124510012021-12-134310012021-12-134510012021-12-132310022021-12-144510012021-12-1423010022021-12-154510012021-12-1523.......找出连续3天及以上减少碳排放量在100以上的用户分析:遇到这类

  9. ruby - 如何让Ruby捕获线程中的语法错误 - 2

    我正在尝试使用ruby​​编写一个双线程客户端,一个线程从套接字读取数据并将其打印出来,另一个线程读取本地数据并将其发送到远程服务器。我发现的问题是Ruby似乎无法捕获线程内的错误,这是一个示例:#!/usr/bin/rubyThread.new{loop{$stdout.puts"hi"abc.putsefsleep1}}loop{sleep1}显然,如果我在线程外键入abc.putsef,代码将永远不会运行,因为Ruby将报告“undefinedvariableabc”。但是,如果它在一个线程内,则没有错误报告。我的问题是,如何让Ruby捕获这样的错误?或者至少,报告线程中的错误?

  10. sql - 查询忽略时间戳日期的时间范围 - 2

    我正在尝试查询我的Rails数据库(Postgres)中的购买表,我想查询时间范围。例如,我想知道在所有日期的下午2点到3点之间进行了多少次购买。此表中有一个created_at列,但我不知道如何在不搜索特定日期的情况下完成此操作。我试过:Purchases.where("created_atBETWEEN?and?",Time.now-1.hour,Time.now)但这最终只会搜索今天与那些时间的日期。 最佳答案 您需要使用PostgreSQL'sdate_part/extractfunction从created_at中提取小时

随机推荐