Laravel 5.6中如何逐次执行压缩SQL文件中的查询?
分块执行压缩SQL文件中的查询(Laravel 5.6)
你的问题很典型——大SQL文件一次性读取执行不仅慢,还占内存,尤其是在CI/CD环境里必须优化。核心挑战是准确识别完整的SQL查询,避免把嵌套子查询、字符串或注释里的分号当成查询结束。下面是具体的解决方案:
关键思路
- 逐块读取压缩文件:不用一次性加载整个文件到内存,每次读一小段(比如4KB),降低内存占用。
- 智能解析SQL语句:处理字符串、单行/多行注释中的分号,确保只拆分真正的查询结束标记。
- 即时执行完整查询:每识别出一个完整的查询,立即执行,不用等整个文件读完。
实现代码
替换你原来的执行代码,用下面的逻辑:
use Illuminate\Support\Facades\DB; $sqlFile = gzopen(__DIR__.'/../sql/queries.sql.gz', 'r'); if ($sqlFile === false) { throw new \Exception('Failed to open legacy database migration file'); } echo "Processing compressed SQL file...\n"; $buffer = ''; $inString = false; $stringDelimiter = ''; $inMultiLineComment = false; $inSingleLineComment = false; $queryCount = 0; // 每次读取4KB块,平衡效率和内存占用 while (false !== ($chunk = gzread($sqlFile, 4096))) { $buffer .= $chunk; $bufferLength = strlen($buffer); $pos = 0; while ($pos < $bufferLength) { // 处理多行注释 if ($inMultiLineComment) { if (isset($buffer[$pos], $buffer[$pos+1]) && $buffer[$pos] === '*' && $buffer[$pos+1] === '/') { $inMultiLineComment = false; $pos += 2; } else { $pos++; } continue; } // 处理单行注释 if ($inSingleLineComment) { if ($buffer[$pos] === "\n" || $buffer[$pos] === "\r") { $inSingleLineComment = false; } $pos++; continue; } // 处理字符串(含转义引号) if ($inString) { if ($buffer[$pos] === '\\' && isset($buffer[$pos+1])) { // 跳过转义字符,不处理后续引号 $pos += 2; } elseif ($buffer[$pos] === $stringDelimiter) { $inString = false; $pos++; } else { $pos++; } continue; } // 检测新的注释标记 if (isset($buffer[$pos], $buffer[$pos+1])) { // 多行注释起始 if ($buffer[$pos] === '/' && $buffer[$pos+1] === '*') { $inMultiLineComment = true; $pos += 2; continue; } // 单行注释起始 if ($buffer[$pos] === '-' && $buffer[$pos+1] === '-') { $inSingleLineComment = true; $pos += 2; continue; } } // 检测字符串起始 if ($buffer[$pos] === "'" || $buffer[$pos] === '"') { $inString = true; $stringDelimiter = $buffer[$pos]; $pos++; continue; } // 检测查询结束的分号(不在注释/字符串内) if ($buffer[$pos] === ';') { // 提取并清理完整查询 $query = trim(substr($buffer, 0, $pos)); if (!empty($query)) { $queryCount++; echo "Executing query #$queryCount...\n"; try { DB::connection('etable')->unprepared($query); } catch (\Exception $e) { gzclose($sqlFile); throw new \Exception("Failed to execute query #$queryCount: $query\nError: " . $e->getMessage()); } } // 保留缓冲区剩余内容,继续处理下一段 $buffer = substr($buffer, $pos + 1); $bufferLength = strlen($buffer); $pos = 0; continue; } $pos++; } } // 处理文件末尾剩余的最后一个查询 $finalQuery = trim($buffer); if (!empty($finalQuery)) { $queryCount++; echo "Executing final query #$queryCount...\n"; try { DB::connection('etable')->unprepared($finalQuery); } catch (\Exception $e) { throw new \Exception("Failed to execute final query: $finalQuery\nError: " . $e->getMessage()); } } gzclose($sqlFile); echo "Completed! Executed $queryCount queries.\n";
代码解释
- 状态跟踪:用三个变量跟踪当前解析状态(是否在字符串、多行注释、单行注释内),彻底避免误拆分查询。
- 逐块解析:每次读取4KB数据到缓冲区,扫描缓冲区找合法的查询结束分号,不会因为chunk截断查询而出错。
- 错误定位:每个查询执行失败时,会抛出包含查询内容的异常,方便你快速定位问题语句。
- 进度反馈:输出查询计数,在CI/CD日志里能实时看到执行进度,不会像原来那样黑盒运行几十分钟。
额外优化建议
- 调整块大小:如果你的SQL查询特别长,可以把
gzread的第二个参数调大(比如8192),减少循环次数提升效率。 - 事务控制:如果迁移查询支持事务,可以把多个查询包在事务里,但注意DDL语句(如
CREATE TABLE)在多数数据库中会自动提交事务,需按需调整。 - 索引后置:如果迁移包含大量数据插入,建议在数据插入完成后再创建索引,能大幅提升插入速度。
内容的提问来源于stack exchange,提问作者Dimitrios Desyllas
相关产品推荐
相关产品推荐

