后台订单网格点 Export,转一会儿就报 Allowed memory size exhausted。把 PHP 内存从 768M 加到 2G 后暂时能导,订单再多一点又失败。这种处理只是把故障日期往后推,而且导出期间单个 PHP-FPM 进程会吃掉大量内存,容易连后台其他请求一起拖慢。

我会先做一个很朴素的对照:导出 100 条、1 万条和相同数量但更窄日期范围的数据,记录峰值内存。如果内存近似按行数线性增长,代码大概率把结果或 CSV 全攒在数组里;如果加某一列后突然暴涨,就去查这列的 renderer 或插件。

网格里“看不见”的 JOIN 也会付出代价

不少项目给订单网格加了商品 SKU、优惠明细、支付附加信息。页面只显示 20 行时问题不明显,导出会移除分页,把 JOIN 扩展后的所有行送进 PHP。先打印导出 collection 的 SQL,并检查:

  • 一张订单是否因为 item、shipment 或 transaction JOIN 变成多行;
  • renderer 是否对每一行再次加载完整订单,形成 N+1 查询;
  • 扩展字段是否反序列化了很大的 payment additional_information;
  • 导出插件是否先 getItems(),再构造一个完整二维数组。

临时拿数据:先缩小范围

如果运营当天就要文件,我会先按创建日期或 increment_id 分成几段,从后台分别导出。这是止血,不是最终方案。切分时要保存过滤条件和导出时间,避免几段之间漏单或重复。

长期方案:稳定游标 + 流式写文件

真正的大批量导出不适合绑在一个浏览器请求里。可以做 CLI 命令或队列任务,每批只取固定数量,以单调递增主键作为游标:

SELECT entity_id, increment_id, created_at, status, grand_total
FROM sales_order
WHERE entity_id > :last_id  AND created_at >= :from  AND created_at < :to
ORDER BY entity_id ASC
LIMIT 1000;

每批读取后立即写入已打开的 CSV 流,释放集合,再保存 last_id。不要用越来越大的 OFFSET;数据量大时它会越翻越慢。时间条件最好用半开区间,例如 [2025-07-01 00:00:00, 2025-07-02 00:00:00),边界更不容易重复。

如果导出过程中订单还会变化,要先定义一致性:是“任务开始时的快照”,还是“读取到这一刻的最新状态”。普通运营报表通常可以记录任务开始时间,并限制最大 entity_id;财务用途则可能需要从报表库或事务一致的快照生成。

CSV 本身也有几个容易忽略的坑

  • 用标准 CSV writer 处理逗号、引号和换行,不要手工字符串拼接;
  • 防止以 =、+、-、@ 开头的用户输入被表格软件当公式执行;
  • 明确金额导出的是 base currency 还是 order currency;
  • 客户邮箱、地址等数据应按权限生成,并设置下载文件的保留期。

验收时我不只看“文件能下载”。还会核对数据库条件下的订单数、CSV 行数、首尾订单、金额合计,并监控峰值内存是否在批次之间回落。这样才能证明导出已经从“碰运气的大请求”变成可控任务。