繁体   English   中英

最佳实践:用PHP导入mySQL文件; 拆分查询

[英]Best practice: Import mySQL file in PHP; split queries

我有一种情况,我必须更新共享主机提供商的网站。 该网站有一个CMS。 使用FTP上传CMS的文件非常简单。

我还必须导入一个大的(相对于PHP脚本的限制)数据库文件(大约2-3 MB未压缩)。 Mysql因外部访问而关闭,因此我必须使用FTP上传文件,并启动PHP脚本进行导入。 遗憾的是,我无法访问mysql命令行函数,所以我必须使用本机PHP解析和查询它。 我也不能使用LOAD DATA INFILE。 我也不能像phpMyAdmin一样使用任何一种交互式前端,它需要以自动化的方式运行。 我也不能使用mysqli_multi_query()

有没有人知道或者有一个已编码的简单解决方案,可以将这样的文件可靠地拆分成单个查询(可能有多行语句)并运行查询。 我想避免自己开始摆弄它,因为我可能遇到很多问题(如何检测字段分隔符是否是数据的一部分;如何处理备忘录字段中的换行符;等等上)。 必须有一个现成的解决方案。

这是一个内存友好的函数,应该能够在单个查询中拆分大文件,而无需一次打开整个文件

function SplitSQL($file, $delimiter = ';')
{
    set_time_limit(0);

    if (is_file($file) === true)
    {
        $file = fopen($file, 'r');

        if (is_resource($file) === true)
        {
            $query = array();

            while (feof($file) === false)
            {
                $query[] = fgets($file);

                if (preg_match('~' . preg_quote($delimiter, '~') . '\s*$~iS', end($query)) === 1)
                {
                    $query = trim(implode('', $query));

                    if (mysql_query($query) === false)
                    {
                        echo '<h3>ERROR: ' . $query . '</h3>' . "\n";
                    }

                    else
                    {
                        echo '<h3>SUCCESS: ' . $query . '</h3>' . "\n";
                    }

                    while (ob_get_level() > 0)
                    {
                        ob_end_flush();
                    }

                    flush();
                }

                if (is_string($query) === true)
                {
                    $query = array();
                }
            }

            return fclose($file);
        }
    }

    return false;
}

我在一个大的phpMyAdmin SQL转储上测试它,它工作得很好。


一些测试数据:

CREATE TABLE IF NOT EXISTS "test" (
    "id" INTEGER PRIMARY KEY AUTOINCREMENT,
    "name" TEXT,
    "description" TEXT
);

BEGIN;
    INSERT INTO "test" ("name", "description")
    VALUES (";;;", "something for you mind; body; soul");
COMMIT;

UPDATE "test"
    SET "name" = "; "
    WHERE "id" = 1;

各自的输出:

SUCCESS: CREATE TABLE IF NOT EXISTS "test" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, "name" TEXT, "description" TEXT );
SUCCESS: BEGIN;
SUCCESS: INSERT INTO "test" ("name", "description") VALUES (";;;", "something for you mind; body; soul");
SUCCESS: COMMIT;
SUCCESS: UPDATE "test" SET "name" = "; " WHERE "id" = 1;

单页PHPMyAdmin - Adminer - 只需一个PHP脚本文件。 检查: http//www.adminer.org/en/

当StackOverflow以XML格式发布月度数据转储时,我编写了PHP脚本以将其加载到MySQL数据库中。 我在几分钟内导入了大约2.2千兆字节的XML。

我的技术是使用参数占位符为列值prepare()一个INSERT语句。 然后使用XMLReader循环遍历XML元素并execute()我准备好的查询,插入参数的值。 我选择了XMLReader,因为它是一个流式XML阅读器; 它以递增方式读取XML输入,而不是要求将整个文件加载到内存中。

您还可以使用fgetcsv()一次读取一行CSV文件。

如果你正在进入InnoDB表,我建议明确地启动和提交事务,以减少自动提交的开销。 我每1000行提交一次,但这是任意的。

我不打算在这里发布代码(因为StackOverflow的许可政策),但是在伪代码中:

connect to database
open data file
PREPARE parameterizes INSERT statement
begin first transaction
loop, reading lines from data file: {
    parse line into individual fields
    EXECUTE prepared query, passing data fields as parameters
    if ++counter % 1000 == 0,
        commit transaction and begin new transaction
}
commit final transaction

用PHP编写这段代码并不是火箭科学,当使用预处理语句和显式事务时,它运行得非常快。 过时的mysql PHP扩展中没有这些功能,但如果使用mysqliPDO_MySQL,则可以使用它们。

当数据文件不包含其中一个字段时,我还添加了诸如错误检查,进度报告和默认值支持等方便的功能。

我在一个abstract PHP类中编写了我的代码,我为每个需要加载的表创建子类。 每个子类声明它要加载的列,并按名称将它们映射到XML数据文件中的字段(如果数据文件是CSV,则按位置映射)。

你不能安装phpMyAdmin,gzip文件(应该使它小得多)并使用phpMyAdmin导入它?

编辑:好吧,如果你不能使用phpMyAdmin,你可以使用phpMyAdmin中的代码。 我不确定这个特殊的部分,但它的结构非常好。

出口

第一步是以合理的格式获取输入,以便在导出时进行解析。 从您的问题看来,您可以控制导出此数据,但不能导入。

~: mysqldump test --opt --skip-extended-insert | grep -v '^--' | grep . > test.sql

这会将测试数据库(不包括所有注释行和空行)转储到test.sql中。 它还禁用扩展插入,这意味着每行有一个INSERT语句。 这将有助于限制导入期间的内存使用量,但代价是导入速度。

进口

导入脚本就像这样简单:

<?php

$mysqli = new mysqli('localhost', 'hobodave', 'p4ssw3rd', 'test');
$handle = fopen('test.sql', 'rb');
if ($handle) {
    while (!feof($handle)) {
        // This assumes you don't have a row that is > 1MB (1000000)
        // which is unlikely given the size of your DB
        // Note that it has a DIRECT effect on your scripts memory
        // usage.
        $buffer = stream_get_line($handle, 1000000, ";\n");
        $mysqli->query($buffer);
    }
}
echo "Peak MB: ",memory_get_peak_usage(true)/1024/1024;

这将使用非常少量的内存,如下所示:

daves-macbookpro:~ hobodave$ du -hs test.sql 
 15M    test.sql
daves-macbookpro:~ hobodave$ time php import.php 
Peak MB: 1.75
real    2m55.619s
user    0m4.998s
sys 0m4.588s

这就是说你在不到3分钟的时间内处理了一个15MB的mysqldump,峰值RAM使用率为1.75 MB。

替代出口

如果你有足够高的memory_limit并且这太慢了,你可以使用以下导出来尝试:

~: mysqldump test --opt | grep -v '^--' | grep . > test.sql

这将允许扩展插入,在单个查询中插入多行。 以下是相同数据库的统计信息:

daves-macbookpro:~ hobodave$ du -hs test.sql 
 11M    test.sql
daves-macbookpro:~ hobodave$ time php import.php 
Peak MB: 3.75
real    0m23.878s
user    0m0.110s
sys 0m0.101s

请注意,它在3.75 MB时使用的RAM超过2倍,但大约需要1/6。 我建议尝试两种方法,看看哪种方法适合您的需求。

编辑:

我无法使用任何CHAR,VARCHAR,BINARY,VARBINARY和BLOB字段类型在任何mysqldump输出中使用换行符。 如果你确实有BLOB / BINARY字段,那么请使用以下内容以防万一:

~: mysqldump5 test --hex-blob --opt | grep -v '^--' | grep . > test.sql

如果不进行解析,则无法可靠地完成拆分查询。 这是有效的SQL,无法使用正则表达式正确分割。

SELECT ";"; SELECT ";\"; a;";
SELECT ";
    abc";

我在PHP中编写了一个包含查询标记生成器的小型SqlFormatter类。 我向它添加了一个splitQuery方法,可以可靠地拆分所有查询(包括上面的例子)。

https://github.com/jdorn/sql-formatter/blob/master/SqlFormatter.php

如果不需要,可以删除格式并突出显示方法。

一个缺点是它需要整个sql字符串在内存中,如果你正在使用庞大的sql文件,这可能是一个问题。 我确定稍微修改一下,你可以让getNextToken方法代替文件指针。

首先,感谢这个话题。 这为我节省了很多时间:)让我为你的代码做一点修复。 有时如果TRIGGERS或PROCEDURES在转储文件中,仅仅检查; 分隔符。 在这种情况下可能是sql代码中的DELIMITER [something],说该语句不会以; 但是[某事]。 例如xxx.sql中的一节:

    DELIMITER //
    CREATE TRIGGER `mytrigger` BEFORE INSERT ON `mytable`
    FOR EACH ROW BEGIN
         SET NEW.`create_time` = NOW();
    END
    //
    DELIMITER ;

所以首先需要有一个falg来检测,该查询不会以; 并删除unqanted查询块,因为mysql_query不需要分隔符(分隔符是字符串的结尾)所以mysql_query需要这样的东西:

    CREATE TRIGGER `mytrigger` BEFORE INSERT ON `mytable`
    FOR EACH ROW BEGIN
         SET NEW.`create_time` = NOW();
    END;

所以有点工作,这里是固定代码:

    function SplitSQL($file, $delimiter = ';')
    {
        set_time_limit(0);            
        $matches = array();
        $otherDelimiter = false;
        if (is_file($file) === true) {
            $file = fopen($file, 'r');
            if (is_resource($file) === true) {
                $query = array();
                while (feof($file) === false) {
                    $query[] = fgets($file);
                    if (preg_match('~' . preg_quote('delimiter', '~') . '\s*([^\s]+)$~iS', end($query), $matches) === 1){     
                        //DELIMITER DIRECTIVE DETECTED
                        array_pop($query); //WE DON'T NEED THIS LINE IN SQL QUERY
                        if( $otherDelimiter = ( $matches[1] != $delimiter )){
                        }else{
                            //THIS IS THE DEFAULT DELIMITER, DELETE THE LINE BEFORE THE LAST (THAT SHOULD BE THE NOT DEFAULT DELIMITER) AND WE SHOULD CLOSE THE STATEMENT                                
                            array_pop($query);
                            $query[]=$delimiter;
                        }                                                                                    
                    }                        
                    if ( !$otherDelimiter && preg_match('~' . preg_quote($delimiter, '~') . '\s*$~iS', end($query)) === 1) {                            
                        $query = trim(implode('', $query));
                        if (mysql_query($query) === false){
                            echo '<h3>ERROR: ' . $query . '</h3>' . "\n";
                        }else{
                            echo '<h3>SUCCESS: ' . $query . '</h3>' . "\n";
                        }
                        while (ob_get_level() > 0){
                            ob_end_flush();
                        }
                        flush();                        
                    }
                    if (is_string($query) === true) {
                        $query = array();
                    }
                }                    
                return fclose($file);
            }
        }
        return false;
}

我希望我也可以帮助别人。 祝你今天愉快!

http://www.ozerov.de/bigdump/对我导入200多MB的sql文件非常有用。

注意:SQL文件应该已经存在于服务器中,以便可以毫无问题地完成该过程

你能使用LOAD DATA INFILE吗?

如果使用SELECT INTO OUTFILE格式化db转储文件,那么这应该就是您所需要的。 没有理由让PHP解析任何东西。

我遇到了同样的问题。 我用正则表达式解决了它:

function splitQueryText($query) {
    // the regex needs a trailing semicolon
    $query = trim($query);

    if (substr($query, -1) != ";")
        $query .= ";";

    // i spent 3 days figuring out this line
    preg_match_all("/(?>[^;']|(''|(?>'([^']|\\')*[^\\\]')))+;/ixU", $query, $matches, PREG_SET_ORDER);

    $querySplit = "";

    foreach ($matches as $match) {
        // get rid of the trailing semicolon
        $querySplit[] = substr($match[0], 0, -1);
    }

    return $querySplit;
}

$queryList = splitQueryText($inputText);

foreach ($queryList as $query) {
    $result = mysql_query($query);
}

您可以使用phpMyAdmin导入文件。 即使它很大,只需使用UploadDir配置目录,将其上传到那里并从phpMyAdmin导入页面中选择它。 一旦文件处理接近PHP限制,phpMyAdmin中断导入,再次显示导入页面,其中包含预定义的值,指示导入的继续位置。

你有什么想法:

system("cat xxx.sql | mysql -l username database"); 

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM