duckdb-luajit 接上 scipy 求解器,SQL 里解线性规划
数据分析做到最后,总有一道坎:算完了,然后呢?
比如排产。仓库里 300 个订单,3 条产线,每条产线工时、物料、交期都不一样,怎么排才能让总成本最低?比如运输。3 个工厂往 3 个城市发货,运费不同、产能不同、需求不同,怎么调才最省?
这类问题有个正经名字:线性规划(LP)。SQL 里的 GROUP BY、窗口函数、CASE WHEN 全用上,也算不出来——因为它是"在约束里找最优",不是"按条件汇总"。
以前的办法:把数据导出给 Python,调 scipy.optimize,算完再把结果导回来。来回折腾,还要维护一套 Python 环境。
我一直在做的 duckdb-luajit 扩展,在 DuckDB 里塞了一个 LuaJIT 运行时,SQL 里能直接跑 Lua。这次我给它的 libs 仓库加了一个新成员:highs——一个纯 Lua 胶水,FFI 直调 HiGHS 求解器。
HiGHS 这个名字可能陌生,但它的用户你天天见:scipy 的 linprog、MATLAB 的 linprog、JuMP 的默认求解器,底层都是它。MIT 许可,C API 干净,LuaJIT 的 FFI 可以直接调,连绑定代码都不用写。
整条链路长这样:
duckdb-luajit highs 架构
装法就一条 SQL:
LOAD luajit;
SELECT * FROM luajit_module(mode := 'install', sql_name := 'highs');
然后,经典饮食问题——用最少的钱满足营养需求:
SELECT luajit_s('highs', {'op':'lp', 'sense':'min',
'col_cost':[0.18, 0.23, 0.05], -- 玉米/牛奶/面包单价
'col_lower':[0,0,0], 'col_upper':[10,10,10],
'row_lower':[5000,-1e30,2000,-1e30], -- 维A、卡路里下限
'row_upper':[1e30,50000,1e30,2250],
'a_start':[0,2,4,5], 'a_index':[0,2,0,2,2], 'a_value':[107,72,500,121,65]});
返回:
{"status":"Optimal","objective":3.15,"solution":[1.9444,10,10]}
1.9444 份玉米、10 份牛奶、10 份面包,总价 3.15 美元——和 MATLAB 官方示例逐位一致。
LP 之外还有两个口子:mip(整数规划,变量要求整数——比如面包只能整份买),qp(二次规划,目标函数带平方项)。同一个接口,换 op 就行。
验证我是拿 HiGHS 官方 CLI 对拍的:运输问题,3 工厂 3 城市 9 个变量 6 条约束,SQL 里解出最优 1510,CLI 独立跑同一份 LP 文件也是 1510,分毫不差。
踩坑也有。QP 第一次跑直接段错误——Highs_qpCall 的 C 签名 25 个参数,q_start 是 INT[] 不是 double[],还比 lpCall 多两个 basis status 指针。FFI 声明少写一个参数,进程当场崩。对着头文件逐字核对才修好。
这套东西在 GitHub 上,duckdb-luajit-libs 仓库,install 一条 SQL 就能用。MIT 协议,随便用。
SQL 里做运筹,门刚打开。线性规划能解了,下一步整数规划、二次规划都进来了——仓库里还有数值线性代数(SVD、特征值、矩阵分解)在路上。
你有没有在 SQL 里遇到"非要导出给 Python 才能算"的问题?评论区聊聊。
● ● ●
参考来源
- 1.HiGHS 求解器:github.com/ERGO-Code/HiGHS
- 2.duckdb-luajit 扩展:github.com/alitrack/duckdb-luajit
- 3.duckdb-luajit-libs 库仓库(highs 在此):github.com/alitrack/duckdb-luajit-libs
- 4.HiGHS C API 头文件(Highs_lpCall/mipCall/qpCall 签名):github.com/ERGO-Code/HiGHS/blob/master/src/interfaces/highs_c_api.h
- 5.MATLAB 饮食问题官方示例(最优解 C=1.944, M=10, B=10, obj=3.15):mathworks.com/help/optim/ug/linprog.html