实现联接

学习目标

  • 实施连接。

  • 区分内连接和外连接。

  • 实现嵌套连接。

连接的基础知识和语法

连接定义

有时,需要将两个 SQL 数据源合并为一个结果集,该结果集由两个数据源的列组成。这称为连接。

例如,读取航班转机时,可能还需要读取航班承运人的详细信息。

结果集的行由连接类型和数据源列之间的连接条件确定。连接条件以关键字 ON 开头,并定义数据源各列之间的关系。

连接类型由相应的关键字定义。在 ABAP SQL 中,支持的连接类型为 INNER JOIN、LEFT OUTER JOIN 和 RIGHT OUTER JOIN。

我们将很快讨论这些联接类型的不同结果。但是,我们先来看看联接的 ABAP SQL 语法。

该示例将承运商数据(承运商标识和承运商名称)和连接数据(连接标识、出发机场和目的地机场)合并到一个结果集中。左数据源是数据库表 /DMO/CARRIER,而数据库表 /DMO/CONNECTION 用作正确的数据源。两个数据源名称之间的关键字 INNER JOIN 指定连接类型。

ON 后的连接条件请求数据库表 /DMO/CARRIER 的字段 CARRIER_ID 的内容等于数据库表 /DMO/CONNECTION 的字段 CARRIER_ID 的内容。

在连接条件以及字段列表、WHERE 子句等中,数据源的列由数据源名称和列名称完全标识,以波浪号 ( ~) 分隔。

注意

从其他 SQL 方言中,您可能被用于句点符号或句号 (.)。这必须由 ABAP SQL 中的其他符号替换,因为句点符号已具有结束语句的含义。

可以在没有数据源名称的情况下寻址列名称。前提条件是列名称仅存在于其中一个数据源中。

在此示例中,只有数据库表 /DMO/CARRIER 包含列 NAME 和 CURRENCY_CODE,而 CONNECTION_ID、AIRPORT_FROM_ID 和 AIRPORT_TO_ID 列仅存在于数据库表 /DMO/CONNECTION 中。因此,这些列名称是唯一的,没有前面的/DMO/CARRIER~ 或 /DMO/CONNECTION~。

另一方面,两个表都包含列 CARRIER_ID。因此,对于此字段 /DMO/CARRIER~ 或 /DMO/CONNECTION~ 为必填字段。

最好对数据源名称使用别名,以缩短语句并提高可读性。在数据源名称后使用附加项 AS 完成此操作。在此示例中,别名 A 用于数据库表 /DMO/CARRIER,别名 C 用于数据库表 /DMO/CONNECTION。

如果数据源具有别名,则必须使用别名而不是原始数据源名称。

自连接中所需的别名

在某些情况下,数据源的别名不仅仅是语法替代方法。在多次包含相同数据源的语句中,它们变得不可或缺。

让我们看一个示例。让我们考虑一个包含转机 (/DMO/CONNECTION) 的表。我们对从法兰克福国际机场(FRA)开始可以到达的目的地感兴趣。但是,我们不是直接航班,而是查找一次转机的航班。

解决方案是表 /DMO/CONNECTION 与自身的连接。第一个连接条件要求第二个连接(第 2 段)从第一个连接(第 1 段)结束的机场开始。第二个连接条件(使用 AND 运算符链接到第一个连接条件)排除第二个连接是原始出发机场的直接返程航班的组合。

很明显,如果没有别名 a(用于 /DMO/CONNECTION 的左侧使用)和 b(用于 /DMO/CONNECTION 的正确使用),这种自连接将无法工作。如果没有不同的别名,则无法区分第一个连接和第二个连接 - 既不在连接条件中,也不在字段列表或 WHERE 子句中。

注意

在此类自联接中,每个列名称前面都需要别名,因为每个列名称都存在于两个数据源中。

试用:连接语法

  1. 创建实施接口 IF_OO_ADT_CLASSRUN 的新全局类。

  2. 将以下代码片段复制到方法 if_oo_adt_classrun~main() 的实施部分:

SELECT FROM /dmo/carrier INNER JOIN /dmo/connection
*    SELECT FROM /dmo/carrier AS a INNER JOIN /dmo/connection AS c
ON /dmo/carrier~carrier_id = /dmo/connection~carrier_id
FIELDS /dmo/carrier~carrier_id,
/dmo/carrier~name AS carrier_name,
/dmo/connection~connection_id,
/dmo/connection~airport_from_id,
/dmo/connection~airport_to_id
WHERE /dmo/carrier~currency_code = 'EUR'
INTO TABLE @DATA(result).
out->write(
EXPORTING
data   = result
name   = 'RESULT'
).
  1. 选择 CTRL + F3 激活类,然后选择 F9 将其作为控制台应用执行。

  2. 分析控制台输出。

  3. 将光标置于第一行(关键字为 SELECT 的行),然后选择 CTRL + < 在该行的第一列添加星号 (*)。

  4. 从下一行的开头移除星号 (*)。分析语法错误,告知您表名 /DMO/CARRIER 已被别名隐藏。

  5. 在 ON 条件中,将表名 /DMO/CARRIER 替换为其别名 A,将表名 /DMO/CONNECTION 替换为其别名 C。

  6. 在 FIELDS 列表和 WHERE 子句中,移除每个字段名称前面的别名和波浪符 (~)。您应该发现仅在字段 carrier_id 前面为必填字段。

  7. 激活并再次执行。

连接类型

观看以下视频,了解有关内连接、右外连接和左外连接的详细信息。

视频:本节含 SAP 官方视频(视频 ID 1_rrgb207z),需在 learning.sap.com 在线观看。

视频:本节含 SAP 官方视频(视频 ID 1_7wdd13yz),需在 learning.sap.com 在线观看。

视频:本节含 SAP 官方视频(视频 ID 1_hcjbz3kl),需在 learning.sap.com 在线观看。

此示例说明了左外连接的结果。结果集中突出显示的两行在内部连接中将不存在。它们对应于数据库表 /DMO/CONNECTION 中没有转机的运输公司。

这些行仅包含有关承运商的信息,即来自左侧数据源的表 /DMO/CARRIER 的信息。通常包含来自正确数据源的信息的列(即 CONNECTION_ID、AIRPORT_FROM_ID 和 AIRPORT_TO_ID 列)用初始值填充。这些值由 ABAP 运行时输入,而不是数据库返回的 Null 值。

注意

CONNECTION_ID 列的类型基于 ABAP 内置类型 N。此类型的初始值不是一系列空格,而是一系列零。

试用:内连接和外连接

  1. 创建实施接口 IF_OO_ADT_CLASSRUN 的新全局类。

  2. 将以下代码片段复制到方法 if_oo_adt_classrun~main() 的实施部分:

SELECT FROM /dmo/Agency AS a
INNER JOIN /dmo/customer AS c
*           LEFT OUTER JOIN /dmo/customer AS c
*          RIGHT OUTER JOIN /dmo/customer AS c
ON a~city         = c~city
FIELDS agency_id,
name AS Agency_name,
a~city AS agency_city,
c~city AS customer_city,
customer_id,
last_name AS customer_name
WHERE ( c~customer_id < '000010' OR c~customer_id IS NULL )
AND ( a~agency_id   < '070010' OR a~agency_id   IS NULL )
INTO TABLE @DATA(result_Join).
out->write(
EXPORTING
data   = result_join
name   = 'RESULT_JOIN'
).
  1. 选择 CTRL + F3 激活类,然后选择 F9 将其作为控制台应用执行。

  2. 分析控制台输出。

注意

代码示例检索地址位于同一城市的客户和旅行社对。它使用 WHERE 子句使结果集保持较小。因此,使用 INNER JOIN 只能找到客户 '000010' 和旅行社 '070010'。WHERE 子句的写入形式也适用于外连接。需要条件<field_name> IS NULL 以便不从数据库中排除具有 NULL 值的结果。

  1. 在 FROM 语句的 SELECT 子句中,将 INNER JOIN 替换为 LEFT OUTER JOIN 并再次激活。或者,可以使用 INNER JOIN 注释行,并使用 LEFT OUTER JOIN 取消注释。

  2. 再次执行 ,并将控制台输出与第一个输出进行比较。

  3. 在 FROM 语句的 SELECT 子句中,将 LEFT OUTER JOIN 替换为 RIGHT OUTER JOIN 并再次激活。或者,可以使用 LEFT OUTER JOIN 注释行,并使用 RIGHT OUTER JOIN 取消注释。

  4. 再次执行 并将控制台输出与前两个输出进行比较。

嵌套连接

多个数据源的连接

在 ABAP SQL 中,您可以同时连接两个以上的数据源。我们先从两个数据源连接开始,即我们之前讨论的加入航班承运人和转机。

现在,我们考虑添加数据库表 /DMO/AIRPORT 以读取启运机场 (AIRPORT_FROM_ID) 的名称和城市。

要联接这三个数据源,我们首先用一对括号将现有联接括起来。现在,第一个联接用作下一个联接的左数据源。我们添加了连接关键字(INNER JOIN、LEFT OUTER JOIN、RIGHT OUTER JOIN)、右侧数据源 (/DMO/AIRPORT)、别名 (AS apf) 和附加连接的连接条件 (ON c~airport_from_id = apf~airport_id)。

现在,我们考虑再次添加数据库表 /DMO/AIRPORT 以同时读取目的地机场 (AIRPORT_TO_ID) 的名称和城市。

要实现这四个数据源的联接,我们重复三个数据源的连接操作:我们将三个数据源的现有联接与另一对括号括起来,并添加联接关键字、右数据源和联接条件。

注意

可重复此操作,直到达到可以相互链接的最大数据源数。目前,一个 SELECT 语句中最多允许 50 个数据源(49 个连接)。

连接顺序

在上一示例中,下一个数据源始终添加到下一个联接的右侧。左数据源是数据库表或联接。所有三个联接的正确数据源都是数据库表。

借助括号,还可以使用联接作为另一个联接的正确数据源。在下方示例中,我们从连接 /DMO/CONNECTION 和 /DMO/AIRPORT (AS apf) 开始。我们用括号将其括起来,并将其用作与 /DMO/AIRPORT (AS apt) 连接的左数据源。要添加 /DMO/CARRIER,请将现有联接与另一对括号括起来。然后,在左括号前添加左数据源 (/DMO/CARRIER) 和连接关键字,在右括号后添加 ON 条件。

注意

在我们的示例中,两个结果相同。但是,在某些情况下,结果可能取决于数据库评估连接的顺序。

括号的重要性

从语法角度来看,不需要括号。但为了便于阅读,强烈建议不要在嵌套连接中省略括号。这应该通过以下示例进行说明。

SELECT 语句包含四个数据库表的连接,但不使用任何括号。一目了然,可以认为首先评估 /DMO/CARRIER 和 /DMO/CONNECTION 的连接,因为这些表是第一个出现在 FROM 子句中的表。

但这个结论是错误的!

如果没有括号,则评估顺序由 ON 条件的顺序确定,而不是由数据源的顺序确定。仔细检查时,我们会看到,将表 /DMO/CARRIER(别名 a)和 /DMO/CONNECTION(别名 c)相互链接的 ON 条件放置在 FROM 子句的最末尾。因此,此连接是最后一个要评估的连接。

提示

在嵌套连接中始终使用括号,以避免这种混淆。

实施嵌套连接

在本练习中,实施嵌套连接以在同一 SELECT 语句中读取转机和机场详细信息。

模板:

  • /LRN/CL_S4D401_TCS_STRING_PROC(全局类)

解决方案:

  • /LRN/CL_S4D401_DBS_NESTED_JOIN(全局类)

任务 1: 复制模板(可选)

复制模板类 /LRN/CL_S4D401_TCS_STRING_PROC。如果已完成上一练习,则可以跳过此任务并继续编辑类 ZCL_##_SOLUTION

步骤

  1. 将类 /LRN/CL_S4D401_TCS_STRING_PROC 复制到您自己的包中的类(建议名称:ZCL_##_SOLUTION,其中 ## 代表组编号)。

  2. 在项目浏览器中,右键单击类 /LRN/CL_S4D401_TCS_STRING_PROC 以打开上下文菜单。

  3. 从上下文菜单中,选择复制....

  4. 在包字段中输入的名称。在名称字段中,输入 ZCL_##_SOLUTION,其中 ## 代表组编号。

  5. 选择下一步

  6. 确认传输请求并选择完成

  7. 激活复制。

  8. Ctrl + F3 激活类。

任务 2: 实施连接

调整本地类 LCL_PASSENGER_FLIGHT 的方法 CLASS_CONSTRUCTOR 的实施。扩展从数据库表 /LRN/CONNECTION 中读取的 SELECT 语句。添加两个连接以读取出发机场和目标机场的时区。

步骤

  1. 导航到本地类 LCL_PASSENGER_FLIGHT 中方法 CLASS_CONSTRUCTOR 的实施。

  2. 例如,可以在编辑器中打开局部类型标签,然后搜索 METHOD class_constructor

  3. 或者,您可以在左侧的概要视图中展开 ZCL_##_SOLUTIONLCL_PASSENGER_FLIGHT,然后选择 CLASS_CONSTRUCTOR

  4. 查找从数据库表 /LRN/CONNECTION 中读取的 SELECT 语句,并为数据源添加别名(建议别名:c)。

  5. 如下调整代码:

SELECT
FROM /lrn/connection AS c
FIELDS carrier_id, connection_id,
airport_from_id, airport_to_id, departure_time, arrival_time
INTO TABLE @connections_buffer.
  1. 添加具有相应 JOIN 类型的连接,该连接从数据库表 /LRN/AIRPORT 中读取出发机场的详细信息。为第二个数据源添加别名(建议别名:f)。

  2. 如下调整代码:

SELECT
FROM lrn/connection AS c
LEFT OUTER JOIN /lrn/airport AS f
FIELDS carrier_id, connection_id,
airport_from_id, airport_to_id, departure_time, arrival_time
INTO TABLE @connections_buffer.
  1. 添加连接条件。

  2. 如下调整代码:

SELECT
FROM /lrn/connection AS c
LEFT OUTER JOIN /lrn/airport AS f
ON c~airport_from_id = f~airport_id
FIELDS carrier_id, connection_id,
airport_from_id, airport_to_id, departure_time, arrival_time
INTO TABLE @connections_buffer.
  1. 添加读取目标机场详细信息的类似联接,并为第三个数据源添加不同的别名(建议的别名:t)。

  2. 如下调整代码:

SELECT
FROM /lrn/connection AS c
LEFT OUTER JOIN /lrn/airport AS f
ON c~airport_from_id = f~airport_id
LEFT OUTER JOIN /lrn/airport AS t
ON c~airport_to_id = t~airport_id
FIELDS carrier_id, connection_id,
airport_from_id, airport_to_id, departure_time, arrival_time
INTO TABLE @connections_buffer.
  1. 将启运机场的时区和目的地机场的时区添加到字段列表。提供合适的别名(建议的别名:timzone_fromtimzone_to)。

  2. 如下调整代码:

SELECT
FROM /lrn/connection AS c
LEFT OUTER JOIN /lrn/airport AS f
ON c~airport_from_id = f~airport_id
LEFT OUTER JOIN /lrn/airport AS t
ON c~airport_to_id = t~airport_id
FIELDS carrier_id, connection_id,
airport_from_id, airport_to_id, departure_time, arrival_time,
f~timzone AS timzone_from,
t~timzone AS timzone_to
INTO TABLE @connections_buffer.
  1. 导航到内部表 connections_buffer 的行类型,并为时区添加两个组件。使用在 SELECT 语句的字段列表中用作别名的相同名称。

警告

确保在组件持续时间之前添加新组件。这很重要,因为未在 SELECT 语句中填充持续时间组件。或者,在 SELECT 语句的 INTO 子句中使用 CORRESPONDING FIELDS OF 附加项。

  1. 查找结构类型 st_connections_buffer 的定义。

  2. 如下调整代码:

TYPES:
BEGIN OF st_connections_buffer,
carrier_id      TYPE /dmo/carrier_id,
connection_id   TYPE /dmo/connection_id,
airport_from_id TYPE /dmo/airport_from_id,
airport_to_id   TYPE /dmo/airport_to_id,
departure_time  TYPE /dmo/flight_departure_time,
arrival_time    TYPE /dmo/flight_departure_time,
timzone_from TYPE timezone,
timzone_to TYPE timezone,
duration        TYPE i,
END OF st_connections_buffer.

任务 3: 移除冗余 SELECT

在本地类 LCL_PASSENGER_FLIGHT 的方法 CLASS_CONSTRUCTOR 的实施中,在计算航班持续时间时使用内部表 connections_buffer 中的时区信息。然后移除对数据库表 /LRN/AIRPORT 的冗余读取访问。

步骤

  1. 返回到本地类 LCL_PASSENGER_FLIGHT 中方法 CLASS_CONSTRUCTOR 的实施。

  2. 照常执行该步骤。

  3. 在第一个 CONVERT 语句的 TIME ZONE 附加中,将对本地内部表**机场的读取访问替换为对结构连接** 的组件 timzone_from 的访问。

  4. 按如下所示调整第一个 CONVERT 语句:

CONVERT DATE today
TIME connection-departure_time
*    TIME ZONE airports[ airport_id = connection-airport_from_id ]-timzone
TIME ZONE connection-timzone_from
INTO UTCLONG DATA(departure_utclong).
  1. 使用组件 timzone_to 以类似方式调整第二个 CONVERT 语句。

  2. 按如下所示调整第二个 CONVERT 语句:

CONVERT DATE today
TIME connection-arrival_time
*    TIME ZONE airports[ airport_id = connection-airport_to_id ]-timzone
TIME ZONE connection-timzone_to
INTO UTCLONG DATA(arrival_utclong).
  1. 向上滚动并在从数据库表 /LRN/AIRPORT 读取的 SELECT 语句的所有代码行前添加注释符号。

  2. 向上滚动到方法 CLASS_CONSTRUCTOR 实施的开头。

  3. 选择从数据库表 /LRN/AIRPORT 中读取的完整 SELECT 语句,然后按 Ctrl + <

  4. 然后,代码应如下所示:

*  SELECT
*    FROM /lrn/airport
*    FIELDS airport_id, timzone
*    INTO TABLE @DATA(airports).
  1. 以控制台应用形式激活并测试全局类。

  2. Ctrl + F3

  3. F9