Lravel-excel 导出不到2000条数据,任务超时

由于评论区有字数限制,在这里统一回复,感谢大家的指导帮助,这个扩展包能试的方法(queueLazyCollectionchunk_sizecsv)我都试了问题还是照旧,一两百条可以导出,800 条左右偶尔成功一次,900、1000往上都是超时(任务重试时间 600s、6000s 都尝试过)。

通过记录各个部分执行的时间,得出查询速度并不慢,主要问题还是在文件写入上。

查询时间日志:

Lravel-excel 导出不到2000条数据,任务超时

偶尔成功的一次:
虽然成功了,效率也不尽人意。:mask:

Lravel-excel 导出不到2000条数据,任务超时

因为需求拖得时间太长了,只能暂时放弃这个包,尝试使用「box/spout」,使用这个包以后同样的数据导出csv耗时如下:

Lravel-excel 导出不到2000条数据,任务超时

效率可能还是一般,但是总算能交作业了。

代码如下:

水平比较菜,代码比较烂,大佬们轻拍,有硬伤和需要优化的地方欢迎指正。

<?php

namespace App\Containers\Statistic\Jobs;

use App\Containers\Order\Models\Order;
use App\Containers\OrderItem\Models\OrderItem;
use App\Containers\Statistic\Models\Report;
use App\Containers\User\Models\User;
use App\Ship\Parents\Jobs\Job;
use Box\Spout\Writer\Common\Creator\WriterEntityFactory;
use Exception;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;

/**
 * Class CallCenterOrdersJob
 */
class CallCenterOrdersJob extends Job
{
    private $recipients;

    public function __construct(array $recipients)
    {
        $this->recipients = $recipients;
    }

    public function handle()
    {
        Log::info('开始(电话中心)订单明细统计报表任务');

        $timeStart = \microtime(\true);

        $report = $this->recipients['report'];

        try {
            // 时间段
            $whereBetween = [$report['conditions']['date_start'], $report['conditions']['date_end']];

            // 订单搜索条件
            $where = [];
            if (isset($report['conditions']['channel_id'])) {
                $where[] = ['channel_id', $report['conditions']['channel_id']];
            }
            if (isset($report['conditions']['status'])) {
                $where[] = ['status', $report['conditions']['status']];
            }

            // 下单客服条件
            $createrWhere = [
                ['group', 'customerservice']
            ];
            if (isset($report['conditions']['department_group'])) {
                $createrWhere[] = ['department_group', $report['conditions']['department_group']];
            }

            // 订单商品条件
            $productWhere = [];
            if (isset($report['conditions']['product_name'])) {
                $productWhere[] = ['name', $report['conditions']['product_name']];
            }
            if (isset($report['conditions']['product_sn'])) {
                $productWhere[] = ['sn', $report['conditions']['product_sn']];
            }

            // 查询订单
            $query = Order::query()
                ->with([
                    'creater',
                    'member',
                    'items',
                ])
                ->whereBetween('created_at', $whereBetween)
                ->where($where)
                // 按客服条件搜索
                ->WhereIn(
                    'creater_id',
                    User::query()
                        ->select('id')
                        ->where($createrWhere)
                        ->whereHas('roles', function (Builder $query) use ($report)
                        {
                            $query->where('id', $report['conditions']['role_id']);
                        })
                        ->pluck('id')
                );

            // 订单商品条件搜索
            if (!empty($productWhere)) {
                $query->WhereIn(
                    'order_id',
                    OrderItem::query()
                        ->select('order_id')
                        ->where($productWhere)
                        ->pluck('order_id')
                );
            }
            Log::info('报表:'.$report->id.'总共有'.$query->count().'条数据');

            $path = 'uploads/media/reports/'.date('Ym/d', time()).'/'.$report->conditions['date_start'].'-'.$report->conditions['date_end'].'-订单明细统计报表(电话中心)'.date('His').'.csv';

            // 创建临时文件
            $tmpPath = 'public/'.$path;
            Storage::disk('local')->put($tmpPath, NULL);

            $writer = WriterEntityFactory::createCSVWriter()
                ->openToFile(\str_replace('storage', 'storage/app', \storage_path($tmpPath)))
                ->addRow(WriterEntityFactory::createRowFromArray([
                    '订购日期',
                    '订单号',
                    '订单状态',
                    '货号',
                    '货品名',
                    '颜色',
                    '型号',
                    '货品积分',
                    '售价',
                    '订单数量',
                    '订单金额',
                    '付款方式',
                    '会员编号',
                    '会员姓名',
                    '组别(角色)',
                    '下单客服',
                    '下单部门',
                ]));

            $query->chunk('1000', function ($orders) use ($writer)
            {
                $addRows = [];
                foreach ($orders as $order) {
                    $addRows[] = WriterEntityFactory::createRowFromArray([
                        '="'.$order->created_at.'"',
                        '="'.$order->sn.'"',
                        $order->status_str,
                        isset($order->items[0]) ? $order->items[0]->sn : '',
                        isset($order->items[0]) ? $order->items[0]->name : '',
                        isset($order->items[0]) ? $order->items[0]->product_color : '',
                        isset($order->items[0]) ? $order->items[0]->product_type : '',
                        $order->bonus_point,
                        isset($order->items[0]) ? $order->items[0]->sell_price_taxed : 0,
                        isset($order->items[0]) ? $order->items->sum('num') : 0,
                        $order->total_amount,
                        $order->is_online_pay ? '在线支付' : '线下支付',
                        $order->member ? '="'.$order->member->sn.'"' : '',
                        $order->member_name,
                        (isset($order->creater) && !empty($order->creater->roles)) ? $order->creater->roles[0]->display_name : '',
                        $order->creater_name,
                        $order->creater_department,
                    ]);
                }
                $writer->addRows($addRows);
            });

            $writer->close();

            // 上传到OSS
            Storage::put($path, Storage::disk('local')->get($tmpPath), 'private');

            // 删除本地缓存
            Storage::disk('local')->delete($tmpPath);

            // 更新报表任务状态
            $report->update([
                'file_path' => $path,
                'status'    => Report::STATUS_COMPLETED,
            ]);

        } catch (\Throwable $th) {
            // 更新报表任务状态
            $report->update([
                'status'    => Report::STATUS_FAILED,
            ]);
            Log::info('报表报错处理:'.$report->id);
            Log::info($th);
            throw $th;
        }

        $timeEnd = \microtime(\true);

        Log::info('电话中心订单明细报表任务总耗时:'.($timeEnd - $timeStart));
    }


    /**
     * 任务失败的处理过程
     *
     * @param  Exception  $exception
     * @return void
     */
    public function failed(Exception $exception)
    {
        // 更新报表任务状态
        $this->recipients['report']->update([
            'status'    => Report::STATUS_FAILED,
        ]);
        Log::info('任务失败处理:'.$this->recipients['report']->id);
        Log::info($exception);
        throw $exception;
    }
}

没错这是分割线,因为这个分割先把剪贴板写好的东西全丢了!
评论区有字数限制,写完提交不了,复制出来想编辑原文,结果复制了一条分割线,剪贴板东西就不见了。:sob:


原问题

如题使用laravel-excel 3.1From Query with Queued导出不到2000条数据(从400万数据中关联查询),任务超时,报如下错误:

Illuminate\Queue\MaxAttemptsExceededException: Maatwebsite\Excel\Jobs\AppendQueryToSheet has been attempted too many times or run too long. The job may have previously timed out. in /var/www/lj-dg-api/vendor/laravel/framework/src/Illuminate/Queue/Worker.php:632
  • Laravel Framework 6.20.7
  • PHP 7.3.25-1+ubuntu18.04.1+deb.sury.org+1 (cli) (built: Dec 26 2020 10:32:33) ( NTS )
  • mysql 8.0 (rds)
  • redis (云服务)

测试环境少量数据(十几条)是可以成功的,当然生产环境数据量也不算大

请大佬们帮忙诊断下问题处在哪里,万分感谢!

laravel-excel 配置

<?php

use Maatwebsite\Excel\Excel;

return [
    'exports' => [

        /*
        |--------------------------------------------------------------------------
        | Chunk size
        |--------------------------------------------------------------------------
        |
        | When using FromQuery, the query is automatically chunked.
        | Here you can specify how big the chunk should be.
        |
        */
        'chunk_size'             => 100,

        /*
        |--------------------------------------------------------------------------
        | Pre-calculate formulas during export
        |--------------------------------------------------------------------------
        */
        'pre_calculate_formulas' => false,

        /*
        |--------------------------------------------------------------------------
        | Enable strict null comparison
        |--------------------------------------------------------------------------
        |
        | When enabling strict null comparison empty cells ('') will
        | be added to the sheet.
        */
        'strict_null_comparison' => false,

        /*
        |--------------------------------------------------------------------------
        | CSV Settings
        |--------------------------------------------------------------------------
        |
        | Configure e.g. delimiter, enclosure and line ending for CSV exports.
        |
        */
        'csv'                    => [
            'delimiter'              => ',',
            'enclosure'              => '"',
            'line_ending'            => PHP_EOL,
            'use_bom'                => false,
            'include_separator_line' => false,
            'excel_compatibility'    => false,
        ],

        /*
        |--------------------------------------------------------------------------
        | Worksheet properties
        |--------------------------------------------------------------------------
        |
        | Configure e.g. default title, creator, subject,...
        |
        */
        'properties'             => [
            'creator'        => '',
            'lastModifiedBy' => '',
            'title'          => '',
            'description'    => '',
            'subject'        => '',
            'keywords'       => '',
            'category'       => '',
            'manager'        => '',
            'company'        => '',
        ],
    ],

    'imports'            => [

        /*
        |--------------------------------------------------------------------------
        | Read Only
        |--------------------------------------------------------------------------
        |
        | When dealing with imports, you might only be interested in the
        | data that the sheet exists. By default we ignore all styles,
        | however if you want to do some logic based on style data
        | you can enable it by setting read_only to false.
        |
        */
        'read_only' => true,

        /*
        |--------------------------------------------------------------------------
        | Ignore Empty
        |--------------------------------------------------------------------------
        |
        | When dealing with imports, you might be interested in ignoring
        | rows that have null values or empty strings. By default rows
        | containing empty strings or empty values are not ignored but can be
        | ignored by enabling the setting ignore_empty to true.
        |
        */
        'ignore_empty' => false,

        /*
        |--------------------------------------------------------------------------
        | Heading Row Formatter
        |--------------------------------------------------------------------------
        |
        | Configure the heading row formatter.
        | Available options: none|slug|custom
        |
        */
        'heading_row' => [
            'formatter' => 'slug',
        ],

        /*
        |--------------------------------------------------------------------------
        | CSV Settings
        |--------------------------------------------------------------------------
        |
        | Configure e.g. delimiter, enclosure and line ending for CSV imports.
        |
        */
        'csv'         => [
            'delimiter'        => ',',
            'enclosure'        => '"',
            'escape_character' => '\\',
            'contiguous'       => false,
            'input_encoding'   => 'UTF-8',
        ],

        /*
        |--------------------------------------------------------------------------
        | Worksheet properties
        |--------------------------------------------------------------------------
        |
        | Configure e.g. default title, creator, subject,...
        |
        */
        'properties'  => [
            'creator'        => '',
            'lastModifiedBy' => '',
            'title'          => '',
            'description'    => '',
            'subject'        => '',
            'keywords'       => '',
            'category'       => '',
            'manager'        => '',
            'company'        => '',
        ],

    ],

    /*
    |--------------------------------------------------------------------------
    | Extension detector
    |--------------------------------------------------------------------------
    |
    | Configure here which writer/reader type should be used when the package
    | needs to guess the correct type based on the extension alone.
    |
    */
    'extension_detector' => [
        'xlsx'     => Excel::XLSX,
        'xlsm'     => Excel::XLSX,
        'xltx'     => Excel::XLSX,
        'xltm'     => Excel::XLSX,
        'xls'      => Excel::XLS,
        'xlt'      => Excel::XLS,
        'ods'      => Excel::ODS,
        'ots'      => Excel::ODS,
        'slk'      => Excel::SLK,
        'xml'      => Excel::XML,
        'gnumeric' => Excel::GNUMERIC,
        'htm'      => Excel::HTML,
        'html'     => Excel::HTML,
        'csv'      => Excel::CSV,
        'tsv'      => Excel::TSV,

        /*
        |--------------------------------------------------------------------------
        | PDF Extension
        |--------------------------------------------------------------------------
        |
        | Configure here which Pdf driver should be used by default.
        | Available options: Excel::MPDF | Excel::TCPDF | Excel::DOMPDF
        |
        */
        'pdf'      => Excel::DOMPDF,
    ],

    /*
    |--------------------------------------------------------------------------
    | Value Binder
    |--------------------------------------------------------------------------
    |
    | PhpSpreadsheet offers a way to hook into the process of a value being
    | written to a cell. In there some assumptions are made on how the
    | value should be formatted. If you want to change those defaults,
    | you can implement your own default value binder.
    |
    | Possible value binders:
    |
    | [x] Maatwebsite\Excel\DefaultValueBinder::class
    | [x] PhpOffice\PhpSpreadsheet\Cell\StringValueBinder::class
    | [x] PhpOffice\PhpSpreadsheet\Cell\AdvancedValueBinder::class
    |
    */
    'value_binder' => [
        'default' => Maatwebsite\Excel\DefaultValueBinder::class,
    ],

    'cache' => [
        /*
        |--------------------------------------------------------------------------
        | Default cell caching driver
        |--------------------------------------------------------------------------
        |
        | By default PhpSpreadsheet keeps all cell values in memory, however when
        | dealing with large files, this might result into memory issues. If you
        | want to mitigate that, you can configure a cell caching driver here.
        | When using the illuminate driver, it will store each value in a the
        | cache store. This can slow down the process, because it needs to
        | store each value. You can use the "batch" store if you want to
        | only persist to the store when the memory limit is reached.
        |
        | Drivers: memory|illuminate|batch
        |
        */
        'driver'     => 'illuminate',

        /*
        |--------------------------------------------------------------------------
        | Batch memory caching
        |--------------------------------------------------------------------------
        |
        | When dealing with the "batch" caching driver, it will only
        | persist to the store when the memory limit is reached.
        | Here you can tweak the memory limit to your liking.
        |
        */
        'batch'     => [
            'memory_limit' => 60000,
        ],

        /*
        |--------------------------------------------------------------------------
        | Illuminate cache
        |--------------------------------------------------------------------------
        |
        | When using the "illuminate" caching driver, it will automatically use
        | your default cache store. However if you prefer to have the cell
        | cache on a separate store, you can configure the store name here.
        | You can use any store defined in your cache config. When leaving
        | at "null" it will use the default store.
        |
        */
        'illuminate' => [
            'store' => null,
        ],
    ],

    /*
    |--------------------------------------------------------------------------
    | Transaction Handler
    |--------------------------------------------------------------------------
    |
    | By default the import is wrapped in a transaction. This is useful
    | for when an import may fail and you want to retry it. With the
    | transactions, the previous import gets rolled-back.
    |
    | You can disable the transaction handler by setting this to null.
    | Or you can choose a custom made transaction handler here.
    |
    | Supported handlers: null|db
    |
    */
    'transactions' => [
        'handler' => 'db',
    ],

    'temporary_files' => [

        /*
        |--------------------------------------------------------------------------
        | Local Temporary Path
        |--------------------------------------------------------------------------
        |
        | When exporting and importing files, we use a temporary file, before
        | storing reading or downloading. Here you can customize that path.
        |
        */
        'local_path'          => storage_path('framework/laravel-excel'),

        /*
        |--------------------------------------------------------------------------
        | Remote Temporary Disk
        |--------------------------------------------------------------------------
        |
        | When dealing with a multi server setup with queues in which you
        | cannot rely on having a shared local temporary path, you might
        | want to store the temporary file on a shared disk. During the
        | queue executing, we'll retrieve the temporary file from that
        | location instead. When left to null, it will always use
        | the local path. This setting only has effect when using
        | in conjunction with queued imports and exports.
        |
        */
        'remote_disk'         => null,
        'remote_prefix'       => null,

        /*
        |--------------------------------------------------------------------------
        | Force Resync
        |--------------------------------------------------------------------------
        |
        | When dealing with a multi server setup as above, it's possible
        | for the clean up that occurs after entire queue has been run to only
        | cleanup the server that the last AfterImportJob runs on. The rest of the server
        | would still have the local temporary file stored on it. In this case your
        | local storage limits can be exceeded and future imports won't be processed.
        | To mitigate this you can set this config value to be true, so that after every
        | queued chunk is processed the local temporary file is deleted on the server that
        | processed it.
        |
        */
        'force_resync_remote' => null,
    ],
];

Job文件

<?php

namespace App\Containers\Statistic\Jobs;

use App\Containers\Statistic\Exports\CallCenterOrdersFromQueryExport;
use App\Containers\Statistic\Models\Report;
use App\Ship\Parents\Jobs\Job;
use Illuminate\Support\Facades\Log;

/**
 * Class CallCenterOrdersJob
 */
class CallCenterOrdersJob extends Job
{
    private $recipients;

    public function __construct(array $recipients)
    {
        $this->recipients = $recipients;
    }

    public function handle()
    {
        Log::info('开始(电话中心)订单明细统计报表任务');

        $report = $this->recipients['report'];

        try {
            // 上传数据到文件
            $path = 'uploads/media/reports/'.date('Ym/d', time()).'/'.$report->conditions['date_start'].'-'.$report->conditions['date_end'].'-订单明细统计报表(电话中心)'.date('His').'.xls';

            Log::info($path);

            (new CallCenterOrdersFromQueryExport($report))->store($path, 's3', NULL, [
                'visibility' => 'private',
            ])->chain([
                New UpdateReportAfterExportCompletedJob(['report' => $report, 'path' => $path]),
            ]);

        } catch (\Throwable $th) {
            // 更新报表任务状态
            $report->update([
                'status'    => Report::STATUS_FAILED,
            ]);
            Log::info('失败啦?');
            Log::info($th);
            throw $th;
        }

    }
}

export 文件

<?php

namespace App\Containers\Statistic\Exports;

use App\Containers\Order\Models\Order;
use App\Containers\Statistic\Models\Report;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Log;
use Maatwebsite\Excel\Concerns\Exportable;
use Maatwebsite\Excel\Concerns\FromQuery;
use Maatwebsite\Excel\Concerns\WithColumnWidths;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\WithMapping;
use Maatwebsite\Excel\Concerns\WithStrictNullComparison;
use Throwable;

class CallCenterOrdersFromQueryExport implements FromQuery, ShouldQueue, WithColumnWidths, WithHeadings, WithStrictNullComparison, WithMapping
{
    use Exportable;

    protected $report;

    public function __construct(object $report)
    {
        Log::info('report', $report->toArray());
        $this->report = $report;
    }

    public function headings(): array
    {
        return [
            '订购日期',
            '订单号',
            '订单状态',
            '货号',
            '货品名',
            '颜色',
            '型号',
            '货品积分',
            '售价',
            '订单数量',
            '订单金额',
            '付款方式',
            '会员编号',
            '会员姓名',
            '组别(角色)',
            '下单客服',
            '下单部门',
        ];
    }

    public function columnWidths(): array
    {
        return [
            'A' => 20,
            'B' => 20,
            'C' => 10,
            'D' => 10,
            'E' => 25,
            'F' => 10,
            'G' => 10,
            'H' => 10,
            'I' => 10,
            'J' => 10,
            'K' => 10,
            'L' => 10,
            'M' => 15,
            'N' => 10,
            'O' => 20,
            'P' => 10,
            'Q' => 10,
        ];
    }

    public function query()
    {
        // 时间段
        $whereBetween = [$this->report['conditions']['date_start'], $this->report['conditions']['date_end']];

        // 订单搜索条件
        $where = [];

        if (isset($this->report['conditions']['channel_id'])) {
            $where[] = ['channel_id', $this->report['conditions']['channel_id']];
        }

        if (isset($this->report['conditions']['status'])) {
            $where[] = ['status', $this->report['conditions']['status']];
        }

        // 下单客服条件
        $createrWhere = [
            ['group', 'customerservice']
        ];
        if (isset($this->report['conditions']['department_group'])) {
            $createrWhere[] = ['department_group', $this->report['conditions']['department_group']];
        }

        // 订单商品条件
        $productWhere = [];

        if (isset($this->report['conditions']['product_name'])) {
            $productWhere[] = ['name', $this->report['conditions']['product_name']];
        }

        if (isset($this->report['conditions']['product_sn'])) {
            $productWhere[] = ['sn', $this->report['conditions']['product_sn']];
        }

        // 查询订单
        $query = Order::query()
            ->with([
                'creater',
                'member',
                'items',
            ])
            ->whereBetween('created_at', $whereBetween)
            ->where($where)
            // 按客服条件搜索
            ->whereHas('creater', function (Builder $query) use ($createrWhere)
            {
                $query->where($createrWhere);

                // 按角色
                if (isset($this->report['conditions']['role_id'])) {
                    $query->whereHas('roles', function (Builder $query)
                    {
                        $query->where('id', $this->report['conditions']['role_id']);
                    });
                }
            });

        // 订单商品条件搜索
        if (!empty($productWhere)) {
            $query->whereHas('items', function (Builder $query) use ($productWhere)
            {
                $query->where($productWhere);
            });
        }

        return $query;
    }

    /**
    * @var Service $order
    */
    public function map($order): array
    {
        return [
            $order->created_at,
            $order->sn,
            $order->status_str,
            isset($order->items[0]) ? $order->items[0]->sn : '',
            isset($order->items[0]) ? $order->items[0]->name : '',
            isset($order->items[0]) ? $order->items[0]->product_color : '',
            isset($order->items[0]) ? $order->items[0]->product_type : '',
            $order->bonus_point,
            isset($order->items[0]) ? $order->items[0]->sell_price_taxed : 0,
            isset($order->items[0]) ? $order->items->sum('num') : 0,
            $order->total_amount,
            $order->is_online_pay ? '在线支付' : '线下支付',
            $order->member ? $order->member->sn : '',
            $order->member_name,
            (isset($order->member) && !empty($order->member->roles)) ? $order->member->roles[0]->display_name : '',
            $order->creater_name,
            $order->creater_department,
        ];
    }

    public function failed(Throwable $exception): void
    {
        Log::info('报错啦:'.$exception->getMessage());
        $this->report->update([
            'status'    => Report::STATUS_FAILED,
        ]);
    }
}

任务完成后处理文件

<?php

namespace App\Containers\Statistic\Jobs;

use App\Containers\Statistic\Models\Report;
use App\Ship\Parents\Jobs\Job;
use Illuminate\Support\Facades\Log;

/**
 * Class UpdateReportAfterExportCompletedJob
 */
class UpdateReportAfterExportCompletedJob extends Job
{
    private $recipients;

    public function __construct(array $recipients)
    {
        $this->recipients = $recipients;
    }

    public function handle()
    {
        Log::info('报表:'.$this->recipients['report']->id.'生成成功啦,在这里更新下文件路径');

        // 更新报表任务状态
        $this->recipients['report']->update([
            'file_path' => $this->recipients['path'],
            'status'    => Report::STATUS_COMPLETED,
        ]);
    }
}

日志

[2021-04-04 20:29:20] production.INFO: 开始(电话中心)订单明细统计报表任务
[2021-04-04 20:29:20] production.INFO: uploads/media/reports/202104/04/2021-03-25-2021-03-31 23:59:59-订单明细统计报表(电话中心)202920.xls
[2021-04-04 20:29:20] production.INFO: report {**不重要的信息已略去**}}

[2021-04-04 20:52:51] production.INFO: 报错啦:Maatwebsite\Excel\Jobs\AppendQueryToSheet has been attempted too many times or run too long. The job may have previously timed out.

大佬们给把把脉,看问题处在哪里。先谢谢啦 :grin:

黑哈尔
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 13

因为你查询数据的时间超过了队列的执行时间

1:延长队列的执行时间

2:优化数据库查询 SQL,400w数据,千万不要用whereHas,改造下查询语句,试试用whereIn代替,以尽量减少查询时间

2年前 评论
黑哈尔 (楼主) 2年前
黑哈尔

@LiamHao 我设置的 retry_after=600 ,在本地导个简单的表,4万条记录,查3000条数据,也会超时,感觉效率非常低。

    public function query()
    {
        return Member::query()->where('created_at', '>', '2006-06-01');
    }

file

线上的查询优化成下边这种了,还是超时:

    public function query()
    {
        // 时间段
        $whereBetween = [$this->report['conditions']['date_start'], $this->report['conditions']['date_end']];

        // 订单搜索条件
        $where = [];

        if (isset($this->report['conditions']['channel_id'])) {
            $where[] = ['channel_id', $this->report['conditions']['channel_id']];
        }

        if (isset($this->report['conditions']['status'])) {
            $where[] = ['status', $this->report['conditions']['status']];
        }

        // 下单客服条件
        $createrWhere = [
            ['group', 'customerservice']
        ];
        if (isset($this->report['conditions']['department_group'])) {
            $createrWhere[] = ['department_group', $this->report['conditions']['department_group']];
        }

        // 订单商品条件
        $productWhere = [];

        if (isset($this->report['conditions']['product_name'])) {
            $productWhere[] = ['name', $this->report['conditions']['product_name']];
        }

        if (isset($this->report['conditions']['product_sn'])) {
            $productWhere[] = ['sn', $this->report['conditions']['product_sn']];
        }

        // 查询订单
        $query = Order::query()
            ->with([
                'creater',
                'member',
                'items',
            ])
            ->whereBetween('created_at', $whereBetween)
            ->where($where)
            // 按客服条件搜索
            ->WhereIn(
                'creater_id',
                User::query()
                    ->select('id')
                    ->where($createrWhere)
                    ->whereHas('roles', function (Builder $query)
                    {
                        $query->where('id', $this->report['conditions']['role_id']);
                    })
                    ->pluck('id')
            );

        // 订单商品条件搜索
        if (!empty($productWhere)) {
            $query->WhereIn(
                'order_id',
                OrderItem::query()
                    ->select('order_id')
                    ->where($productWhere)
                    ->pluck('order_id')
            );
        }

        return $query;
    }
2年前 评论
LiamHao 2年前
黑哈尔 (作者) (楼主) 2年前
黑哈尔 (作者) (楼主) 2年前
LiamHao 2年前
skys215 2年前
黑哈尔 (作者) (楼主) 2年前

chunk_size 可以调到5000 或者1w

2年前 评论
黑哈尔 (楼主) 2年前

LazyCollection

2年前 评论
黑哈尔 (楼主) 2年前
crackfan (作者) 2年前
黑哈尔 (楼主) 2年前

用php-ext-xlswriter,github:github.com/viest/php-ext-xlswriter ,原来使用laravel-excel 导出xls效率太低还会出现导出数据不完整,后面换用了php-ext-xlswriter 就行了。

2年前 评论
黑哈尔 (楼主) 2年前

导出大量的数据,尽量用原生sql,它要比laravel封装的快。

2年前 评论
黑哈尔 (楼主) 2年前

sql 太慢了。 explain 看看

2年前 评论
黑哈尔 (楼主) 2年前

如果你确定是laravel-excel 效率问题,你可以试试这个库 rap2hpoutre/fast-excel

2年前 评论
黑哈尔 (楼主) 2年前
  1. 最终方案建议直接换:github.com/viest/php-ext-xlswriter
  2. 考虑部署问题,折中方案:github.com/mk-j/PHP_XLSXWriter

当然 csv 格式也是一种方案,缺点就是单元格样式没法设置,例如:较长的数据字符串(身份证号码)会自动转成科学计数法

2年前 评论

多打印运行耗时看看是哪里最花时间。

2年前 评论

再买个服务器专门来做导出execl

2年前 评论

chunkById, 再加上手写文件csv算了 你试试, 每次chunk 小于500条

<?php
    class csv {
        public function fillCsvHeader(FilesystemAdapter $storage, string $fileName, array $headers): void
        {
            // 如果存在就要删除
            if ($storage->exists($fileName)) {
                $storage->delete($fileName);
            }

            $data = [];

            foreach ($headers as $value) {
                $data[] = $this->ornamentString($value);
            }

            $storage->append($fileName, implode(',', $data));
        }

        public function fillCsvBody(FilesystemAdapter $storage, string $fileName, array $bodys): void
        {
            $string = '';

            end($bodys);
            $lastKey = key($bodys);

            foreach ($bodys as $key => $body) {
                if ($key === $lastKey) {
                    $string .= implode(',', $body);
                    break;
                }
                $string .= implode(',', $body) . PHP_EOL;
            }

            $storage->append($fileName, $string);
        }

        public function ornamentString(?string $string): string
        {
            if (is_null($string)) {
                $string = 'null';
            }

            return '"' . $string . '"';
        }

        public function test()
        {
            $headers = ['ID', 'assetID'];

            $storage = Storage::disk('public');

            $fileName = date('Y-m-d')."json.csv";

            $this->fillCsvHeader($storage, $fileName, $headers);


            JsonProgram::query()->chunkById(
                '200',
                function ($jsonPrograms) use ($storage, $fileName) {

                    $jsonPrograms->load('episodes');
                    $bodys = [];
                    foreach ($jsonPrograms as $jsonProgram) {
                        $bodys[] = [
                            $this->ornamentString($jsonProgram->id),
                            $this->ornamentString($jsonProgram->assetID),
                        ];
                    }

                    if (! empty($bodys)) {
                        $this->fillCsvBody($storage, $fileName, $bodys);
                    }
            });

            $get = $storage->get($fileName);

            // $storage->delete($fileName);

            return $get;
        }

    }
2年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!