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

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

我还必须导入一个大的(相对于PHP脚本的限制)数据库文件(大约2-3 MB未压缩)。 Mysql从外部访问是closures的,所以我必须使用FTP上传文件,并启动一个PHP脚本来导入它。 可悲的是,我没有访问mysql命令行函数,所以我必须使用本机PHPparsing和查询它。 我也不能使用LOAD DATA INFILE。 我也不能使用像phpMyAdmin这样的交互式前端,它需要以自动化的方式运行。 我也不能使用mysqli_multi_query()

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

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

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

我testing了一个大的phpMyAdmin SQL转储,它工作得很好。


一些testing数据:

 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()我准备好的查询,插入参数的值。 我select了XMLReader,因为它是一个stream式XML阅读器。 它逐渐读取XMLinput,而不是要求将整个文件加载到内存中。

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

如果您正在创buildInnoDB表,我build议您明确启动并提交事务,以减less自动提交的开销。 我承诺每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编写这个代码不是火箭科学,当使用预处理语句和显式事务时,它运行的很快。 这些function在过时的mysql PHP扩展中不可用,但是如果使用mysqli或PDO_MySQL,则可以使用它们。

当数据文件不包含某个字段时,我还添加了错误检查,进度报告以及对缺省值的支持等方便的function。

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

出口

第一步是在导出时input一个合理的格式进行parsing。 从你的问题看来,你有控制这个数据的导出,但不是导入。

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

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

import

导入脚本就像这样简单:

 <?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的内存使用率为1.75MB的mysqldump。

备用导出

如果你有足够高的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使用超过2倍的RAM,但大约需要1/6。 我build议尝试这两种方法,看看是否适合您的需求。

编辑:

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

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

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

编辑:那么,如果你不能使用phpMyAdmin,你可以使用从phpMyAdmin的代码。 我不确定这个特定的部分,但它是一个很好的结构。

拆分查询不能可靠地完成parsing。 这是有效的SQL,不可能用正则expression式正确分割。

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

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

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

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

一个缺点是,它需要整个SQLstring在内存中,这可能是一个问题,如果你正在处理巨大的SQL文件。 我肯定有一点修补,你可以使getNextToken方法在文件指针上工作。

首先感谢这个话题。 这为我节省了很多时间:)让我为你的代码做一点点修复。 有时如果TRIGGERS或PROCEDURES在转储文件中,检查是不够的; 分隔符。 在这种情况下,可能会在SQL代码中使用DELIMITER [东西],要说声明不会以; 但[某事]。 例如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不需要分隔符(分隔符是string的结尾),所以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格式化你的数据库转储文件,这应该正是你所需要的。 没有理由让PHPparsing任何东西。

我遇到了同样的问题。 我用一个正则expression式来解决它:

 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来导入文件。 即使它很大,只需使用UploadDirconfiguration目录,将其上传到phpMyAdmin导入页面即可。 一旦文件处理将接近PHP的限制,phpMyAdmin中断导入,再次显示导入页面预定义的值,指出在哪里继续导入。

你有什么想法:

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