最佳实践:在PHP中导入mySQL文件;拆分查询
我有一个情况,我必须更新一个共享托管提供商的网站。该网站有一个CMS。使用FTP上传CMS文件非常简单 我还必须导入一个大的(相对于PHP脚本的限制)数据库文件(大约2-3MB未压缩)。Mysql已关闭,无法从外部访问,因此我必须使用FTP上传一个文件,并启动一个PHP脚本将其导入。遗憾的是,我无法访问最佳实践:在PHP中导入mySQL文件;拆分查询,php,mysql,Php,Mysql,我有一个情况,我必须更新一个共享托管提供商的网站。该网站有一个CMS。使用FTP上传CMS文件非常简单 我还必须导入一个大的(相对于PHP脚本的限制)数据库文件(大约2-3MB未压缩)。Mysql已关闭,无法从外部访问,因此我必须使用FTP上传一个文件,并启动一个PHP脚本将其导入。遗憾的是,我无法访问mysql命令行函数,因此我必须使用本机PHP解析和查询它。我也不能使用加载数据填充。我也不能使用像phpMyAdmin这样的任何交互前端,它需要以自动化的方式运行。我也不能使用mysqli\u
mysql
命令行函数,因此我必须使用本机PHP解析和查询它。我也不能使用加载数据填充。我也不能使用像phpMyAdmin这样的任何交互前端,它需要以自动化的方式运行。我也不能使用mysqli\u multi\u query()
是否有人知道或拥有一个已编码的简单解决方案,该解决方案能够可靠地将此类文件拆分为单个查询(可能有多行语句)并运行查询。由于我可能会遇到许多问题(如何检测字段分隔符是否是数据的一部分;如何处理备注字段中的换行符;等等),我希望避免自己动手处理它。必须有现成的解决方案。您不能安装phpMyAdmin,gzip文件(这会使文件更小)并使用phpMyAdmin导入吗 编辑:如果不能使用phpMyAdmin,可以使用phpMyAdmin中的代码。我不确定这个特定的部分,但它的结构一般都很好。你能用吗 如果您使用SELECT INTO OUTFILE格式化db转储文件,这正是您所需要的。没有理由让PHP解析任何内容。已回答: 此外:
- 您认为:
system("cat xxx.sql | mysql -l username database");
这是一个内存友好的函数,它应该能够在单个查询中拆分一个大文件,而无需立即打开整个文件:
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;
}
和相应的输出:
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;
当StackOverflow以XML格式发布他们每月的数据转储时,我编写了PHP脚本将其加载到MySQL数据库中。我在几分钟内导入了大约2.2GB的XML 我的技巧是使用参数占位符插入列值的
语句。然后使用循环遍历XML元素和我准备的查询,插入参数的值。我选择XMLReader是因为它是一个流式XML阅读器;它以增量方式读取XML输入,而不需要将整个文件加载到内存中
您还可以使用一次读取一行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编写这段代码并不是什么了不起的事,当使用准备好的语句和显式事务时,它会运行得非常快。这些功能在过时的PHP扩展中不可用,但如果使用或,则可以使用它们
我还添加了一些方便的东西,如错误检查、进度报告,以及在数据文件不包含其中一个字段时支持默认值
我在一个abstract
PHP类中编写了代码,我为需要加载的每个表创建了子类。每个子类声明要加载的列,并按名称(如果数据文件是CSV,则按位置)将它们映射到XML数据文件中的字段。单页PHPMyAdmin-Adminer-仅一个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导入文件。即使它很大,也只需使用配置目录,将其上传到那个里,然后从phpMyAdmin导入页面中选择它。一旦文件处理接近PHP限制,phpMyAdmin将中断导入,再次显示带有预定义值的导入页面,指示继续导入的位置。Export
第一步是以合理的格式获取输入,以便在导出时进行解析。从你的问题
您似乎可以控制此数据的导出,但不能控制导入
~: mysqldump test --opt --skip-extended-insert | grep -v '^--' | grep . > test.sql
这会将测试数据库(不包括所有注释行和空行)转储到test.sql中。它还禁用
扩展INSERT,意味着每行有一条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;
这说明,您在不到3分钟的时间内处理了一个15MB的mysqldump,其峰值RAM使用率为1.75MB
替代出口
如果内存限制足够高,但速度太慢,可以使用以下导出尝试此操作:
~: 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,但所需时间约为RAM的1/6。我建议尝试这两种方法,看看哪种适合你的需要
编辑:
使用CHAR、VARCHAR、BINARY、VARBINARY和BLOB字段类型,我无法在任何mysqldump输出中显示换行符。如果您确实有BLOB/二进制字段,请使用以下内容以防万一:
~: mysqldump5 test --hex-blob --opt | grep -v '^--' | grep . > test.sql
如果不进行分析,则无法可靠地拆分查询。下面是无法用正则表达式正确拆分的有效SQL
SELECT ";"; SELECT ";\"; a;";
SELECT ";
abc";
我用PHP编写了一个小的SqlFormatter类,其中包括一个查询标记器。我添加了一个splitQuery方法来可靠地拆分所有查询(包括上面的示例)
如果不需要,可以删除格式并突出显示方法
一个缺点是它需要整个sql字符串都在内存中,如果您使用的是大型sql文件,这可能会成为一个问题。我是苏
SELECT ";"; SELECT ";\"; a;";
SELECT ";
abc";
DELIMITER //
CREATE TRIGGER `mytrigger` BEFORE INSERT ON `mytable`
FOR EACH ROW BEGIN
SET NEW.`create_time` = NOW();
END
//
DELIMITER ;
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;
}