# FT.JOIN 命令
使用说明:
FT.JOIN 是 fkv_ds 搜索模块提供的 SQL/OQL 风格联表查询命令,支持在多个 RediSearch 索引(目前主要为 HASH 索引)之间执行 INNER / LEFT OUTER 等值连接(equi-join),并支持 WHERE、RETURN、ORDER BY、LIMIT、TIMEOUT 等子句。
# 一、命令语法
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 a与join_a a等价。- 连接条件
ON <qual-field> = <qual-field>只支持等值连接(equi-join)。 <qual-field>必须带表别名,格式为alias.field。
# 二、子句说明
# 2.1 FROM / JOIN 表引用
每个参与连接的索引必须指定一个别名(alias),后续 ON、WHERE、RETURN、ORDER BY 中均通过别名引用字段。
# 2.2 ON 连接条件
当前仅支持等值连接:
ON a.fjm = lj.ljjc
连接字段必须在对应索引的 Schema 中存在,否则会返回错误:
JOIN field not found in left/right index schema: <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 NULL在FT.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)执行:
- 通过
FT.SEARCHfan-out 从各个 master 分片拉取连接所需字段。 - 在协调节点本地构建 hash table 并执行 join。
- 返回合并后的结果。
因此,集群模式下 join 的计算和内存消耗集中在协调节点,建议:
- 对连接字段建立合适的索引(如
TAG)。 - 使用
WHERE和LIMIT尽早减少参与 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 语法解析失败 |
# 七、当前限制
- 连接类型:仅支持 INNER JOIN 和 LEFT OUTER JOIN 的等值连接。
- WHERE 下推:只有全部字段都属于第一个基表的条件才能下推到
FT.SEARCH;涉及其他表的条件在连接完成后过滤。 - 连接键类型:建议连接字段使用
TAG类型;NUMERIC/TEXT 等类型行为取决于FT.SEARCH对应查询语法。 - 超时控制:
TIMEOUT目前主要作用于底层FT.SEARCH,join 本身的 hash 构建、probe、排序阶段暂未强制检查超时。 - 资源上限:单节点构建侧最大行数
kMaxJoinBuildSize = 1M,结果集上限kMaxJoinResultRows = 10M(硬编码)。