LW IT Solutions logo LW IT Solutions logo LW IT Solutions
« Blog Overview /IT & Networks / Extracting Individual Tables from a Large SQL...
Read this article in other languages:

Extracting Individual Tables from a Large SQL Backup with PHP

Extracting Individual Tables from a Large SQL Backup with PHPExtracting Individual Tables from a Large SQL Backup with PHP

Management Summary

A database export is a file containing stored information and instructions for rebuilding database tables. Such files can become too large to open conveniently when only one section is needed. The article shows how a small PHP program reads the file in portions and copies selected tables, making targeted inspection possible with limited memory. The extracted text is useful for investigation, but related records or settings may be missing, so it is not automatically a complete recovery copy.

Contents
  1. Why one on/off flag is insufficient
  2. Required dump format
  3. PHP example: explicitly named table sections
  4. From an excerpt to a restore
  5. Questions and answers
  6. Sources and review date

A large MySQL dump may contain only one table that needs further work. Extracting relevant text does not require loading the entire file into an editor or PHP memory. The important distinction is the intended result: a text excerpt for inspection or a database backup that can actually be restored.

A line-based filter checks text lines. An SQL parser also recognises the structure of SQL statements, such as where a quoted value begins and ends. This filter does not perform that analysis. The PHP example below copies selected table sections identified by standard comments. Its output remains a text excerpt for review. A targeted fresh export or a validated restore workflow is preferable when recoverability is the goal.

SQL excerpts: separate database and table selection

Why one on/off flag is insufficient

The filter tracks two things separately: the current database and whether the current table should be copied. A database change ends the current selection. Every new table marker requires another selection decision. For example, after a selected orders section, an unselected logs section must turn copying off again. Checking table markers only while copying is disabled would continue copying unrelated tables after the first match.

fgets() alone does not guarantee tiny memory usage. Without a length limit, it reads a whole line, and an extended INSERT can be large. This example reads at most 65,536 bytes per step and examines markers only at the start of a line. Long data lines are copied in chunks.

Required dump format

  • An uncompressed text file using the usual mysqldump comment format with database markers, such as an export made with --databases or --all-databases.
  • The file contains -- Current Database: and the relevant -- Table structure for table and/or -- Dumping data for table markers. Dumps made with --skip-comments are not supported by this method.
  • Exact database and table names, rather than one ambiguous shared prefix. Embedded backticks in identifiers and different generator formats are outside its scope.
  • PHP 8 CLI and a local filesystem supporting hard links within one directory. A hard link gives the same file contents another name; here it creates the final name without replacing an existing file. Input, partial output, and final output belong outside publicly accessible web directories.

Compressed dumps first need decompression into a protected workspace with sufficient storage. Inspecting the file header helps identify the format, but does not establish the complete contents, every later table, or all line lengths.

A grooved chocolate bar breaks cleanly into even squares, while a smooth one shatters. Likewise, the script needs the section markers in the database dump; without these comments the split fails.
A grooved chocolate bar breaks cleanly into even squares, while a smooth one shatters. Likewise, the script needs the section markers in the database dump; without these comments the split fails.

PHP example: explicitly named table sections

php extract.php dump.sql excerpt.txt shop orders customers selects orders and customers from database shop. Neither the output file nor its .part file may already exist.

<?php
// PHP 8 CLI. Produces a text excerpt, not a validated restore file.
umask(0077);
if ($argc < 5) {
    fwrite(STDERR, "Usage: php extract.php input.sql excerpt.txt database table [table...]\n");
    exit(2);
}
[, $input, $output, $database] = $argv;
$tables = array_fill_keys(array_slice($argv, 4), true);
$part = $output . '.part';
$in = $out = false;
$created = false;

try {
    $in = @fopen($input, 'rb');
    if ($in === false) throw new RuntimeException('Cannot open input');
    $out = @fopen($part, 'xb');
    if ($out === false) throw new RuntimeException('Cannot create new partial output');
    $created = true;
    $currentDatabase = null;
    $copy = false;
    $lineStart = true;
    $seen = [];

    while (($chunk = fgets($in, 65537)) !== false) {
        if ($lineStart) {
            if (preg_match('/^-- Current Database: `([^`]+)`\r?\n?$/D', $chunk, $m)) {
                $currentDatabase = $m[1];
                $copy = false;
            } elseif (preg_match('/^-- (?:Table structure|Dumping data) for table `([^`]+)`\r?\n?$/D', $chunk, $m)) {
                $copy = $currentDatabase === $database && isset($tables[$m[1]]);
                if ($copy) $seen[$m[1]] = true;
            } elseif (str_starts_with($chunk, '-- ')) {
                $copy = false;
            }
        }
        if ($copy) {
            $offset = 0;
            $length = strlen($chunk);
            while ($offset < $length) {
                $n = fwrite($out, substr($chunk, $offset));
                if ($n === false || $n === 0) throw new RuntimeException('Write failed');
                $offset += $n;
            }
        }
        $lineStart = str_ends_with($chunk, "\n");
    }
    if (!feof($in)) throw new RuntimeException('Read failed');
    if (array_diff_key($tables, $seen)) {
        throw new RuntimeException('Requested table marker missing; format or names differ');
    }
    if (!fflush($out)) throw new RuntimeException('Flush failed');
    if (!fclose($out)) throw new RuntimeException('Close failed');
    $out = false;
    // Same-directory hard link: fails if the final name already exists.
    if (!@link($part, $output)) throw new RuntimeException('Cannot publish new output');
    if (!unlink($part)) throw new RuntimeException('Output exists, partial link cleanup failed');
    $created = false;
    fwrite(STDERR, "Text excerpt written. Separate restore validation required.\n");
} catch (Throwable $e) {
    if (is_resource($out)) fclose($out);
    if ($created && is_file($part)) unlink($part);
    fwrite(STDERR, $e->getMessage() . "\n");
    exit(1);
} finally {
    if (is_resource($in)) fclose($in);
}

The final output appears only after the input has been read, writes have succeeded, and at least one matching marker has been found for each requested table. A marker proves section selection, not complete SQL objects. An existing result is not overwritten. Errors produce a nonzero exit status rather than reporting a partial excerpt as a finished result.

From an excerpt to a restore

Session settings such as character set and SQL mode, foreign-key dependencies, triggers, views, and routines may sit outside the copied sections. For example, an orders table may refer to customer IDs in a separate customers table. Copying the orders section alone does not include those customer records. Unusually formatted SQL strings can also contain comment-like text. This example does not interpret those SQL structures and is not a universal import filter.

When the original server remains available, mysqldump accepts a database followed by explicitly selected table names. For InnoDB, --single-transaction can provide a consistent backup; other storage engines and concurrent schema changes require an appropriate strategy. When only a dump remains, an isolated restore followed by targeted object export offers a more controlled route. Target version, storage, dependencies, and permissions need checking before import.

Questions and answers

How fast is the filter?

The entire input file is read. Runtime and memory use depend on storage, output volume, PHP, and other factors. A blanket promise of several gigabytes in seconds is not reliable. The bounded read buffer simply avoids holding an entire unusually long line in PHP memory.

What does a missing match mean?

A name may be absent, spelled differently, or present without the expected comments. The script reports an error in that case. An empty excerpt is not presented as evidence of an empty table.

Does success prove that every selected object can be restored?

No. A restore test with schema, row, and dependency checks is a separate step. The original dump remains unchanged.

Sources and review date

2026-10-24

Lukas Wojcik

Lukas Wojcik

Systems architect and technology enthusiast specializing in scalable tracking solutions, GMP Stack (GA4 & GTM), and robust backend architectures. Advocate for clean code and privacy-first design.

Get in touch

A question or an idea for a web project? Tell me a little about it. This form uses reCAPTCHA and formconfirmed to protect against spam.

Write a comment

Experience with other hardware or firmware and questions about the configuration are welcome here.

The email address is not published. Required fields are marked with an asterisk.

LUKASWOJCIK.COM · NEWSLETTER

Fresh ideas in your inbox

New articles, practical tools and updates from the projects.

Choose topics (optional)
No selection: all topics.

Subscription starts after confirmation by email. · Privacy

Privacy · Newsletter

Email address, language, selected topics and subscription/confirmation dates are stored to deliver the newsletter. Double opt-in is required. Every issue includes an immediate unsubscribe link. Formconfirmed checks the signup for spam. No email open or click tracking.

Read the full privacy policy ↗