【问题标题】:Best practice: Import mySQL file in PHP; split queries最佳实践:在 PHP 中导入 mySQL 文件;拆分查询
【发布时间】:2010-12-25 09:08:31
【问题描述】:

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

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

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

【问题讨论】:

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

标签: php mysql


【解决方案1】:

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

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

【讨论】:

    【解决方案2】:

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

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

    【讨论】:

      【解决方案3】:

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

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

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

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

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

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

      【讨论】:

      • 为什么要将此添加到具有高度评价和接受答案的问题中?
      【解决方案4】:

      【讨论】:

      • 感谢您指出重复的内容,但我在那里找不到适合我需要的解决方案。
      • 它建议看看 phpMyAdmin 的代码,这很有意义。
      • 我已经尝试过一次,但没有走得太远,因为代码非常复杂。如果这是唯一的方法,我会努力完成它,但在某个地方必须有某种独立的脚本。
      • 我已经为答案添加了几个链接。我建议阅读它们。
      【解决方案5】:

      导出

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

      ~: 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 时使用了超过 2 倍的 RAM,但需要大约 1/6 的时间。我建议尝试这两种方法,看看哪种方法适合您的需求。

      编辑:

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

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

      【讨论】:

      • 干杯霍博达夫。我首先尝试了您的解决方案,它基本上可以工作,但是它从许多表中删除了许多记录。粗略检查,这是因为这些记录包含实际的换行符。虽然这可能很容易解决,但赏金已经用完了,我觉得不得不选择开箱即用的对我有用的解决方案,在这种情况下是 Axel 的。感谢您抽出宝贵时间,如果您想更改答案以考虑换行符内容,我很乐意为您测试运行它(我无法转储 SQL,因为它包含机密信息)。跨度>
      • @Pekka:什么字段类型中有换行符?我尝试使用 TEXT 和 VARCHAR 列,我的转储看起来像:INSERT INTO newline VALUES (1,'Four score, \nand seven years\nago');
      • 我也无法用 BLOB 字段重现它。
      • 这很奇怪。我看一下导入的数据,它停在的记录号总是一样的。
      【解决方案6】:

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

      【讨论】:

        【解决方案7】:

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

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

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

          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;
          

          【讨论】:

          • Pekka 没问题,很高兴我能帮上忙。
          • 请问这条线是什么意思? if (preg_match('~' . preg_quote($delimiter, '~') . '\s*$~iS', end($query)) === 1)
          • @lulalala: 这意味着:“以不区分大小写的方式匹配(转义的)$delimiter 字符,后跟任意数量(0 到 ∞)的空格(或换行符、制表符、等)就在行尾之前“。
          • @AlixAxel:您认为您可以重写它以仅支持单行查询吗?我正在寻找替代方法。
          • 重要提示:这不尊重注释掉的行/块
          【解决方案9】:

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

          【讨论】:

          • 这不是我的自动化方案的解决方案,但很高兴知道。感谢您的链接。 +1
          • 老兄!这东西至高无上!!!我希望我知道它之前存在,它会为我节省大量时间!
          • 不是这篇文章的解决方案,而是简单而优雅的工具,我肯定会在我未来的项目中使用它。
          【解决方案10】:

          当 StackOverflow 以 XML 格式发布他们每月的数据转储时,我编写了 PHP 脚本将其加载到 MySQL 数据库中。我在几分钟内导入了大约 2.2 GB 的 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 类中编写了我的代码,我为我需要加载的每个表进行了子类化。每个子类声明它要加载的列,并按名称(如果数据文件是 CSV,则按位置)将它们映射到 XML 数据文件中的字段。

          【讨论】:

          • 这确实是一种不错的技术,但这并不能提供拆分单个查询的解决方案,而 IMO 是最难的问题。
          • 我认为解析 SQL 脚本并不实用,因为有太多的边缘情况。我建议将数据转储准备为 仅数据,使用 XML 或 CSV 或其他可以在 PHP 中轻松解析的格式。
          • 我同意 Bill 的观点,但这似乎不是 Pekka 的解决方案(至少我从他的问题中了解到)。
          • 我投了反对票。如果您对 StackOverflow 投了反对票,请发表评论以解释原因。
          • 谢谢比尔。正如我对问题所做的修改,导出阶段已经使用mysqldump 完成了,所以虽然它通常可能是使用您描述的导出格式的更好方法,但我在这个问题中的要求是导入实际的 SQL 查询.
          【解决方案11】:

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

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

          【讨论】:

          【解决方案12】:

          你怎么看:

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

          【讨论】:

          • 不能这样做 - 正如我在问题中所写,我无法访问命令行。 (不过,反对票不是我的)。
          • 我忘了发表我的评论:这是一个共享主机,你不能使用“系统”功能和很多“有点危险”的功能。
          【解决方案13】:

          你可以使用LOAD DATA INFILE?

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

          【讨论】:

          • 认为 在这种情况下,我的 mySQL 用户的 LOAD DATA INFILE 已关闭,但会检查。
          • 我赞成这一点,以解决问题。答案在技术上是正确的,即使它对我没有帮助,我不明白为什么它应该被否决。
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2019-08-27
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2011-01-01
          • 2013-05-10
          相关资源
          最近更新 更多