编程 MySQL 里跑 JavaScript 不是让你把业务搬进数据库:Percona 的 component_js_lang 只补 SQL 缺的那几步

2026-10-26 21:06:52

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 无法表述的那一步,业务决策仍留在应用层。

  1. 归一化 JSON key:四个合作方把 customer.id 拼成四种写法。norm_keys() 只有 9 行,报表从 51 行、四个 UNION ALL 分支缩到 19 行,且不再出现合作方名字。
  2. 入口处校验:proper_name()、clean_email()、clean_phone()、valid_iban()、clean_contact()。2000 行里抓出 460 行,每行附带具体错因。触发器保证脏数据进不了 contacts,无论插入方是 Web 应用、移动端、夜间导入还是凌晨两点的 mysql 命令行。被拒绝的行接下来怎么处理,是应用的事。
  3. 客户去重:Levenshtein 需要一张网格,lev_distance() 放进 JOIN 谓词。3000 个客户对 2000 条注册是 600 万对,按城市和首字母做 SQL 粗筛后剩 21,352 对进入距离计算。名字完全相等只能覆盖 21.7% 的重复;加上两道距离后约 80% 召回、99.7% 精度。
  4. 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 前的五个问题

  1. 它处理的是数据库里已经存在的数据吗?如果需要外部服务、文件或用户会话,那就是应用代码。
  2. 它是确定性的、在事务之外没有副作用吗?
  3. 它是从 SQL 里被调用的吗?好的 routine 坐在 SELECT、JOIN、JSON_TABLE 或触发器内部。
  4. 结果比输入小,或者省掉了一次往返吗?
  5. 是不是每个客户端都需要同一条规则?

落地建议

CREATE_JS_ROUTINE 要在 CREATE ROUTINE 之上单独授予,别顺手放进通用权限里。函数和 schema 迁移一起进版本库。检查调用点用的是阻塞条件还是过滤条件,让函数尽可能只在少量行上执行。

参考:JavaScript in MySQL: Extending SQL, Not Moving the Application

复制全文 生成海报 MySQL 数据库 JavaScript

推荐文章

程序员茄子在线接单