本文档介绍如何在 TDSQL Boundless 数据库中使用
JOIN 语句进行多表连接查询,包括各种连接类型、连接算法的使用。开始之前
在进行单表查询之前,请确保:
1. 已创建 TDSQL Boundless 实例。
2. 已导入测试数据:本文档使用 TPC-H 基准测试数据集作为示例,详情请参考 TPC-H 基准测试数据模型。
3. 已建立数据库连接:通过 MySQL 客户端或应用程序连接到 TDSQL Boundless。
JOIN 类型
内连接(INNER JOIN)
内连接只返回两张表中同时满足连接条件的行。如果某一侧没有匹配的行,则该行不会出现在结果中。
查询每个订单对应的客户名称:
SELECTo.o_orderkey,o.o_orderdate,o.o_totalprice,c.c_name,c.c_mktsegmentFROM orders oINNER JOIN customer c ON o.o_custkey = c.c_custkey;
在这个查询中,只有在
customer 表中存在对应客户的订单才会被返回。多表内连接 — 查询订单明细及其所属订单和客户信息:
SELECTc.c_name,o.o_orderkey,o.o_orderdate,l.l_partkey,l.l_quantity,l.l_extendedpriceFROM customer cINNER JOIN orders o ON c.c_custkey = o.o_custkeyINNER JOIN lineitem l ON o.o_orderkey = l.l_orderkeyWHERE o.o_orderdate BETWEEN '1995-01-01' AND '1995-03-31'ORDER BY l.l_extendedprice DESCLIMIT 20;
注意:
由于使用的是
INNER JOIN,没有下过订单的客户不会出现在结果中。如果需要包含没有订单的客户,应使用 LEFT JOIN。左外连接(LEFT JOIN)
左外连接返回左表的所有行。对于右表中没有匹配的行,结果中右表的列填充为
NULL。查询所有客户及其订单数量(包括没有下过订单的客户):
SELECTc.c_custkey,c.c_name,c.c_mktsegment,COUNT(o.o_orderkey) AS order_countFROM customer cLEFT JOIN orders o ON c.c_custkey = o.o_custkeyGROUP BY c.c_custkey, c.c_name, c.c_mktsegmentORDER BY order_count ASCLIMIT 20;
与
INNER JOIN 不同,即使某个客户在 orders 表中没有任何记录,该客户仍然会出现在结果中,其 order_count 为0。使用 LEFT JOIN 查找不匹配的行 — 查询没有下过订单的客户:
SELECT c.c_custkey, c.c_name, c.c_phoneFROM customer cLEFT JOIN orders o ON c.c_custkey = o.o_custkeyWHERE o.o_orderkey IS NULL;
这种写法等价于
NOT EXISTS 子查询,但在某些场景下执行效率更高:-- 等价写法SELECT c_custkey, c_name, c_phoneFROM customer cWHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.o_custkey = c.c_custkey);
右外连接(RIGHT JOIN)
右外连接返回右表的所有行,对于左表中没有匹配的行,左表的列填充为
NULL。其逻辑与 LEFT JOIN 对称。查询所有国家及其客户数量:
SELECTn.n_name AS nation_name,COUNT(c.c_custkey) AS customer_countFROM customer cRIGHT JOIN nation n ON c.c_nationkey = n.n_nationkeyGROUP BY n.n_nameORDER BY customer_count DESC;
实践教程:在实际开发中,建议统一使用
LEFT JOIN 代替 RIGHT JOIN,只需调换表的顺序即可。这样可以保持 SQL 风格一致,提高可读性。上述查询等价于:
SELECTn.n_name AS nation_name,COUNT(c.c_custkey) AS customer_countFROM nation nLEFT JOIN customer c ON c.c_nationkey = n.n_nationkeyGROUP BY n.n_nameORDER BY customer_count DESC;
交叉连接(CROSS JOIN)
交叉连接返回两张表的笛卡尔积,即左表的每一行与右表的每一行组合。如果左表有 m 行、右表有 n 行,结果集将有 m * n 行。
查询所有区域和市场细分的组合:
SELECTr.r_name AS region_name,seg.segment_nameFROM region rCROSS JOIN (SELECT DISTINCT c_mktsegment AS segment_nameFROM customer) segORDER BY r.r_name, seg.segment_name;
注意:
交叉连接会产生非常大的结果集,在生产环境中应谨慎使用,通常仅用于生成维度组合等特定场景。
左半连接(LEFT SEMI JOIN)
左半连接用于判断左表的行在右表中是否存在匹配,只返回左表的列,且每行最多返回一次。TDSQL 语法中并不直接支持 LEFT SEMI JOIN 语法,通常通过
EXISTS 或 IN 子查询来表达。查询有过订单的客户:
SELECT c.c_custkey, c.c_name, c.c_acctbalFROM customer cWHERE EXISTS (SELECT 1 FROM orders oWHERE o.o_custkey = c.c_custkey);
等价的
IN 写法:SELECT c_custkey, c_name, c_acctbalFROM customerWHERE c_custkey IN (SELECT o_custkey FROM orders);
与
INNER JOIN 不同,左半连接不会因右表存在多条匹配而产生重复行。例如一个客户有10个订单,INNER JOIN 会返回10行,而左半连接只返回1行。隐式连接
不使用
JOIN 关键字,而是在 FROM 子句中列出多张表,并在 WHERE 子句中指定连接条件。隐式连接的行为等同于 INNER JOIN。SELECTc.c_name,o.o_orderkey,o.o_totalpriceFROM customer c, orders oWHERE c.c_custkey = o.o_custkeyAND o.o_orderdate >= '1995-01-01'ORDER BY o.o_totalprice DESCLIMIT 10;
实践教程:建议使用显式的
JOIN ... ON 语法代替隐式连接。显式语法将连接条件与过滤条件分离,SQL 的意图更加清晰,也更易于维护。JOIN 相关算法
TDSQL Boundless 优化器会基于代价评估自动选择最优的连接算法。目前支持两种核心算法:Nested Loop Join 和 Hash Join。
Nested Loop Join
Nested Loop Join(嵌套循环连接)是最基础的连接算法。其原理是遍历外表(驱动表)的每一行,然后在内表中查找匹配的行。
TDSQL Boundless 默认采用 Batched Key Access(BKA) 方式执行 Nested Loop Join。BKA 的核心优化思路是:将外表的多行键值批量收集后,一次性发送给内表进行查找,而不是逐行发送。这种批处理方式有两个显著优势:
1. 减少 RPC 频次:将多次单行查找合并为一次批量请求,大幅降低网络开销
2. 提升存储引擎读取效率:内表收到一批键值后,批量索引查找和回表,提升存储引擎读取效率
适用场景:
内表的连接列上有索引
外表数据量相对较小,或经过 WHERE 过滤后行数较少
索引选择性好(即通过索引能快速定位少量行)
示例:
-- 典型的 Nested Loop Join 场景-- orders 作为外表,customer 通过主键 c_custkey 作为内表SELECTo.o_orderkey,o.o_totalprice,c.c_nameFROM orders oINNER JOIN customer c ON o.o_custkey = c.c_custkeyWHERE o.o_orderdate = '1995-03-15';
在这个查询中,
orders 表经过 o_orderdate 过滤后行数较少,作为驱动表;customer 表的 c_custkey 是主键,作为被驱动表。BKA 会将过滤后的 o_custkey 值批量发送,一次性在 customer 表中查找匹配行。Hash Join
Hash Join(哈希连接)的原理是:首先将较小的表(构建表)加载到内存中构建哈希表,然后遍历较大的表(探测表),对每一行通过哈希函数快速查找匹配行。
适用场景:
连接列上没有合适索引
等值连接(
= 条件)示例:
-- 典型的 Hash Join 场景-- 两张大表的等值连接,且连接列无索引SELECTl.l_orderkey,l.l_extendedprice,l.l_discount,p.p_name,p.p_typeFROM lineitem lINNER JOIN part p ON l.l_partkey = p.p_partkeyWHERE p.p_type LIKE '%BRASS%';
算法选择
TDSQL Boundless 优化器基于代价模型自动选择连接算法,主要考虑以下因素:
因素 | Nested Loop Join (BKA) | Hash Join |
索引依赖 | 需要内表连接列上有索引 | 不依赖索引 |
数据量 | 适合外表较小、内表较大的场景 | 适合两表都较大的场景 |
内存消耗 | 较低,按批次处理 | 较高,需要在内存中构建哈希表 |
连接类型 | 等值和非等值连接 | 仅支持等值连接 |
通过
EXPLAIN 可以查看优化器选择的连接算法:EXPLAIN SELECTo.o_orderkey, c.c_nameFROM orders oINNER JOIN customer c ON o.o_custkey = c.c_custkeyWHERE o.o_orderdate = '1995-03-15';
JOIN 顺序
在多表连接查询中,表的连接顺序对查询性能有重要影响。TDSQL Boundless 优化器会自动评估不同的连接顺序,选择代价最低的执行方案。
自动优化
优化器在生成执行计划时,会综合考虑表的大小、索引情况、过滤条件的选择性等因素,自动调整连接顺序:
-- 优化器会自动决定 customer、orders、lineitem 的连接顺序SELECTc.c_name,o.o_orderdate,l.l_extendedpriceFROM customer cINNER JOIN orders o ON c.c_custkey = o.o_custkeyINNER JOIN lineitem l ON o.o_orderkey = l.l_orderkeyWHERE c.c_mktsegment = 'BUILDING'AND o.o_orderdate < '1995-03-15'AND l.l_shipdate > '1995-03-15';
使用 Hint 指定连接顺序
当优化器选择的连接顺序不理想时,可以通过 Hint 手动干预连接顺序。
JOIN_ORDER:指定所有表的完整连接顺序:
SELECT /*+ JOIN_ORDER(o, c, l) */c.c_name,o.o_orderdate,l.l_extendedpriceFROM customer cINNER JOIN orders o ON c.c_custkey = o.o_custkeyINNER JOIN lineitem l ON o.o_orderkey = l.l_orderkeyWHERE c.c_mktsegment = 'BUILDING'AND o.o_orderdate < '1995-03-15'AND l.l_shipdate > '1995-03-15';
JOIN_ORDER(o, c, l) 强制优化器严格按 orders > customer > lineitem 的顺序执行连接,忽略 SQL 中的表书写位置。JOIN_PREFIX:仅指定连接顺序的前几张表,其余表由优化器自动决定:
SELECT /*+ JOIN_PREFIX(o, c) */c.c_name,o.o_orderdate,l.l_extendedpriceFROM customer cINNER JOIN orders o ON c.c_custkey = o.o_custkeyINNER JOIN lineitem l ON o.o_orderkey = l.l_orderkeyWHERE c.c_mktsegment = 'BUILDING'AND o.o_orderdate < '1995-03-15'AND l.l_shipdate > '1995-03-15';
JOIN_PREFIX(o, c) 强制前两张表的连接顺序为 orders > customer,而 lineitem 的连接位置由优化器自行选择。当只需要控制部分关键表的顺序、其余表交由优化器优化时,JOIN_PREFIX 比 JOIN_ORDER 更灵活。