ByteNoteByteNote
D1 里用 json_extract 做模糊查
字

字节笔记本

2026年10月7日 · 约 12 分钟读完

D1 里用 json_extract 做模糊查

API中转
¥120

Cloudflare D1 是建在 SQLite 上的 SQL。JSON 可以放进声明成 JSON 的列,也可以放进 TEXT。查询时用 json_extract 取出路径,再对这个文本做 LIKE。模糊条件扫的是抽出来的值,不是整格原样比较。

从 JSON 里抽出路径再 LIKE

插入时用 json 包一层

对话里的表是 users:整型主键、name、profile JSON。插入时用 json('{"age":30,"city":"New York","hobbies":["reading","hiking"]}'),让库存的是 JSON 值,而不是一段未校验的文本。查询年龄和城市:

sql
SELECT name,
       json_extract(profile, '$.age') AS age,
       json_extract(profile, '$.city') AS city
FROM users
WHERE json_extract(profile, '$.city') = 'New York';

路径 $.city 是顶层键。$.address.street 走到嵌套对象。等号是精确匹配。要模糊,把等号换成 LIKE '%York%',仍然先 json_extract。百分号是 SQL 的通配,不是 JSON 的语法。

另一张表 products 把细节放在 TEXT 列 details,插入的是普通字符串 '{"brand":"Dell",...}'。这样更宽松,拼错了 JSON 也能落库。查询前 SQLite 仍要用 JSON 函数去读,读失败就拿不到字段。能在写入时用 json() 包一层,就少一次“看起来像 JSON、解析时才爆”的数据。

应用代码里用预编译语句,避免把用户输入拼进 SQL。对话里的写法是 db.prepare('INSERT INTO users (name, profile) VALUES (?, ?)'),名字一个参数,JSON.stringify 之后的 profile 另一个参数,再 stmt.run。字符串化发生在应用里,占位符负责不把引号打断 SQL。

数组用 json_each 拆开

爱好在数组里,对整段 profile 做 LIKE '%read%' 也会碰中,但会误伤别的字段里同样的字母。对话里的做法是先抽出 $.hobbies,再 json_each 拆成行:

sql
SELECT id, name
FROM users, json_each(json_extract(profile, '$.hobbies'))
WHERE value LIKE '%ing%';

每一项爱好变成一行 value。reading 和 hiking 都能匹配 %ing%。一个人有两项命中时,这个人会出现两次,外面要不要 DISTINCT 取决于你要人还是要爱好。

嵌套街道用 json_extract(profile, '$.address.street') LIKE '%Main%'。路径写错时,json_extract 给出空,LIKE 不会命中,查询成功但结果是空的。空结果要先核对路径,再怀疑数据。

对话里还有一份用递归 CTE 包住 json_tree 的查询。json_tree 在 SQLite 里是表值函数,直接 FROM json_tree(profile) 才能遍历每个节点,再用 type 滤掉 object 和 array,对叶子的 value 做 LIKE。那份 CTE 把列的来源写成子查询里的 json_tree(profile),和表值函数的用法对不上,不要原样执行。要搜任意深度,用 json_tree 的表值形式,并限制在已知的小表上。

LIKE 扫的是文本

LIKE '%York%' 不能用普通的等值索引把百分号两边都省掉。城市这种固定路径,精确查询写成 json_extract(...) = 'New York' 更合适。模糊只留给搜索框。对话里提醒过:数据变大之后,这种查询会变慢,常查的路径值得单独想索引,精确匹配比 LIKE 便宜。

不要对整列 profile 做 LIKE '%secret%' 当权限过滤。抽出的是搜索,不是访问控制。谁能读这张表,仍然靠库的权限和应用里的条件,而不是靠模糊语句“碰巧搜不到”。

更新某一项时,读出 JSON、在应用里改、再整格写回,和在 SQL 里用 JSON 函数替换,是两条路。对话里的预编译插入覆盖的是整格 profile。只改 city 却把整个对象重写时,要带上原来的 age 和 hobbies,否则一次更新会把没改的键抹掉。

数组拆行之后再匹配

五次查询各自打在哪一层

对话里的模糊查询可以按数据形状对上,不要混成一条万能语句。

第一条,顶层属性:json_extract(profile, '$.city') LIKE '%York%'。New York 能中,Yorktown 也能中。若只要整座城市,用等号和完整字符串。

第二条,数组。样本写成 profile -> '$.hobbies' LIKE '%read%'。箭头是取出路径。更稳的拆法是后面那条 json_each,因为 LIKE 打在整个数组文本上时,括号和引号也算文本,匹配的是子串不是元素。

第三条,嵌套对象:json_extract(profile, '$.address.street') LIKE '%Main%'。路径多一段就多一个点。address 不存在时结果为空,语句不报错。

第四条用递归 CTE 调用 json_tree。表值函数应出现在 FROM 里。写成 CTE 内部的 SELECT key, value FROM json_tree(profile) 再和 users 做无连接条件的组合,既可能语法不对,也会在行数上相乘。要遍历所有叶子,用 FROM users, json_tree(users.profile),并限制 type 不是 object 也不是 array,再对 value 做 LIKE。这是 SQLite 里 json_tree 的用法。对话里那份 CTE 不要原样上库。

第五条是爱好:json_each(json_extract(profile, '$.hobbies')),value LIKE '%ing%'。reading、hiking 命中。返回的是用户的 id 和 name。一项命中一行,需要人的集合时加 DISTINCT。

写入和查询用同一格

JSON 列的插入样本把 json() 套在字面量外面。TEXT 列的插入没有 json(),只是一段引号里的文本。读的函数两边都能试,但 TEXT 里如果少了引号或逗号,json_extract 给空。插入路径统一用 json() 或在应用里 JSON.stringify 之后再入库,读的时候才有稳定的路径。

预编译是 INSERT INTO users (name, profile) VALUES (?, ?)。第一个问号是 'Bob' 这种名字,第二个是 JSON.stringify 的结果,对象里有 age、city、hobbies。不要把 JSON.stringify 的结果再拼进 SQL 字符串。用户名里有引号时,占位符会处理,字符串拼接不会。

更新整格 profile 时带上未修改的键。查询只投影 json_extract 出来的年龄和城市,不代表库里只存了这两项。写回若只构造 {city: ...},爱好就没了。先读出整格,改一个键,再写回整格。

模糊查询对话里已经说明会更慢,精确匹配更合适做过滤。搜索框再用 LIKE。这不是权限判断。

路径写对,再谈模糊

D1 里 JSON 可以进 JSON 列,用 json() 插入,用 json_extract 按 $.键 取出。精确条件用等号,模糊才用 LIKE。数组用 json_each 拆开,避免在整段文本里误匹配。任意深度用 json_tree 的表值形式。写入走预编译和 JSON.stringify。模糊查询是搜索,不是权限。

插入样本可以当成最小验收。一行 Alice,profile 里有 age 为 30、city 为 New York、hobbies 含 reading 和 hiking。精确查询 $.city 等于 New York 应返回这一行。LIKE '%York%' 也返回。json_each 加 LIKE '%ing%' 返回她,而且可能两行。$.address.street 在这行数据里不存在,模糊查询应为空,而不是报错。空结果先对路径,再查数据。

Bob 那条走预编译,JSON.stringify 的对象含 age、city、hobbies。用问号占位,不要拼接。products 若用 TEXT 存细节,少一个引号时 json_extract 拿不到 brand。能改成 json() 插入就改。更新 city 时读出整格再写回,避免只写一个键把爱好清掉。

任意深度的搜索用 FROM json_tree(...),滤掉 object 和 array。对话里的递归 CTE 不上生产。数据变大之后,精确条件优先,LIKE 留给搜索框。谁能读表,不由模糊语句决定。

json() 和 JSON.stringify 都是在写入前把文本收成 JSON。前者在 SQL 里,后者在应用里,预编译语句用问号接住后者。读的时候路径统一从 $. 开始。顶层用 $.city,嵌套用 $.address.street,数组先 json_extract 再 json_each。不要对整段 profile 做 LIKE,那会把别的键里的相同字母也算成命中。搜索框可以模糊,列表过滤用等号。对话里关于索引的提醒留在数据变大之后,先把路径写对。

users 的主键是整数自增,name 是文本,profile 是 JSON。查询投影里给抽出的值起了 age 和 city 两个别名,方便应用读取,不改变库里的形状。products.details 是文本列时,应用要自己保证字符串是合法 JSON。两种列可以并存,同一条业务不要一会儿写 JSON 列、一会儿写文本列,否则 json_extract 的路径看起来一样,空结果的原因却不同。插入 Alice 那一行用来验收城市等号和爱好的拆行查询,两条都命中再算路径写对。空的街道查询保持为空,不要把它当成数据库本身出了故障。

相关文章

分享: