# FT.JOIN 命令

使用说明: FT.JOIN 是 fkv_ds 搜索模块提供的 SQL/OQL 风格联表查询命令,支持在多个 RediSearch 索引(目前主要为 HASH 索引)之间执行 INNER / LEFT OUTER 等值连接(equi-join),并支持 WHERERETURNORDER BYLIMITTIMEOUT 等子句。


# 一、命令语法

FT.JOIN
  [FROM] <index> [AS <alias>]
  [INNER | LEFT] JOIN <table-ref> [AS <alias>] ON <qual-field> = <qual-field>
  [[INNER | LEFT] JOIN <table-ref> [AS <alias>] ON <qual-field> = <qual-field>] ...
  [WHERE <where-expr>]
  [RETURN <count> <projection> ...]
  [ORDER BY <sort-expr> [ASC | DESC] [, ...]]
  [LIMIT <offset> <count>]
  [TIMEOUT <ms>]

<table-ref> :: = <index> | ( <subquery> )

<subquery> :: = [FROM] <index> [AS <alias>]
                [INNER | LEFT] JOIN <table-ref> [AS <alias>] ON <qual-field> = <qual-field>
                [[INNER | LEFT] JOIN <table-ref> [AS <alias>] ON <qual-field> = <qual-field>] ...
                [WHERE <where-expr>]
                [RETURN <count> <projection> ...]
                [ORDER BY <sort-expr> [ASC | DESC] [, ...]]
                [LIMIT <offset> <count>]
                [TIMEOUT <ms>]

说明:

  • FROM 关键字可选,但为了可读性建议显式写出。
  • AS 关键字在部分语法中可选,例如 join_a AS ajoin_a a 等价。
  • 连接条件 ON <qual-field> = <qual-field> 只支持等值连接(equi-join)。
  • <qual-field> 必须带表别名,格式为 alias.field

# 二、子句说明

# 2.1 FROM / JOIN 表引用

每个参与连接的索引必须指定一个别名(alias),后续 ONWHERERETURNORDER BY 中均通过别名引用字段。

# 2.2 ON 连接条件

当前仅支持等值连接:

ON a.fjm = lj.ljjc

连接字段必须在对应索引的 Schema 中存在,否则会返回错误:

JOIN field not found in left/right index schema: &lt;field>

# 2.3 WHERE 过滤条件

WHERE 子句用于过滤参与连接或连接后的结果。根据条件中引用的表别名不同,执行方式分为两类:

  • 可下推条件:如果条件中的字段全部属于第一个基表(FROM 后的表),则会被翻译成对应索引的 FT.SEARCH 查询,在索引层提前过滤。
  • 后连接条件:如果条件引用了非第一个基表的字段(例如右表、子查询结果表),则在连接完成后对结果行进行过滤。

一个 WHERE 子句中允许同时存在可下推条件和后连接条件,内部会自动拆分到合适的阶段执行。

支持的操作符:

操作符 说明 示例
= 等于 a.flag = 'Y'
!= / <> 不等于 a.flag != 'N'
< 小于(数值) a.age < 30
<= 小于等于(数值) a.age <= 30
> 大于(数值) a.age > 18
>= 大于等于(数值) a.age >= 18
IS NULL 字段值为 NULL 或字段不存在 a.flag IS NULL
IS NOT NULL 字段值存在且不为 NULL a.flag IS NOT NULL

支持的逻辑组合:

关键字 说明 示例
AND 逻辑与 a.flag = 'Y' AND a.fjm = 'BJ'
OR 逻辑或 a.flag = 'Y' OR a.flag = 'N'
NOT 逻辑非 NOT a.flag = 'Y'
() 改变优先级 a.flag = 'Y' AND (a.czmc = '*京*' OR a.ljm = '*京*')

注意

  • 字符串字面量需要加单引号,例如 'Y''*京*'
  • * 在字符串值中可作为通配符使用,具体匹配行为取决于对应字段在索引中的类型(TEXT 字段支持 *京* 这种通配查询,TAG 字段暂不支持)。
  • IS NULL / IS NOT NULLFT.SEARCH 下推时映射到 RediSearch 的 ismissing(@field);连接后过滤时则按结果行的实际字段值判断。

# 2.4 RETURN 投影

RETURN 后紧跟字段数量,然后列出需要返回的字段,支持通过 AS 指定输出列名:

RETURN 3 a.name b.name c.name
RETURN 5 l.dm AS dm l.qc AS ddlqc l.jc AS ddljc g.qc AS gjqc g.jc AS gjjc

# 2.5 ORDER BY 排序

支持按字段或表达式排序,可指定 ASC(默认)或 DESC

支持的表达式:

表达式 说明
toNumber(alias.field) 将字段值按数值排序,常用于 ljpx 这类存储为字符串但语义为数字的字段
toPinyin(alias.field) 按字段值的全拼排序
toPinyinInitial(alias.field) 按字段值的拼音首字母排序(每个汉字取拼音的第一个字符)
alias.field 按字段原始字符串值排序

示例:

ORDER BY toNumber(lj.ljpx) ASC, toPinyin(a.fjm) ASC
ORDER BY toPinyinInitial(a.czmc) ASC

# 2.6 LIMIT 分页

LIMIT <offset> <count>

offset 为起始行偏移,count 为返回的最大行数。

# 2.7 TIMEOUT 超时

TIMEOUT <ms>

设置查询超时时间(毫秒),目前主要作用于底层 FT.SEARCH 阶段。


# 三、返回格式

FT.JOIN 返回一个数组,包含表头和数据行:

[[header1, header2, ...], [[val1, val2, ...], [val1, val2, ...], ...]]

例如:

[['a.name', 'lj.ljpx'], [['Beijing', '10'], ['Shanghai', '20']]]

LEFT JOIN 时,右表不匹配字段返回空字符串 ''


# 四、使用示例

以下示例假设已创建索引并插入数据。

# 4.1 创建示例索引

FT.CREATE join_a ON HASH PREFIX 1 ja: SCHEMA fjm TAG name TEXT flag TAG
FT.CREATE join_lj ON HASH PREFIX 1 jl: SCHEMA ljjc TAG ljpx NUMERIC SORTABLE

HSET ja:1 fjm BJ name Beijing flag Y
HSET ja:2 fjm SH name Shanghai flag Y
HSET ja:3 fjm GZ name Guangzhou flag Y
HSET jl:1 ljjc BJ ljpx 10
HSET jl:2 ljjc SH ljpx 20

# 4.2 两表 INNER JOIN

FT.JOIN join_a AS a join_lj AS lj ON a.fjm = lj.ljjc
RETURN 2 a.name lj.ljpx
ORDER BY toNumber(lj.ljpx) ASC

返回:

[['a.name', 'lj.ljpx'], [['Beijing', '10'], ['Shanghai', '20']]]

# 4.3 两表 LEFT OUTER JOIN

FT.JOIN LEFT join_a a join_lj lj ON a.fjm = lj.ljjc
RETURN 2 a.name lj.ljpx
ORDER BY a.name ASC

返回(GZ 没有匹配到 lj 表数据,右表字段为空):

[['a.name', 'lj.ljpx'], [['Beijing', '10'], ['Guangzhou', ''], ['Shanghai', '20']]]

# 4.4 带 WHERE 过滤

FT.JOIN join_a a join_lj lj ON a.fjm = lj.ljjc
WHERE a.fjm = 'BJ'
RETURN 2 a.name lj.ljpx
FT.JOIN join_a a join_lj lj ON a.fjm = lj.ljjc
WHERE a.flag = 'Y'
RETURN 2 a.name lj.ljpx
ORDER BY toNumber(lj.ljpx) ASC

# 4.4.1 不等值过滤

FT.JOIN join_a a join_lj lj ON a.fjm = lj.ljjc
WHERE a.flag != 'N'
RETURN 2 a.name lj.ljpx
ORDER BY toNumber(lj.ljpx) ASC

等价的 <> 写法:

FT.JOIN join_a a join_lj lj ON a.fjm = lj.ljjc
WHERE a.flag <> 'N'
RETURN 2 a.name lj.ljpx
ORDER BY toNumber(lj.ljpx) ASC

# 4.4.2 IS NULL / IS NOT NULL 过滤

创建带 NULL / 缺失字段的示例数据:

FT.CREATE join_null_a ON HASH PREFIX 1 jna: SCHEMA code TAG name TEXT flag TAG
FT.CREATE join_null_lj ON HASH PREFIX 1 jnl: SCHEMA code TAG px NUMERIC SORTABLE

HSET jna:1 code BJ name Beijing flag Y
HSET jna:2 code SH name Shanghai flag Y
HSET jna:3 code GZ name Guangzhou flag Y
HSET jna:4 code SZ name Shenzhen
HSET jnl:1 code BJ px 10
HSET jnl:2 code SH px 20

查询 flag 为 NULL 的记录(只有 Shenzhen 没有 flag 字段):

FT.JOIN LEFT join_null_a a join_null_lj lj ON a.code = lj.code
WHERE a.flag IS NULL
RETURN 2 a.name lj.px
ORDER BY a.name ASC

返回:

[['a.name', 'lj.px'], [['Shenzhen', '']]]

查询 flag 不为 NULL 的记录:

FT.JOIN LEFT join_null_a a join_null_lj lj ON a.code = lj.code
WHERE a.flag IS NOT NULL
RETURN 2 a.name lj.px
ORDER BY a.name ASC

返回:

[['a.name', 'lj.px'], [['Beijing', '10'], ['Guangzhou', ''], ['Shanghai', '20']]]

# 4.5 OR、括号和 NOT

创建示例数据:

FT.CREATE join_or_a ON HASH PREFIX 1 joa: SCHEMA flag TAG czmc TEXT ljm TEXT key TAG
FT.CREATE join_or_b ON HASH PREFIX 1 job: SCHEMA key TAG ljpx TEXT SORTABLE

HSET joa:1 flag Y czmc 北京南站 ljm 北京 key k1
HSET joa:2 flag Y czmc 南京站 ljm 南京 key k2
HSET joa:3 flag N czmc 上海虹桥 ljm 上海 key k3
HSET job:1 key k1 ljpx 京广线
HSET job:2 key k2 ljpx 京沪线

OR 条件:

FT.JOIN join_or_a a join_or_b b ON a.key = b.key
WHERE a.flag = 'Y' OR a.flag = 'N'
RETURN 2 a.czmc b.ljpx
ORDER BY a.czmc ASC

返回:

[['a.czmc', 'b.ljpx'], [['北京南站', '京广线'], ['南京站', '京沪线']]]

括号组合:

FT.JOIN join_or_a a join_or_b b ON a.key = b.key
WHERE a.flag = 'Y' AND (a.czmc = '*京*' OR a.ljm = '*京*')
RETURN 2 a.czmc b.ljpx
ORDER BY a.czmc ASC

NOT 条件:

FT.JOIN LEFT join_or_a a join_or_b b ON a.key = b.key
WHERE NOT a.flag = 'Y'
RETURN 2 a.czmc b.ljpx
ORDER BY a.czmc ASC

返回:

[['a.czmc', 'b.ljpx'], [['上海虹桥', '']]]

# 4.6 右表字段过滤

WHERE 条件可以引用右表字段,作为连接完成后的过滤。

FT.JOIN join_or_a a join_or_b b ON a.key = b.key
WHERE b.ljpx = '*京*'
RETURN 2 a.czmc b.ljpx
ORDER BY a.czmc ASC

组合使用左表和右表字段:

FT.JOIN join_or_a a join_or_b b ON a.key = b.key
WHERE a.flag = 'Y' AND (a.czmc = '*京*' OR b.ljpx = '*京*')
RETURN 2 a.czmc b.ljpx
ORDER BY a.czmc ASC

# 4.7 带 LIMIT 分页

FT.JOIN join_a a join_lj lj ON a.fjm = lj.ljjc
RETURN 2 a.name lj.ljpx
ORDER BY toNumber(lj.ljpx) ASC
LIMIT 0 10

# 4.8 派生子查询(Derived Subquery)

FT.CREATE idx_z ON HASH PREFIX 1 z: SCHEMA gbm TAG zname TEXT
FT.CREATE idx_l ON HASH PREFIX 1 l: SCHEMA dm TAG fmosdm TAG qc TEXT jc TEXT
FT.CREATE idx_g ON HASH PREFIX 1 g: SCHEMA dm TAG qc TEXT jc TEXT

HSET z:1 gbm DM1 zname Z1
HSET z:2 gbm DM2 zname Z2
HSET l:1 dm DM1 fmosdm DM1 qc LQC1 jc LJC1
HSET l:2 dm DM2 fmosdm DM2 qc LQC2 jc LJC2
HSET g:1 dm DM1 qc GQC1 jc GJC1
HSET g:2 dm DM2 qc GQC2 jc GJC2

FT.JOIN FROM idx_z AS z
JOIN (
  FROM idx_l AS l JOIN idx_g AS g ON l.fmosdm = g.dm
  RETURN 5 l.dm AS dm l.qc AS ddlqc l.jc AS ddljc g.qc AS gjqc g.jc AS gjjc
) AS a ON z.gbm = a.dm
RETURN 5 z.gbm a.ddlqc a.ddljc a.gjqc a.gjjc
ORDER BY z.gbm ASC

返回:

[['z.gbm', 'a.ddlqc', 'a.ddljc', 'a.gjqc', 'a.gjjc'],
 [['DM1', 'LQC1', 'LJC1', 'GQC1', 'GJC1'],
  ['DM2', 'LQC2', 'LJC2', 'GQC2', 'GJC2']]]

# 4.9 多表连接(3+ 表)

FT.CREATE idx_a ON HASH PREFIX 1 a: SCHEMA id TAG name TEXT
FT.CREATE idx_b ON HASH PREFIX 1 b: SCHEMA id TAG aid TAG name TEXT
FT.CREATE idx_c ON HASH PREFIX 1 c: SCHEMA id TAG bid TAG name TEXT

HSET a:1 id 1 name A1
HSET a:2 id 2 name A2
HSET b:1 id 10 aid 1 name B1
HSET b:2 id 20 aid 2 name B2
HSET c:1 id 100 bid 10 name C1
HSET c:2 id 200 bid 20 name C2

FT.JOIN FROM idx_a AS a
JOIN idx_b AS b ON a.id = b.aid
JOIN idx_c AS c ON b.id = c.bid
RETURN 3 a.name b.name c.name
ORDER BY a.name ASC

返回:

[['a.name', 'b.name', 'c.name'], [['A1', 'B1', 'C1'], ['A2', 'B2', 'C2']]]

# 五、集群模式使用

FT.JOIN 在集群模式下由接收请求的协调节点(coordinator)执行:

  1. 通过 FT.SEARCH fan-out 从各个 master 分片拉取连接所需字段。
  2. 在协调节点本地构建 hash table 并执行 join。
  3. 返回合并后的结果。

因此,集群模式下 join 的计算和内存消耗集中在协调节点,建议:

  • 对连接字段建立合适的索引(如 TAG)。
  • 使用 WHERELIMIT 尽早减少参与 join 的数据量。
  • 避免在数据量极大的索引上执行无过滤的多表 join。

集群测试脚本:

sudo python3 smoke_test/test_search_cluster.py \
  --server $(pwd)/_ci_build_/sudis_ds \
  --conf $(pwd)/skills/run-fkv-ds/dataservice.test.conf \
  --src-dir $(pwd) \
  --dump-dir $(pwd)/_ci_build_

# 六、常见错误

错误信息 原因
SEARCH_INDEX_NOT_FOUND Index not found: <index> 指定的索引不存在
JOIN field not found in left index schema: <field> 左表连接字段不在索引 Schema 中
JOIN field not found in right index schema: <field> 右表连接字段不在索引 Schema 中
WHERE condition references unknown table '<alias>' WHERE 中引用了未定义的表别名
Invalid WHERE condition near '<token>' WHERE 语法解析失败

# 七、当前限制

  1. 连接类型:仅支持 INNER JOIN 和 LEFT OUTER JOIN 的等值连接。
  2. WHERE 下推:只有全部字段都属于第一个基表的条件才能下推到 FT.SEARCH;涉及其他表的条件在连接完成后过滤。
  3. 连接键类型:建议连接字段使用 TAG 类型;NUMERIC/TEXT 等类型行为取决于 FT.SEARCH 对应查询语法。
  4. 超时控制TIMEOUT 目前主要作用于底层 FT.SEARCH,join 本身的 hash 构建、probe、排序阶段暂未强制检查超时。
  5. 资源上限:单节点构建侧最大行数 kMaxJoinBuildSize = 1M,结果集上限 kMaxJoinResultRows = 10M(硬编码)。