You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Laravel销售报表查询优化求助:3000+数据耗时10分钟

Laravel销售报表查询优化方案

你的查询耗时10分钟核心原因在于重复子查询、缺少索引、全量数据加载以及不必要的SQL计算,以下是针对性优化方案:

1. 替换重复子查询为LEFT JOIN + 分组

原查询中多次对order_voucher执行子查询,每条数据都会触发6次独立查询,这是性能瓶颈的核心。改用LEFT JOIN结合分组取第一条记录的方式:

$query = OrderDetail::select(
    'order_detail.id as no',
    'order_detail.order_id as id',
    'order_booking.booking_number as booking_id',
    'order_booking.sales_code as sales_code',
    DB::raw('CONCAT(customer.first_name, " ", customer.last_name) as fullname'),
    'customer.sap_code',
    'customer.phone_number as handphone',
    'order.created_at as date_order',
    'order.invoice_number as invoice',
    'order.so_number',
    'order.ro_sap as do_number',
    // 替换order_shipping.*为具体需要的字段
    'order_shipping.address as shipping_address',
    'store_location.sap_code as kode_toko',
    'store_location.name as name_location',
    'order_detail.name as name_prod',
    'order_detail.model as model',
    'order_detail.product_price as a_pcs',
    'order_detail.qty',
    'order_detail.price',
    'metode.name as metode_pembayaran',
    'order_booking.tenor as name_tenor',
    // 用CASE分组替换子查询
    DB::raw('MAX(CASE WHEN ov.voucher_type_id <=4 THEN ov.voucher_code END) as vch_code_product'),
    DB::raw('MAX(CASE WHEN ov.voucher_type_id <=4 THEN ov.discount ELSE 0 END) as payment_voucher_product'),
    DB::raw('MAX(CASE WHEN ov.voucher_type_id =5 THEN ov.voucher_code END) as vch_code_shipment'),
    DB::raw('MAX(CASE WHEN ov.voucher_type_id =5 THEN ov.discount ELSE 0 END) as payment_voucher_shipment'),
    DB::raw('MAX(CASE WHEN ov.voucher_type_id =6 THEN ov.voucher_code END) as vch_code_bank'),
    DB::raw('MAX(CASE WHEN ov.voucher_type_id =6 THEN ov.discount ELSE 0 END) as payment_voucher_bank'),
    'order_status.name as name_status',
    'order_booking.channel as channel',
    'order_booking.device as device',
    'order_detail.rating as rating',
    'order_detail.cwh_code',
    'order.shipping_cost'
)
->join('order', 'order_detail.order_id', '=', 'order.id')
->join('order_booking', 'order_booking.id', '=', 'order.booking_id')
->join('order_shipping', 'order_shipping.order_id', '=', 'order.id')
->join('customer', 'customer.id', '=', 'order.customer_id')
->join('store_location', 'store_location.id', '=', 'order.store_location_id')
->join('order_status', 'order_status.id', '=', 'order.order_status_id')
->join('metode', 'metode.id', '=', 'order_booking.metode_id')
// 左连接order_voucher并按order_id取第一条有效记录
->leftJoin(DB::raw('(SELECT ov.* FROM order_voucher ov 
    JOIN (SELECT order_id, MIN(id) as min_id FROM order_voucher 
          WHERE voucher_type_id IN (4,5,6) GROUP BY order_id) ov_min 
    ON ov.id = ov_min.min_id) ov'), 'ov.order_id', '=', 'order.id')
->where(['order.status' => 1])
->whereBetween('order.created_at', [$start, $end])
->groupBy('order_detail.id')
->orderBy('no', 'asc');

2. 添加数据库索引

给关联字段、查询条件字段添加索引,大幅提升JOIN和WHERE语句的执行效率:

-- order表索引
CREATE INDEX idx_order_status_created_at ON `order`(status, created_at);
CREATE INDEX idx_order_booking_id ON `order`(booking_id);
CREATE INDEX idx_order_customer_id ON `order`(customer_id);
CREATE INDEX idx_order_store_location_id ON `order`(store_location_id);

-- order_detail表索引
CREATE INDEX idx_order_detail_order_id ON order_detail(order_id);

-- order_voucher表索引
CREATE INDEX idx_order_voucher_order_id_type ON order_voucher(order_id, voucher_type_id);

-- 关联表辅助索引
CREATE INDEX idx_order_booking_metode_id ON order_booking(metode_id);
CREATE INDEX idx_order_shipping_order_id ON order_shipping(order_id);

3. 分块处理数据,避免内存溢出

3000+行数据用get()会一次性加载到内存,改用chunk()分块处理,同时适合生成下载文件:

// 示例:分块生成CSV报表
$file = fopen(storage_path('app/sales_report.csv'), 'w');
// 写入表头
fputcsv($file, [
    '序号', '订单ID', '预订号', '销售码', '客户全名', 'SAP码', '手机号',
    '下单日期', '发票号', 'SO号', 'DO号', '收货地址', '门店编码', '门店名称',
    '产品名称', '型号', '单价', '数量', '折扣', '小计', '支付方式', '分期期限',
    '产品优惠券码', '产品优惠金额', '运费优惠券码', '运费优惠金额',
    '银行优惠券码', '银行优惠金额', '实付金额', '订单状态', '渠道', '设备',
    '评分', 'CWH码', '运费'
]);

$query->chunk(200, function($rows) use ($file) {
    foreach ($rows as $row) {
        // 在PHP中计算折扣、小计、实付金额
        $discount = $row->price !== null ? ($row->a_pcs - $row->price) : 0;
        $subtotal = $row->qty * $row->a_pcs - $discount;
        $payment = $subtotal - $row->payment_voucher_product - $row->payment_voucher_shipment - $row->payment_voucher_bank;

        // 组装行数据写入文件
        fputcsv($file, [
            $row->no, $row->id, $row->booking_id, $row->sales_code,
            $row->fullname, $row->sap_code, $row->handphone,
            $row->date_order->format('Y-m-d H:i:s'), $row->invoice,
            $row->so_number, $row->do_number, $row->shipping_address,
            $row->kode_toko, $row->name_location, $row->name_prod,
            $row->model, $row->a_pcs, $row->qty, $discount,
            $subtotal, $row->metode_pembayaran, $row->name_tenor,
            $row->vch_code_product, $row->payment_voucher_product,
            $row->vch_code_shipment, $row->payment_voucher_shipment,
            $row->vch_code_bank, $row->payment_voucher_bank,
            $payment, $row->name_status, $row->channel,
            $row->device, $row->rating, $row->cwh_code,
            $row->shipping_cost
        ]);
    }
});

fclose($file);

4. 复杂计算逻辑移到PHP处理

原SQL中的subtotal、payment等复杂计算,移到PHP中处理,减少数据库计算压力,同时更易维护。

5. 缓存高频报表数据

如果是固定时间段的报表(如月度报表),可以将查询结果缓存,避免重复计算:

$cacheKey = 'sales_report_' . $start->format('Y-m') . '_' . $end->format('Y-m');
$reportData = Cache::remember($cacheKey, 3600, function() use ($query) {
    return $query->get();
});

6. 分析查询执行计划

用Laravel查询日志或数据库EXPLAIN语句定位剩余性能瓶颈:

DB::enableQueryLog();
$query->get();
dd(DB::getQueryLog());

或者直接在数据库执行:

EXPLAIN [你的完整SQL语句];

内容的提问来源于stack exchange,提问作者Mike

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.22 21:58:14