6

我有一个 mysql 表,其中每条记录可以有无限的自定义字段(EAV 模型,没关系),每个字段可以有无限的选项,每个选项可以有无限的值。
现在我正在尝试构建一个导出工具,它将导出所有这些自定义字段及其值,即:每个字段的名称 => 值对。这不是重要的部分,这里只是为了强调我们正在讨论针对单个记录的大量 mysql 查询,并且导出的大小将非常大。

对于我的主表中的每一行,我必须执行大约 100 个单独的 sql 查询来获取字段、字段选项和字段选项值。这些查询非常快,因为它们都使用了正确的索引,但我们仍然在谈论针对单个记录的 100 个查询,我希望我的主表中只有大约 50k 条记录才能开始。

现在,我要做的是:

set_time_limit(0);
ini_set('memory_limit', '1G');
ini_set("auto_detect_line_endings", true);

$count = $export->count();
$date = date('Y-m-d-H-i-s');
$fileName = CHtml::encode($export->name) .'-'. $date . '.csv';

$processAtOnce = 100;
$rounds = round($count / $processAtOnce);

header("Content-disposition: attachment; filename={$fileName}");
header("Content-Type: text/csv");

$headerSet = false;
for ($i = 0; $i < $rounds; ++$i) {

    $limit = $processAtOnce;
    $offset = $i * $processAtOnce;
    $rows = $export->find($limit, $offset);

    if (empty($rows)) {
        continue;
    }

    $outStream = fopen('php://output', 'w');

    if (!$headerSet) {
        fputcsv($outStream, array_keys($rows[0]), ',', '"');    
        $headerSet = true;
    }

    foreach ($rows as $row) {
        fputcsv($outStream, array_values($row), ',', '"');
    }

    echo fgets($outStream);

    fclose($outStream);
}

基本上,我计算所有记录并为它们“分页”以进行导出,然后遍历页面以避免一次加载过多的 sql 结果。
我想知道这是否是一种有效的方法?有什么想法吗?

我的替代方法是计算所有记录,将它们分成“页面”,并为每个页面执行一个 ajax 请求(在成功发出前一个请求后调用递归函数)。在进行ajax请求时,一次处理可能有1k条记录(这1k条也将像上面的例子一样被拆分,例如内部运行10次,有100条结果),将它们写入临时目录(如part-1.csv, part-2.csv),最后在处理完所有记录后,从包含所有 csv 部分的文件夹中创建一个存档并强制浏览器下载它,然后将其从服务器中删除(window.location.href 从最后一个ajax调用)。
这是上述的一个很好的选择吗?

请注意,我的目标是限制内存使用量,这就是为什么我认为第二种方法对我有更多帮助的原因。

请让我知道你在想什么。
谢谢。

4

3 回答 3

7

我的最后一种方法是第二种方法,经过大量测试后,我得出结论,就我而言,第二种方法在内存使用方面要好得多,即使完成整个导出的时间更长,这并不重要,因为GUI 将使用有关导出的实时统计信息进行更新,并且在等待导出完成时总体上是一个良好的用户体验。

这些是我采取的步骤:
1)加载页面并向服务器发出第一个 ajax 请求。
2)服务器将一次读取前 1000 条记录,每批 100 条记录,以避免从 mysql 一次返回许多结果。
3)结果以part-x.csv的形式写入文件,其中x是ajax发送的请求号。
4) 当没有更多记录添加到文件中时,最后一次 ajax 调用将创建存档,并删除包含 part-x.csv 文件的文件夹。然后服务器将返回一个名为“download”的json参数,其中将包含通过PHP下载文件的url(fopen + fread + flush + fclose,然后取消链接存档文件)
5)使用“download”参数,浏览器将执行window.location.href = json.download并强制下载文件。

我知道,这样的工作更多,但正如我所说,最终结果似乎比像我第一次那样一次加载要好。

于 2013-09-29T21:37:00.017 回答
3

下面是导出大型 CSV 文件的更优化方法(感谢@Joe 提供上述代码) -

  1. 循环向服务器发出 Ajax 请求。下面将是 AJAX 调用过程。
  2. 服务器将一次读取一批记录( chunkSize )中的第一条记录,以避免从 MySQL 一次返回太多结果。
  3. 文件exported_file.csv 将在第一个请求中以写入模式打开,在后续请求中以附加模式打开。
  4. 结果将写入此文件。当没有更多记录添加到文件中时,js函数将发送文件下载。

下面是示例 JS 函数 -

<script>
var exportedRecords = 0;
var chunkSize = 500; // as per query performance

for( start=0; start <= totalRecords; start += chunkSize){
    chunkCSVExport(,0, chunkSize);
}

function chunkCSVExport(start,chunkSize){
        requestData['start']  = start;
        requestData['limit']  = chunkSize;
        jQuery.ajax({
            type : "post",
            dataType : "json",
            url :  action,
            data : formData,
            success: function(response) {
                console.log(response);
                exportedRecords += chunkSize;
                downloadfile();
            }
        });
    }

function downloadfile(){   
         if(exportedRecords>=totalRecords){
                     // call download file function here
                }
}

</script>

下面是示例 PHP 代码 -

<?php
$start = $_POST['start']; //added the missing closing single quote 
$limit = $_POST['limit'];


if($start==0) {
    $handle = fopen( 'file-export.csv', 'w' );
}else{
    $handle = fopen( 'file-export.csv', 'a' );
}


// Run The query from start to limit
$results = getresults($query)

if($start==0) {
    $headerDisplayed = false;
}else{
    $headerDisplayed = true;
}

foreach ( $results as $data ) {
    // Add a header row if it hasn't been added yet
    if ( !$headerDisplayed ) {
        // Use the keys from $data as the titles
        fputcsv($handle, $arrHeaders);
        $headerDisplayed = true;
    }
    // Put the data into the stream
    fputcsv($handle, $data);
}

// Close the file
fclose($handle);

// Output some stuff for jquery to use
$response = array(
    'result'        => 'success'
);
echo json_encode($response);
exit;
?>
于 2018-05-18T07:09:06.420 回答
2

感谢 Twisted1919 的帖子给了我一些启发。我知道这篇文章有点旧,但我想我会发布一些我的解决方案的代码,以防它帮助其他人。

它使用一些 Wordpress 函数进行数据库查询。

我将替换您的步骤 3 和 4。

<?php
// if its a fist run truncate the file. else append the file
if($start==0) {
    $handle = fopen( 'temp/prod-export'. '.csv', 'w' );
}else{
    $handle = fopen( 'temp/prod-export'. '.csv', 'a' );
}
?>

一些基本的 jQuery

<script>
    // do stuff on the form submit
    $('#export-form').submit(function(e){
        e.preventDefault();
        var formData = jQuery('#export-form').serializeObject();
        var chunkAndLimit = 1000;
        doChunkedExport(0,chunkAndLimit,formData,$(this).attr('action'),chunkAndLimit);
    });
    // function to trigger the ajax bit
    function doChunkedExport(start,limit,formData,action,chunkSize){
        formData['start']  = start;
        formData['limit']  = limit;
        jQuery.ajax({
            type : "post",
            dataType : "json",
            url :  action,
            data : formData,
            success: function(response) {
                console.log(response);
                if(response.result=='next'){
                    start = start + chunkSize;
                    doChunkedExport(start,limit,formData,action,chunkSize);
                }else{
                    console.log('DOWNLOAD');
                }
            }
        });
    }
    // A function to turn all form data into a jquery object
    jQuery.fn.serializeObject = function(){
        var o = {};
        var a = this.serializeArray();
        jQuery.each(a, function() {
            if (o[this.name] !== undefined) {
                if (!o[this.name].push) {
                    o[this.name] = [o[this.name]];
                }
                o[this.name].push(this.value || '');
            } else {
                o[this.name] = this.value || '';
            }
        });
        return o;
    };
</script>

php位

<?php
global $wpdb;

$postCols = array(
    'post_title',
    'post_content',
    'post_excerpt',
    'post_name',
);

header("Content-type: text/csv");

$start = intval($_POST['start']);
$limit = intval($_POST['limit']);

// check the total results to workout the finish point
$query = "SELECT count(ID) as total FROM `wp_posts` WHERE post_status = 'publish';";
$results = $wpdb->get_row( $query, ARRAY_A );
$totalResults = $results['total'];
$result = 'next';
if( ($start + $limit ) >= $totalResults){
    $result = 'finished';
}

// if its a fist run truncate the file. else append the file
if($start==0) {
    $handle = fopen( 'temp/prod-export'. '.csv', 'w' );
}else{
    $handle = fopen( 'temp/prod-export'. '.csv', 'a' );
}

$cols = implode(',',$postCols);
//The query
$query = "SELECT {$cols} FROM `wp_posts` WHERE post_status = 'publish' LIMIT {$start},{$limit};";
$results = $wpdb->get_results( $query, ARRAY_A );

if($start==0) {
    $headerDisplayed = false;
}else{
    $headerDisplayed = true;
}

foreach ( $results as $data ) {
    // Add a header row if it hasn't been added yet
    if ( !$headerDisplayed ) {
        // Use the keys from $data as the titles
        fputcsv($handle, array_keys($data));
        $headerDisplayed = true;
    }
    // Put the data into the stream
    fputcsv($handle, $data);
}

// Close the file
fclose($handle);

// Output some stuff for jquery to use
$response = array(
    'result'        => $result,
    'start'         => $start,
    'limit'         => $limit,
    'totalResults'  => $totalResults
);
echo json_encode($response);


// Make sure nothing else is sent, our file is done
exit;
?>
于 2017-02-22T13:11:05.127 回答