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

Best practice: Import mySQL file in PHP; split queries

提问人:Pekka 提问时间:12/11/2009 最后编辑:Robert HarveyPekka 更新时间:7/29/2014 访问量:22193

问:

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

我还必须导入一个大的(相对于PHP脚本的范围)数据库文件(未压缩约2-3 MB)。Mysql 已关闭,无法从外部访问,因此我必须使用 FTP 上传文件,并启动 PHP 脚本来导入它。可悲的是,我无法访问命令行函数,因此我必须使用本机 PHP 解析和查询它。我也不能使用 LOAD DATA INFILE。我也不能使用任何类型的交互式前端,如phpMyAdmin,它需要以自动化的方式运行。我也不能使用.mysqlmysqli_multi_query()

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

PHP 的MySQL

评论

2赞 Alix Axel 1/6/2010
另外,您能提供一些测试数据吗?
0赞 Pekka 1/8/2010
为大家的伟大投入而欢呼。我需要给自己找时间在赏金用完之前检查它们并测试它们。:)
0赞 Pekka 1/8/2010
现在提供测试数据很困难,但总的来说,它是各种表,有各种肮脏的东西(大量的换行符、HTML 代码、二进制数据等等)。
0赞 Pekka 1/8/2010
还有一件事,我已经看到有一些建议将数据放入原始mySQL查询以外的预解析格式。虽然这可能是有道理的,但我对这个方向非常犹豫,因为输出端已经使用 很好地设置了。目前,将转储文件拆分为单个查询的解决方案在我看来是最有希望的。mysqldump
0赞 Corey Ballou 1/9/2010
顺便说一句,2-3MB 的数据库文件绝不。大型数据库通常在 GB 甚至 TB 的范围内。

答:

1赞 Lukáš Lalinský 12/11/2009 #1

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

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

评论

0赞 Pekka 12/11/2009
不,我需要一个自动化的解决方案来每天运行。
1赞 Frunsi 1/7/2010
这里是phpMyAdmin的代码: phpmyadmin.svn.sourceforge.net/viewvc/phpmyadmin/trunk/... - 早期版本更容易阅读,但你可以在这里看到它是如何工作的。
0赞 mluebke 12/11/2009 #2

您可以使用 LOAD DATA INFILE 吗?

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

评论

0赞 Pekka 12/11/2009
我认为在这种情况下,我的mySQL用户关闭了LOAD DATA INFILE,但会检查。
0赞 Pekka 1/10/2010
我对此投赞成票以平衡它。答案在技术上是正确的,即使它对我没有帮助,我不明白为什么它应该被否决。
0赞 BYK 1/4/2010 #3

已经回答:从 PHP 中加载 .sql 文件 另外:

评论

0赞 Pekka 1/4/2010
感谢您指出重复项,但我在那里看不到适合我需求的解决方案。
0赞 BYK 1/4/2010
它建议查看phpMyAdmin的代码,这是非常有意义的。
0赞 Pekka 1/4/2010
我尝试过一次,但没有走得很远,因为代码非常复杂。如果这是唯一的方法,我会努力完成它,但必须在某个地方有某种独立的脚本。
0赞 BYK 1/4/2010
我添加了几个指向答案的链接。我建议阅读它们。
-3赞 opHASnoNAME 1/4/2010 #4

你怎么看:

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

评论

0赞 Pekka 1/4/2010
不能那样做 - 正如我在问题中所写,我无法访问命令行。(不过,反对票不是我的)。
0赞 Arno 1/4/2010
我忘了发表评论:这是一个共享主机,你不能使用“系统”功能和很多“不知何故危险”的功能。
52赞 Alix Axel 1/6/2010 #5

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

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;

评论

0赞 Alix Axel 1/11/2010
没问题,Pekka,很高兴我能帮上忙。
3赞 lulalala 10/7/2011
请问这句话是什么意思?if (preg_match('~' . preg_quote($delimiter, '~') . '\s*$~iS', end($query)) === 1)
2赞 Alix Axel 10/9/2011
@lulalala:意思是:“以不区分大小写的方式匹配(转义的)$delimiter字符,后跟行尾前任意数量的空格(0 到 ∞)(或换行符、制表符等)”。
1赞 genesis 10/26/2011
@AlixAxel:你认为你可以把它重写为只支持单行查询吗?我正在寻找替代方法。
1赞 Jay Wick 4/12/2014
重要提示:这不考虑注释掉的行/块
3赞 Bill Karwin 1/7/2010 #6

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

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

您还可以使用 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则可以使用它们。

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

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

评论

0赞 Alix Axel 1/8/2010
这确实是一种很好的技术,但这并不能提供拆分单个查询的解决方案,这是 IMO 最困难的问题。
0赞 Bill Karwin 1/8/2010
我不认为解析SQL脚本是不切实际的,因为有太多的边缘情况。我建议仅将数据转储作为数据,使用 XML 或 CSV 或其他可以在 PHP 中轻松解析的格式。
0赞 Alix Axel 1/8/2010
我同意你的看法,比尔,但这似乎不是 Pekka 的解决方案(至少我从他的问题中是这样理解的)。
0赞 Bill Karwin 1/9/2010
我投了反对票。如果您对 StackOverflow 投反对票,请发表评论以解释原因。
1赞 Pekka 1/10/2010
谢谢比尔。当我修改这个问题时,导出阶段已经使用 ,所以虽然它通常是使用导出格式的更好方法,如您所述,但我在这个问题中的要求是导入实际的 SQL 查询。mysqldump
6赞 Zaje 1/8/2010 #7

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

评论

0赞 Pekka 1/10/2010
这不是我的自动化方案的解决方案,但很高兴知道。谢谢你的链接。+1
2赞 Ghostrider 6/11/2010
纨绔子弟!这些东西至高无上!!我希望我以前就知道它的存在,它会为我节省大量时间!
0赞 Ahmad Mushtaq 6/11/2017
不是这篇文章的解决方案,而是简单而优雅的工具,我肯定会在我未来的项目中使用它。
0赞 calvinlough 1/8/2010 #8

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

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);
}
-1赞 Michal Čihař 1/8/2010 #9

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

1赞 hobodave 1/9/2010 #10

出口

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

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

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

替代导出

如果您的memory_limit足够高,但速度太慢,则可以使用以下导出来尝试此操作:

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

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

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

请注意,它使用的 RAM 是 3.75 MB 的 2 倍以上,但花费的时间约为 1/6。我建议尝试这两种方法,看看哪种方法适合您的需求。

编辑:

我无法使用任何 CHAR、VARCHAR、BINARY、VARIN 和 BLOB 字段类型让换行符出现在任何 mysqldump 输出中。如果您确实有 BLOB/BINARY 字段,请使用以下字段以防万一:

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

评论

0赞 Pekka 1/10/2010
干杯霍博达夫。我首先尝试了您的解决方案,它基本上有效,但它从许多表中删除了许多记录。粗略检查时,这是因为这些记录包含实际的换行符。虽然这可能很容易解决,但赏金已经用完了,出于公平考虑,我不得不选择开箱即用的解决方案,在这种情况下是 Axel 的解决方案。感谢您抽出宝贵时间接受采访,如果您想更改答案以考虑换行内容,我很乐意为您测试运行它(我无法转储 SQL,因为它包含机密信息)。
0赞 hobodave 1/11/2010
@Pekka:哪种字段类型中有换行符?我尝试使用 TEXT 和 VARCHAR 列,我的转储如下所示:INSERT INTO newline VALUES (1,'Four score, \nand seven years\nago');
0赞 hobodave 1/11/2010
我也无法使用 BLOB 字段重现它。
0赞 Pekka 1/11/2010
这很奇怪。我看一下导入的数据,它停止的记录编号始终相同。
0赞 Jeremy Dorn 5/23/2012 #11

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

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

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

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

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

一个缺点是它要求整个 sql 字符串都在内存中,如果您正在处理巨大的 sql 文件,这可能是一个问题。我敢肯定,只要稍加修改,您就可以使getNextToken方法在文件指针上工作。

评论

1赞 random_user_name 10/21/2012
为什么要将此添加到具有高度评价、接受答案的问题中?
0赞 Kovge 1/22/2013 #12

首先,感谢您的这个话题。这为我节省了很多时间:) 让我对你的代码做一些小的修复。 有时,如果 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,来检测,查询不是以 ; 并删除未确定的查询块,因为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;
}

我希望我也能帮助别人。 有好的一天!

0赞 Frederic Anand 5/9/2013 #13

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

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