SQL 和 SPL 的多对一连接对比

【摘要】
    连接(JOIN)用于把来自两个或多个表的记录结合起来。SQL 和 SPL 是大家比较熟悉的程序语言,本文将探讨对于连接问题,这两种语言的解决方案和基本原理。如何简便快捷的处理连接运算,这里为你全程解析,并提供 SQL 和 SPL 示例代码。SQL 和 SPL 的多对一连接对比

前文介绍了一对一、多对一两种表间关联关系,本文继续介绍多对一关联关系。

多对一关联,是指一张表的任意条记录能与另外一张表的一条记录进行对应。多对一关联关系,常见于有外键关系的两个表。表 A 的某些字段与表 B 的主键关联。A 表中与 B 表主键关联的字段称为指向 B 表的外键,B 也称为 A 的外键表

例如有员工表和部门表,其中员工表的部门 ID 字段,指向了部门表的 ID 字段。每个员工都有一个部门,但是每个部门可以有多个员工,员工表和部门表是多对一的关系。

我们经常把一对多和多对一混在一起来讲,其实是受了 SQL 的误导。在 SQL 中对这两种关联关系并没有进行区分,也没有单独提供各自的解决方案。

解决多对一关联的思路与一对多完全不同,我们可以在连接时将外键值转换成对应记录,或者将需要的字段值附加到“多”表上,然后就可以当作是一张表进行查询运算了。但是在 SQL 中并没有单独支持多对一关联关系,仍然是使用 JOIN 进行连接。而 SPL 则是提供了 A.switch() 等函数,针对外键表连接(多对一)进行优化和提速。

 

【例 1】 根据员工表和部门表,查询所有员工的姓名及其所在部门名称。部分数据如下:

EMPLOYEE:

ID

NAME

BIRTHDAY

DEPARTMENTID

SALARY

1

Rebecca

1974/11/20

6

7000

2

Ashley

1980/07/19

2

11000

3

Rachel

1970/12/17

7

9000

4

Emily

1985/03/07

3

7000

5

Ashley

1975/05/13

6

16000

DEPARTMENT:

ID

NAME

MANAGER

1

Administration

18

2

Finance

2

3

HR

4

4

Marketing

6

5

Production

7

 

SQL的解决方案:

    SQL处理多对一关系时,还是使用 JOIN 语句将两表连接,连接条件是外键字段与外键表的主键字段相等。这里要查询所有员工,所以使用了左连接 LEFT JOIN,SQL 语句如下:

   

    SELECT

      E.NAME, D.NAME DEPT_NAME

   FROM EMPLOYEE E

   LEFT JOIN

   DEPARTMENT D

   ON E.DEPARTMENTID=D.ID

 

SPL的解决方案:

    SPL提供了函数 A.switch(),将编码字段的键值转换为对应的记录。


A

1

=T("Employee.txt")

2

=T("Department.txt")

3

=A1.switch(DEPARTMENTID, A2:ID)

4

=A3.new(NAME, DEPARTMENTID.NAME:DEPT_NAME)

A1:导入员工表。

A2:导入部门表。

A3:使用函数 A.switch() 将部门 ID 进行外键对象化,即将部门 ID 的字段值转换成相应的部门记录。

A4:返回员工名称和部门名称,其中部门名称可以通过部门记录的 NAME 字段获得。

 

    由于这个例子比较简单,SQL 和 SPL 都可以很好的解决。可能有的同学会有疑问,在连接时将需要的外键字段引入就可以了,为什么还要进行外键对象化呢?接下来我们用一个稍微复杂一点的例子来说明。

 

【例 2】 根据员工表和部门表,查询哪些美国籍员工有一个中国籍经理。部分数据如下:

EMPLOYEE:

ID

NAME

BIRTHDAY

DEPARTMENTID

SALARY

1

Rebecca

1974/11/20

6

7000

2

Ashley

1980/07/19

2

11000

3

Rachel

1970/12/17

7

9000

4

Emily

1985/03/07

3

7000

5

Ashley

1975/05/13

6

16000

DEPARTMENT:

ID

NAME

MANAGER

1

Administration

18

2

Finance

2

3

HR

4

4

Marketing

6

5

Production

7

 

SQL的解决方案:

    在本例中,我们不能简单的将需要的外键表字段添加到员工表上,需要进行两次连接:一是部门表与员工表通过员工 ID 进行连接, 查找经理的国籍。二是员工表与部门表通过部门 ID 进行连接,查找员工的部门。SQL 语句如下:

 

   SELECT *

   FROM EMPLOYEE E2

   LEFT JOIN

      (SELECT D1.ID,D1.MANAGER,E1.NATION MANAGER_NATION

      FROM DEPARTMENT D1

      LEFT JOIN EMPLOYEE E1

      ON D1.MANAGER=E1.ID

   ) D2

   ON E2.DEPARTMENTID=D2.ID

   WHERE D2.MANAGER_NATION='Chinese' AND E2.NATION='American'

   

    这个 SQL 看起来就比较复杂了。因为每增加一次连接 SQL 语句就要多嵌套一层,并且要提前想清楚我们需要外键表中的哪些字段。

 

SPL的解决方案:

    SPL还是可以通过函数 A.switch() 进行外键的对象化来解决问题:


A

1

=T("Employee.txt").keys(ID)

2

=T("Department.txt").keys(ID)

3

=A2.switch(MANAGER, A1)

4

=A1.switch(DEPARTMENTID, A2)

5

=A4.select(NATION=="American" && DEPARTMENTID.MANAGER.NATION=="Chinese")

A1:导入员工表,以 ID 为主键。

A2:导入部门表,以 ID 为主键。

A3:使用函数 A.switch() 外键对象化,将部门表的经理字段,转换成相应的员工记录。

A4:使用函数 A.switch() 外键对象化,将员工表的部门字段,转换成相应的部门记录。

A5:选出经理是中国籍的美国籍员工。

 

与 SQL 语句相比,SPL 语句并没有变得复杂。多了一层关联关系,SPL 脚本只是按照逻辑增加了一次外键对象化。我们不需要每次外键连接都提前想好需要哪些字段,可以直接从记录对象中取得,在选出时也非常符合自然逻辑,部门的经理的国籍(DEPARTMENTID.MANAGER.NATION)是中国籍。

 

【例 3】 根据课程表和选课表,查询有多少学生选修了“Matlab”课程。部分数据如下:

COURSE

ID

NAME

TEACHERID

1

Environmental   protection and sustainable development

5

2

Mental   health of College Students

1

3

Matlab

8

4

Electromechanical   basic practice

7

5

Introduction to modern life science

3

SELECT_COURSE

ID

COURSEID

STUDENTID

1

6

59

2

6

43

3

5

52

4

5

44

5

5

37

 

SQL的解决方案:

    我们想要在连接时,删除不匹配的记录(选出课程不是 Matlab 的),可以使用内连接。SQL 语句如下:

 

   SELECT

      COUNT(*) COUNT

   FROM SELECT_COURSE SC

   INNER JOIN

      COURSE C

   ON SC.COURSEID=C.ID

   WHERE NAME='Matlab'

 

SPL的解决方案:

    在函数 A.switch() 中,选项 @i 用于在连接时删除不匹配的记录。


A

1

=T("Course.csv")

2

=T("SelectCourse.csv")

3

=A1.select(NAME:"Matlab")

4

=A2.switch@i(COURSEID, A3:ID).count()

A1:导入课程表。

A2:导入选课表。

A3:在课程表中选出名为 Matlab 的记录。

A4:使用函数 A.switch() 的选项 @i,在连接时删除不匹配的记录,即剩下的都是选择了 Matlab 课程的记录,再统计数量。

 

【例 4】 根据销售表和客户表,查询 2014 年新增客户的销售情况(即销售表的客户 ID 不在客户表中的记录)。部分数据如下:

SALES

ID

CUSTOMERID

ORDERDATE

SELLERID

PRODUCTID

AMOUNT

10248

VINET

2013/7/4

5

59

2440

10249

TOMSP

2013/7/5

6

38

1863.4

10250

HANAR

2013/7/8

4

65

1813

10251

VICTE

2013/7/8

3

66

670.8

10252

SUPRD

2013/7/9

4

46

3730

CUSTOMER

ID

NAME

CITY

POSTCODE

TEL

ALFKI

Sanchuan Industrial Co., Ltd

Tianjin

343567

(030) 30074321

ANATR

Southeast industries

Tianjin

234575

(030) 35554729

ANTON

Tanson trade

Shijiazhuang

985060

(0321) 5553932

AROUT

Guoding Co., Ltd

Shenzhen

890879

(0571) 45557788

BERGS

Tongheng machinery

Nanjing

798089

(0921) 9123465

 

SQL的解决方案:

    我们想要在连接时,只保留不匹配的记录(销售表的客户 ID 不在客户表中的),在 SQL 中我们可以使用 NOT IN 或者 NOT EXISTS 来实现。SQL 语句如下:

 

   SELECT *

   FROM SALES S

   WHERE

      EXTRACT (YEAR FROM ORDERDATE)=2014

      AND

      CUSTOMERID NOT IN

         (SELECT DISTINCT ID

         FROM CUSTOMER)

 

    或者:

 

   SELECT *

   FROM SALES S

   WHERE

      EXTRACT (YEAR FROM ORDERDATE)=2014

      AND

      NOT EXISTS

         (SELECT *

         FROM CUSTOMER C

         WHERE S.CUSTOMERID=C.ID)

 

SPL的解决方案:

    在函数 A.switch() 中,选项 @d 时只保留不匹配的记录。


A

1

=T("Sales.csv")

2

=T("Customer.txt")

3

=A1.select(year(ORDERDATE)==2014)

4

=A3.switch@d(CUSTOMERID, A2:ID)

A1:导入销售表。

A2:导入客户表。

A3:在销售表中选出 2014 的记录。

A4:使用函数 A.switch() 的选项 @d,在连接时只保留不匹配的记录,即 2014 年新增客户的销售记录。

 

【例 5】 根据组织机构表,查询每个机构的上级机构名称。部分数据如下:

ID

ORG_NAME

PARENT_ID

1

Head   Office

0

2

Beijing   Branch Office

1

3

Shanghai   Branch Office

1

4

Chengdu   Branch Office

1

5

Beijing R&D Center

2

 

SQL的解决方案:

    在组织机构表中,上级机构 ID(PARENT_ID)指向的还是本表,这是一个自连接的例子。我们可以对组织机构表查询两次,分别看作两个表,再使用之前用到的连接方式即可。SQL 语句如下:

 

   SELECT

      ORG1.ID,ORG1.ORG_NAME,ORG2.ORG_NAME PARENT_NAME

   FROM

      ORGANIZATION ORG1

   LEFT JOIN

   ORGANIZATION ORG2

   ON ORG1.PARENT_ID=ORG2.ID

   ORDER BY ID

 

SPL的解决方案:

    在 SPL 中,我们还是可以使用函数 A.switch() 进行外键对象化,外键表选择自身即可。


A

1

=T("Organization.txt")

2

=A1.switch(PARENT_ID, A1:ID)

3

=A2.new(ID, ORG_NAME, PARENT_ID.ORG_NAME:PARENT_NAME)

A1:导入组织机构表。

A2:使用函数 A.switch() 进行外键对象化,将父机构 ID 字段值转换为相应的父机构记录。

A3:返回所有机构的名称和父机构的名称。

 

【例 6】 查询 2015 年每个客户的名称和订单总金额。订单表、订单明细表和客户表部分数据如下:

ORDERS:

ID

CUSTOMERID

EMPLOYEEID

ORDER_DATE

ARRIVAL_DATE

10248

VINET

5

2012/07/04

2012/08/01

10249

TOMSP

6

2012/07/05

2012/08/16

10250

HANAR

4

2012/07/08

2012/08/05

10251

VICTE

3

2012/07/08

2012/08/05

10252

SUPRD

4

2012/07/09

2012/08/06

ORDER_DETAIL:

ID

ORDER_NUMBER

PRODUCTID

PRICE

COUNT

DISCOUNT

10814

1

48

102.0

8

0.15

10814

2

48

102.0

8

0.15

10814

3

48

306.0

24

0.15

10814

4

48

102.0

8

0.15

10814

5

48

204.0

16

0.15

CUSTOMER:

ID

NAME

CITY

POSTCODE

TEL

ALFKI

Sanchuan Industrial Co., Ltd

Tianjin

343567

(030) 30074321

ANATR

Southeast industries

Tianjin

234575

(030) 35554729

ANTON

Tanson trade

Shijiazhuang

985060

(0321) 5553932

AROUT

Guoding Co., Ltd

Shenzhen

890879

(0571) 45557788

BERGS

Tongheng machinery

Nanjing

798089

(0921) 9123465

 

SQL的解决方案:

    本题中同时存在一对多(订单表和订单明细表)和多对一(订单表和客户表)关联关系。在 SQL 中对于一对多和多对一关系是没有特意区分的,但是我们必须搞清楚两者的不同。我们可以先处理外键表(多对一),将相应外键记录或需要的字段值附加到“多”表上,然后就变成只有主子表(一对多)的情况了。SQL 语句如下:

 

   SELECT

      CUSTOMER_NAME, SUM(AMOUNT) AMOUNT

   FROM (

      SELECT

         Orders1.ID, CUSTOMER_NAME, Detail.PRICE*Detail.COUNT AMOUNT

      FROM (

         SELECT

            Orders.ID,Customer.NAME CUSTOMER_NAME

         FROM ORDERS Orders

         LEFT JOIN

         CUSTOMER Customer

         ON Orders.CUSTOMERID=Customer.ID

         WHERE EXTRACT (YEAR FROM ORDER_DATE)=2015

      ) Orders1

      INNER JOIN

      ORDER_DETAIL Detail

      ON Orders1.ID=Detail.ID

   )

   GROUP BY CUSTOMER_NAME

   ORDER BY CUSTOMER_NAME

 

SPL的解决方案:

    在 SPL 中,我们首先使用外键对象化,将订单表中的客户 ID 转换为对应的客户记录然后就只有单个主子表关系了,使用函数 join() 进行连接即可。


A

1

=T("Orders.txt")

2

=T("Customer.txt")

3

=A1.select(year(ORDER_DATE)==2015).switch(CUSTOMERID, A2:ID)

4

=T("OrderDetail.txt").group(ID)

5

=join(A3:Orders, ID;A4:Detail, ID)

6

=A5.groups(Orders.CUSTOMERID.NAME; Detail.sum(PRICE*COUNT):AMOUNT)

A1:导入订单表。

A2:导入客户表。

A3:选出 2015 年的订单,并使用函数 A.switch() 将客户 ID 外键对象化,转换为对应的客户记录。

A4:导入订单明细表,并按订单 ID 分组。

A5:使用函数 join() 按照订单 ID 连接订单表和订单明细表。

A6:分组汇总每个客户的总销售额。

 

【例 7】 查询商品名称中包含“water”,在 2014 年下单,订单总金额大于 200 元,没有使用分期,得到 5 星好评的订单信息(订单号、产品名称和总金额)。表间关联关系和部分数据如下:

..

ORDERS:

ID

CUSTOMERID

EMPLOYEEID

ORDER_DATE

ARRIVAL_DATE

10248

VINET

5

2012/07/04

2012/08/01

10249

TOMSP

6

2012/07/05

2012/08/16

10250

HANAR

4

2012/07/08

2012/08/05

10251

VICTE

3

2012/07/08

2012/08/05

10252

SUPRD

4

2012/07/09

2012/08/06

ORDER_DETAIL:

ID

ORDER_NUMBER

PRODUCTID

PRICE

COUNT

DISCOUNT

10814

1

48

102.0

8

0.15

10814

2

48

102.0

8

0.15

10814

3

48

306.0

24

0.15

10814

4

48

102.0

8

0.15

10814

5

48

204.0

16

0.15

ORDER_PAYMENT:

ID

PAY_DATE

AMOUNT

CHANNEL

INSTALMENTS

10814

2014/01/05

816.0

3

0

10848

2014/01/23

800.25

2

1

10848

2014/01/23

800.25

0

0

10848

2014/01/23

800.25

3

1

10966

2014/03/20

572.0

2

1

EVALUATION:

ID

SCORE

DATE

COMMENT

10248

4

2012/07/12


10249

1

2012/07/06


10250

4

2012/07/10


10251

2

2012/07/11


10252

3

2012/07/16


PRODUCT:

ID

NAME

SUPPLIERID

CATEGORY

1

Apple   Juice

2

1

2

Milk

1

1

3

Tomato   sauce

1

2

4

Salt

2

2

5

Sesame   oil

2

2

 

SQL的解决方案:

    在本题中,既有一对多和多对一关系,又有一对一关系。如果我们不区分这几种连接方式,直接用 JOIN 来连接,就会产生多对多的关系,这样做是错误的。我们应该先处理多对一关系(外键表),将相应外键记录或需要的字段值附加到“多”表上,然后就变成只有一对一和一对多关系的情况了。再将子表按照主表主键(订单 ID)进行分组,主表主键(订单 ID)就变成子表事实上的主键了。最后将订单表、订单明细表、订单回款表和评价表按照订单 ID 连接即可。SQL 语句如下:

 

   SELECT

      Orders.ID,Detail1.NAME, Detail1.AMOUNT

   FROM (

      SELECT ID

      FROM ORDERS

      WHERE

         EXTRACT (YEAR FROM Orders.ORDER_DATE)=2014

   ) Orders

   INNER JOIN (

      SELECT ID,NAME, SUM(AMOUNT) AMOUNT

      FROM (

         SELECT

            Detail.ID,Product.NAME,Detail.PRICE*Detail.COUNT AMOUNT

         FROM ORDER_DETAIL Detail

         INNER JOIN

         PRODUCT Product

         ON Detail.PRODUCTID=Product.ID

         WHERE NAME LIKE '%water%'

      )

      GROUP BY ID,NAME

   ) Detail1

   ON Orders.ID=Detail1.ID

   INNER JOIN(

      SELECT

         DISTINCT ID

      FROM ORDER_PAYMENT

      WHERE INSTALMENTS=0

   ) Payment

   ON Orders.ID = Payment.ID

   INNER JOIN(

      SELECT ID

      FROM EVALUATION

      WHERE SCORE=5

   ) Evaluation

   ON Orders.ID = Evaluation.ID

 

这个 SQL 已经很难看懂了,无论是编写 SQL,还是后期维护,都需要不少工作量。更重要的是,由于需要多次连接和嵌套查询,我们很难判断 SQL 语句的正确性。

 

SPL的解决方案:


A

1

=T("Orders.txt").select(year(ORDER_DATE)==2014)

2

=T("Product.txt").select(like(NAME, "*water*"))

3

=T("OrderDetail.txt").switch@i(PRODUCTID, A2:ID)

4

=A3.group(ID).select(sum(PRICE*COUNT)>200)

5

=T("OrderPayment.txt").select(INSTALMENTS==0).group(ORDER)

6

=T("Evaluation.txt").select(SCORE==5)

7

=join(A1:Orders,ID;A4:Detail,ID;A5:Payment,ORDER;A6:Evaluation,ID)

8

=A7.new(Orders.ID,Detail.PRODUCTID.NAME,Detail.sum(PRICE*COUNT):AMOUNT)

A1:导入订单表,并选出 2014 年的记录。

A2:导入产品表,并选出名称含有 water 的产品。

A3:导入订单明细表,并将产品 ID 字段外键对象化,转换为相应的产品记录。

A4:将订单明细按照订单 ID 分组,并选出总金额大于 200 的记录。

A5:导入订单回款表,并选出没有使用分期付款的记录。

A6:导入评价表,并选出 5 星好评的记录。

A7:使用函数 join() 按照订单 ID 连接订单表、订单明细表、订单回款表和评价表。

A8:返回满足条件的订单 ID、产品名称和订单总金额。

 

    与 SQL 相比,SPL 脚本只是比前面的例子多了两行代码。而且每个表格的导入、选出、分组等运算,都是各自独立的,连接的代码只有A7一行。SPL 脚本的逻辑非常清晰,就是按照前面介绍的思路一步一步书写就完成了。

 

    通过以上的例子我们可以看到,多对一与一对多关系是有很大的区别的。SQL 没有区分两者的不同,需要用户自己去注意两者的区别。而在 SPL 中,明确的区分了两者的不同,分别提供了不同的函数来支持多对一和一对多两种关联关系。我们再来按照 SPL 的解题方式,总结一下多表关联查询的思路:

(1)   有多对一关联关系(外键表)时,将相应外键记录或需要的字段值附加到“多”表上。

(2)   有一对多关联关系(主子表)时,子表按照主表主键分组后,主表主键就成为了子表事实上的主键。

(3)   多表按照主键(或事实上的主键)进行连接。


Employee.txt

Course.txt

SelectCourse.csv

Sales.csv

Customer.txt

Organization.txt

Orders.txt

Customer.txt

OrderDetail.txt

Product.txt

OrderPayment.txt

Evaluation.txt

SQL 与 SPL 对比系列:
SQL 和 SPL 的集合运算对比
SQL 和 SPL 的选出运算对比
SQL 和 SPL 的有序运算对比
SQL 和 SPL 的等值分组对比
SQL 和 SPL 的非等值分组对比
SQL 和 SPL 的有序分组对比
SQL 和 SPL 的一对一和一对多连接对比
SQL 和 SPL 的多对一连接对比
SQL 和 SPL 的多对多连接对比
SQL 和 SPL 的基本静态转置对比
SQL 和 SPL 的复杂静态转置对比
SQL 和 SPL 的动态转置对比
SQL 和 SPL 的递归对比