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

时间:2009-12-10 18:35:48

标签: php mysql

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

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

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

13 个答案:

答案 0 :(得分:50)

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

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;

答案 1 :(得分:6)

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

答案 2 :(得分:3)

当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,则按位置映射)。

答案 3 :(得分:1)

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

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

答案 4 :(得分:1)

导出

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

~: 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

答案 5 :(得分:0)

您可以使用LOAD DATA INFILE?

吗?

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

答案 6 :(得分:0)

答案 7 :(得分:0)

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

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);
}

答案 8 :(得分:0)

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

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

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

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

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

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

答案 9 :(得分:0)

首先,感谢这个话题。这为我节省了很多时间:) 让我为你的代码做一点修复。 有时如果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;
}

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

答案 10 :(得分:0)

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

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

答案 11 :(得分:-1)

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

答案 12 :(得分:-3)

你怎么看?

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