MySQL 里跑 JavaScript 不是让你把业务搬进数据库:Percona 的 component_js_lang 只补 SQL 缺的那几步
Percona Live Europe 2026 上,Dmitry Lenev 和 Marco Tusa 的演讲题目是「JavaScript in My Database? Over My Dead Query」,讲的是 component_js_lang:Percona Server for MySQL 8.4 上基于 V8 的 JavaScript 引擎。从 Percona Server for MySQL 8.4.5-5 起以 tech preview 形式提供,免费开源;Oracle 侧同类能力属于 MySQL Enterprise。
最常见的误读是把这个能力等同于「把应用逻辑搬进数据库」——把订单流转、定价规则、REST 调用塞进存储过程。这个图景不成立,因为引擎本身就把边界钉死了。
引擎边界:一个没有 I/O 的 V8
在 component_js_lang 里,一个 routine 能做的事被严格限制:
- 没有网络 I/O。没有
fetch(),没有 socket。 - 没有文件系统访问。
- 没有 Node.js 或 DOM API。没有
require(),没有process,没有 npm 生态。 - 没有可用的 async。所有代码在语句内部同步执行完。
所以它不是「数据库里的 Node」,而是一个纯函数运行时。
定位:SQL 管集合,JS 管值
JavaScript routine 只是给 SQL 词汇表加了一个函数,调用方式和 LOWER()、JSON_EXTRACT() 一样,可以出现在 SELECT、JOIN、WHERE、GROUP BY 或触发器里。唯一的新语法是 LANGUAGE JS:
CREATE FUNCTION norm_keys(d JSON) RETURNS JSON
DETERMINISTIC NO SQL LANGUAGE JS AS $$
const walk = (j) => {
if (j === null || typeof j !== 'object') return j
if (Array.isArray(j)) return j.map(walk)
const out = {}
for (const [k, v] of Object.entries(j))
out[k.toLowerCase()] = walk(v)
return out
}
return walk(typeof d === 'string' ? JSON.parse(d) : d)
$$;
DETERMINISTIC NO SQL 不只是 binlog 的记录要求:它声明了相同输入得到相同输出、函数内部不执行 SQL 语句。这两点决定了调用点能否被优化器安全地放到过滤条件里。
SQL 真的缺的那几步
边界出现在 SQL 及其过程语言表达不了的地方:
- 未知形状的递归:
JSON_TABLE要求每条路径都是字面量,路径本身依赖数据时就没法写。 - 数组:SQL 没有数组类型,任何需要填一张网格的算法都无处安放。
- 大整数:
BIGINT止步于 19 位,而 IBAN 的 mod-97 校验需要 29 位。 - 带聚合的真实迭代:递归 CTE 不能包含聚合或窗口函数(
ERROR 3575);PL 里的WHILE循环每条语句都要付一次执行器往返。 - 大小写敏感的字符串处理:
UPPER()/LOWER()不知道 McDonald 里那个大写 D。
四个 demo
四个演示都在 pleu_2026 仓库里,形状一致:SQL 做集合运算,一个小 JS 函数做 SQL 无法表述的那一步,业务决策仍留在应用层。
- 归一化 JSON key:四个合作方把
customer.id拼成四种写法。norm_keys()只有 9 行,报表从 51 行、四个UNION ALL分支缩到 19 行,且不再出现合作方名字。 - 入口处校验:
proper_name()、clean_email()、clean_phone()、valid_iban()、clean_contact()。2000 行里抓出 460 行,每行附带具体错因。触发器保证脏数据进不了contacts,无论插入方是 Web 应用、移动端、夜间导入还是凌晨两点的 mysql 命令行。被拒绝的行接下来怎么处理,是应用的事。 - 客户去重:Levenshtein 需要一张网格,
lev_distance()放进 JOIN 谓词。3000 个客户对 2000 条注册是 600 万对,按城市和首字母做 SQL 粗筛后剩 21,352 对进入距离计算。名字完全相等只能覆盖 21.7% 的重复;加上两道距离后约 80% 召回、99.7% 精度。 - Federico 该住哪:用 Weiszfeld 算法迭代求几何中位数。过程式 SQL 11.00 秒、327,033 条语句;JavaScript 0.52 秒、6 条语句。这个 demo 用到
session.runSql(),所以目前只能跑在 MySQL Enterprise 上,从 JavaScript 调用 SQL 是 Percona Server 路线图上的头号条目。把 44,347 个原始站点压成 16,232 个加权端点,则是两个窗口函数的事。
接受一个 routine 前的五个问题
- 它处理的是数据库里已经存在的数据吗?如果需要外部服务、文件或用户会话,那就是应用代码。
- 它是确定性的、在事务之外没有副作用吗?
- 它是从 SQL 里被调用的吗?好的 routine 坐在
SELECT、JOIN、JSON_TABLE或触发器内部。 - 结果比输入小,或者省掉了一次往返吗?
- 是不是每个客户端都需要同一条规则?
落地建议
CREATE_JS_ROUTINE 要在 CREATE ROUTINE 之上单独授予,别顺手放进通用权限里。函数和 schema 迁移一起进版本库。检查调用点用的是阻塞条件还是过滤条件,让函数尽可能只在少量行上执行。
参考:JavaScript in MySQL: Extending SQL, Not Moving the Application