The hidden engineering of safe large-scale data migration — told through the story of moving a library of twelve million books.

INGOAMPT · ARTICLES
Data Engineering · Databases · For Everyone

Why You Can’t Just Copy 12 Million Records

The hidden engineering of safe large-scale data migration — told through the story of moving a library of twelve million books.

Beginner-friendly start · expert-level finish · ~35 min read

There is a moment, familiar to almost anyone who has ever worked with data, when a beautifully simple plan collides with reality. You have found a treasure: a giant public dataset — the entire DBLP computer-science bibliography, say, or all of PubMed, or a Wikipedia dump — released for free as one enormous file. You want it in a database so you can query it. So you write the obvious three lines of code: open the file, parse it, insert every record. You hit “run,” lean back, and wait.

And wait. And then, three hours in, your laptop’s fan screaming, the process dies. Out of memory. Or the power blips. Or you realize the import has been silently dropping records for the last hour and you have no idea how many. You are left with a half-populated database, no idea what’s in it, and a sinking feeling that “just copy the data” was not, in fact, just copying the data.

This article is about why that happens, and what professionals do instead. It starts from absolute first principles — no database knowledge assumed — and gets progressively deeper.

Each section carries a depth label (Beginner, Intermediate, or Deep) so you can stop whenever you have what you need.


1 · The Naive View vs. Reality

Depth: Beginner

The library analogy

Imagine you have been asked to move a library. Not a small one — a library of twelve million books. Your friend, who has moved apartments a few times, says: “Easy. Pick up the books, carry them to the new building, put them on the shelves.”

That instruction is not wrong. It is exactly what you must do to each book. But it ignores every hard part of the problem: you cannot carry twelve million books at once, so you need boxes. The move takes weeks — what happens if you get sick on day four? Do you start over? And when you are done, how do you prove that all twelve million books arrived, none lost, none damaged, none duplicated?

Converting a giant data file into a queryable database is precisely this problem. The naive view — “just load the file into SQL” — is the friend saying “just carry the books.” It works fine for a shelf of fifty books (a small file). It fails catastrophically for the library (a multi-gigabyte file with millions of records).

THE THREE WALLS EVERY BIG MIGRATION HITS ⏱ TIME The job runs for HOURS. Power cut at hour 3? Crash? Accidental Ctrl-C? Restart from zero = maybe never finishing. 🧠 MEMORY A 5 GB file cannot be loaded whole into an 8 GB laptop’s RAM. Naive parser = crash before record #1. 🔍 TRUST “It finished without errors” is NOT the same as “the data is correct.” 12.8M records: you cannot eyeball it. Everything in this article is a weapon against one of these three walls. Streaming defeats MEMORY · transactions + checkpoints defeat TIME · verification defeats TRUST
Figure 1 — Time, memory, trust. Every large data migration collides with the same three walls. The entire discipline of safe migration is a set of tools for defeating them one at a time.
Simple example A 50-record address book: your three-line script works perfectly, finishes in a blink, and you can check the result by eye. The same script pointed at DBLP’s 12,796,119-record XML file: crashes on memory or, if it survives, runs for hours with no way to resume and no proof of completeness. Same code — the scale changed the problem’s nature.

2 · Verify the Source Before You Do Anything

Depth: Beginner → Intermediate

Before you parse a single byte, answer a deceptively basic question: did the file even download correctly? Multi-gigabyte downloads fail in quiet, nasty ways — a connection drops at 98% and you get a truncated file; a disk error flips a bit. If you build a database on a subtly broken file, every downstream step is wasted. The old adage applies in full force: garbage in, garbage out.

Checksums: the fingerprint of a file

The standard tool is the checksum (hash). A checksum function reads a file of any size and produces a short fixed-length “fingerprint.” The key property: change even a single byte of the file and the fingerprint changes completely (the avalanche effect). So the workflow is simple: the provider publishes the correct file’s fingerprint; you compute your copy’s fingerprint; compare. Match = your copy is byte-for-byte identical.

CHECKSUM VERIFICATION — PROVING YOUR COPY IS PERFECT Provider’s file dblp.xml.gz (1 GB) Your download dblp.xml.gz (1 GB?) hash fn hash fn 7ee3323762f1fed8b939… 7ee3323762f1fed8b939… MATCH ✓ One flipped byte anywhere in 1 GB → a completely different fingerprint. Mismatch = re-download.
Figure 2 — Trust begins at the download. Serious providers publish the fingerprint of the correct file (DBLP ships a .md5 companion file for every dump). Comparing fingerprints proves your gigabytes arrived intact before you invest hours building on them.

MD5 vs. SHA-256 — which fingerprint?

Table 1 · The two common hash functions
PropertyMD5SHA-256
Fingerprint size128 bits (32 characters)256 bits (64 characters)
SpeedFasterSlower, still fast (~500 MB/s on a modern CPU)
Collision resistanceBroken — two different files CAN be crafted with the same MD5 (demonstrated 2004; exploited by the Flame malware in 2012)No known practical collision attack
Good forCatching accidental corruption (downloads)Accidental corruption and malicious tampering

The practical nuance: MD5 remains fine for its most common job — detecting download corruption, which is not a crafted attack. When you must defend against someone substituting a file, use SHA-256 (the standard behind TLS certificates and code signing). Recording both costs nothing extra: one streaming pass over the file can compute both at once.

Test the compression, and write down the provenance

Compressed archives carry their own built-in integrity check: gzip -t file.gz decompresses in memory and verifies the internal CRC-32 checksum without writing anything. A fast, free second opinion.

Finally, record provenance — the data’s origin story: exact source URL, download date, snapshot version, verified checksum. Living datasets change; DBLP rebuilds its XML continuously and publishes monthly snapshots, each now registered with its own DOI (scheme 10.4230/ dblp.xml.YYYY-MM-DD) via Schloss Dagstuhl. “I used DBLP” is unreproducible; “I used the snapshot of 2026-07-01, DOI X, MD5 Y” is science.

Simple example You download a 1 GB file and your Wi-Fi hiccups at 98%. The file looks complete in Finder — the size is close, it even opens partially. Only the checksum comparison reveals the truth: fingerprints differ, the file is truncated, re-download. Without the check, you’d have discovered this three hours into the import — or never.

3 · Streaming vs. Loading: Reading a File Bigger Than Your Memory

Depth: Intermediate

DOM parsing: the whole library on your desk

A program can read a structured file like XML in two fundamentally different ways. DOM parsing reads the entire document and builds a complete navigable tree of it in memory. Wonderfully convenient — and hopeless at scale. In the library analogy: it insists every one of the twelve million books be laid out on your desk before you begin. The memory cost is far more than the file size, because every element becomes an in-memory object with overhead. DBLP’s own documentation warns that recent versions of dblp.xml no longer fit in a standard 4 GB Java heap and recommends allocating 8 GB — just to hold the tree, before doing anything useful with it.

Streaming: one box at a time

The alternative — streaming (SAX, or iterparse in Python) — reads sequentially and hands you one record at a time: process it, discard it, receive the next. Your desk only ever holds one box. Memory use stays small and flat whether the file has twelve thousand records or twelve million.

MEMORY USE WHILE READING A 5 GB FILE RAM file position → DOM: grows with the file 💥 crash: RAM exhausted before the end Streaming: flat, ~tens of MB 8 GB laptop limit Measured reality: a properly-released iterparse ran a 223 MiB file in ~23 MiB of RAM — constant, regardless of size.
Figure 3 — Why streaming is mandatory, not optional. DOM memory grows with the file and exceeds an ordinary laptop long before 5 GB. Streaming stays flat forever. One trap: you must actively clear() each finished record — forget that, and your “streaming” parser silently rebuilds the whole tree anyway (the same benchmark ballooned from 23 MiB to 1.5 GB when elements weren’t released).

Real-world XML ambushes: the DTD and the billion laughs

Real dumps hold two more traps. First, entities: dblp.xml is plain ASCII and writes “Müller” as Müller, with the meaning of ü defined in a separate grammar file — the DTD. Parse without the DTD present, and the parser fails at the first accented name. Second, entity-expansion limits: DBLP contains so many entities that parsers may refuse it until you raise a security limit (its own docs show -DentityExpansionLimit=2500000).

Why does that limit exist at all? Because of a classic attack: the billion laughs. An attacker defines entity 1 as “lol”, entity 2 as ten copies of entity 1, entity 3 as ten copies of entity 2… nine levels deep. The file is under 1 KB — but expanding it produces one billion “lol”s, nearly 3 GB, crashing the parser. Parsers therefore ship with expansion limits ON by default. The engineering judgment: understand why the guardrail exists, then knowingly raise it for a trusted file like DBLP — and never blindly disable it for input you don’t control.

Simple example Think of reading a 1,000-page novel. DOM = photocopying all 1,000 pages onto your desk before reading a word (desk overflows). Streaming = reading page by page, each page recycled after reading (desk always holds one page). And the DTD is the book’s glossary in a separate booklet: lose the booklet, and half the words become unreadable symbols.

4 · Schema Design: From Shapeless Dump to Tidy Tables

Depth: Intermediate → Deep

XML and JSON dumps are semi-structured: hierarchical, flexible, full of optional and repeated fields. Relational databases want rows and columns with fixed types. Bridging that gap is data modeling, and three decisions matter most:

Ordered relationships: author order is data

A publication has a list of authors, in a specific order — and in science, order carries meaning (first author, last author). Cramming names into one text field (“Alice, Bob, Carol”) destroys queryability; storing them as an unordered set destroys the order. The relational answer: a linking table with one row per (publication, author) pair plus an explicit position column (author 1, author 2, …). Tables have no inherent row order — if order matters, store it as data.

Keep the raw record: never let your projection become the only truth

Parsing is choosing: which fields, how to normalize, how to interpret. Choices can be wrong, and bugs are discovered months later. Two patterns preserve your escape hatch: (a) keep the immutable raw file (with its checksum) as the authoritative source, treating the database as a rebuildable projection; and/or (b) store the original record fragment alongside the parsed columns. Related discipline: separate authoritative values from derived ones — if you normalize an author name, keep the original string too. It is the database equivalent of showing your work.

Simple example Six months in, you discover your parser dropped everything after an & in titles — “Bell & Howell Labs” became “Bell”. If the raw file (or raw fragment column) still exists: fix the code, rebuild, done in an hour. If you only kept the parsed columns: the information is gone forever.

5 · Transactional Safety — the Heart of the Matter

Depth: Deep (but the trucks are for everyone)

What is a transaction?

A transaction is a group of database operations treated as one indivisible unit: either all take effect, or none do. The classic example is a bank transfer — subtract from savings, add to checking. A crash between the two steps must not evaporate your money; the transaction guarantees the pair happens completely or un-happens completely. Databases call these guarantees ACID: Atomicity (all-or-nothing), Consistency (rules never violated), Isolation (concurrent work doesn’t collide), Durability (committed = survives a crash). For bulk loading, atomicity and durability are the stars.

The central decision: how big is each truckload?

THREE WAYS TO TRUCK 12 MILLION BOOKS A · ONE BOOK PER TRIP (commit every record) 🚚 📕 🚚 📗 🚚 📘 🚚 📙 … Maximum safety, absurd overhead: measured ~85 inserts/second. 12.8M records at this rate ≈ 42 HOURS. ✗ B · ALL BOOKS, ONE GIANT TRIP (one huge transaction) 🚛 📚 📚 📚 📚 📚 📚 📚 📚 📚 📚 📚 📚 →→→→→→→→→→ 🏛 Fast — but crash at book 11,999,000 = EVERYTHING rolls back. Hours lost, zero kept. ✗ C · BOXES OF 1,000 (batched transactions) ✓ 🚚 [📦 ×1000]✓ 🚚 [📦 ×1000]✓ 🚚 [📦 ×1000]✓ … Nearly all the speed of B (~96,000 inserts/second measured ceiling), crash cost bounded to ONE box. The professional’s choice.
Figure 4 — Batching is the compromise that wins. A famous SQLite benchmark spans ~85 to ~96,000 inserts per second — a thousandfold difference — driven almost entirely by transaction strategy. Batches of ~1,000– 10,000 capture the speed while capping what a crash can cost.
Table 2 · The truckload tradeoff
ApproachSpeedCrash costVerdict
One transaction per record~85/s (measured)Lose ≤ 1 recordToo slow
One giant transactionFastLose EVERYTHINGToo fragile
Batches of ~1,000≈ the fast ceilingLose ≤ 1 batch✓ Standard practice

Two more classic accelerators: prepared statements (compile the INSERT once, reuse it 12.8M times instead of re-parsing SQL text per row) and deferred index creation (load first, build indexes after — the same benchmark measured 63,300/s for load-then-index vs. 47,700/s for index-then-load).

Journal modes and how paranoid to be

Under the hood, SQLite delivers atomicity via a journal. Two modes: the rollback journal (default) writes the old content aside before changing pages — a leftover journal after a crash lets it undo the incomplete change automatically. WAL (write-ahead logging) does the inverse: appends new changes to a side file, folding them in later — generally faster, and readers keep working while a writer writes. Orthogonal to that, the synchronous setting controls how forcefully each commit is pushed to the physical disk:

Table 3 · Safety vs. speed knobs in SQLite
SettingBehaviorRisk on power loss
synchronous = FULL (default)Sync at every critical momentSafest, slowest
WAL + synchronous = NORMALSync less oftenMay lose the most recent commits — but never corrupts; SQLite’s docs call it “a good choice for most applications”
synchronous = OFFNever wait for the diskFastest; database can be CORRUPTED

The pragmatic bulk-load angle: if the database is rebuildable from the verified raw file, some practitioners deliberately relax durability during the load for speed (one documented case cut an estimated 50-hour import of ~50M records to ~17 minutes with aggressive settings), then restore safe settings for the finished product. Legitimate — only because the raw file remains the authoritative source. Never do this to data you cannot rebuild.

Checkpoints: the bookmark that defeats the time wall

CHECKPOINTED INGESTION — A CRASH COSTS MINUTES, NOT HOURS start12.8M done ✓1k✓500k✓2M✓5M✓8.3M 💥 crash Every committed batch also durably records a BOOKMARK: “complete through record 8,300,000”. On restart, the importer reads the bookmark and continues from 8,300,001 — not from zero. Only the last unfinished batch is redone. resumes here → Pair with IDEMPOTENCY (re-running does no harm: duplicates safely ignored) and the importer can be killed and restarted endlessly, always converging on the correct result.
Figure 5 — Resume, don’t restart. Because the batch commit and the bookmark update live in the same transaction, they can never disagree: no batch is marked done that wasn’t written, none written without being marked. A 3-hour job becomes interruptible at will.

Two layers of crash recovery — the database’s job and yours

When a process dies mid-write, the database’s own state heals automatically: on next open, SQLite’s journal rolls back the half-finished transaction. You write no code for this. But your application’s bookkeeping does not heal itself: if your importer marked a batch “running” and then died, that flag stays “running” forever — the process it describes is gone. Your code must detect stale “running” markers on startup and reclaim them. Forgetting this second layer is a classic source of pipelines that look busy but are dead. And one humble guard worth its five lines of code everywhere: refuse to overwrite an existing database unless explicitly told to — a re-run of a script should never silently destroy hours of completed work.

Simple example You’re baking 12,800 cookies in batches of 1,000. After each tray you tick a checklist: “8 trays done.” Power cut at tray 9? The oven (database) automatically discards the half-baked tray — and your checklist (checkpoint) says exactly where to resume: tray 9, not tray 1. Without the checklist you’d have to start from cookie #1, unable even to prove which cookies exist.

6 · Validation and Reconciliation: Trust, but Verify

Depth: Deep

We defeated memory (streaming) and time (batches + checkpoints). The third wall remains: trust. An importer can finish cleanly while being quietly, badly wrong. A parser bug might skip every record containing an unusual character. A schema mismatch might silently truncate long fields. None of these raise an error. All of them corrupt your dataset. The only defense is to actively audit the claim of success.

Reconciliation: 600-year-old wisdom

The most powerful check is reconciliation: compare an independent count of the source against what actually landed in the database. This is exactly the logic of double-entry bookkeeping, trusted by accountants since the 1400s: record everything twice, from two directions, and the totals must agree — a mismatch is a guaranteed sign of an error somewhere.

RECONCILIATION — TWO INDEPENDENT COUNTS MUST AGREE COUNT 1 · the source census Independent scan of the raw XML: total records: 12,796,119 articles: 4,387,689 conference papers: 3,921,218 …per-type, per-key, all unique COUNT 2 · the database SELECT COUNT(*) after loading: total rows: 12,796,119 articles: 4,387,689 conference papers: 3,921,218 …same breakdown, same keys MATCH? Agree → an error would have to fool two different tools identically. Essentially impossible. Disagree by even 1 → STOP. Find out why before trusting anything.
Figure 6 — Double-entry bookkeeping for data. Count the source with one tool, count the database with another, and demand exact agreement — overall and per record type, since a bug that drops only one type hides inside a correct-looking grand total.

The full audit kit

Beyond the headline counts, four cheap checks close the remaining gaps:

  • Key uniqueness — if every record has a supposedly unique key, prove it: GROUP BY key HAVING COUNT(*) > 1 must return nothing. Duplicates expose either source problems or a bug in your resume logic.
  • Referential integrity — every author-link must point at a real record: PRAGMA foreign_key_check reports each violation. (SQLite does not enforce foreign keys by default — enable them with PRAGMA foreign_keys = ON.)
  • Physical soundnessPRAGMA integrity_check walks the entire file structure (B-trees, indexes, page links) and reports corruption; its faster cousin quick_check covers most of it in O(N) time.
  • Sampling — counts prove records exist, not that their contents are right. Pull a few hundred random records from the database, pull the same records from the raw source, compare field by field. Cheap insurance against systematic content bugs (like mangled character encoding) that counts can never reveal.

Test small first — always

Never debut new pipeline code on the full dataset. Stage it: 1,000 records (does it work at all?) → 100,000 records (does it survive scale and edge cases?) → only then the full run. Each rehearsal surfaces bugs while they cost minutes instead of hours. This is not timidity; it is how experienced engineers avoid burning an afternoon on a run that was doomed from minute one.

Simple example A moving company delivers your 12 million books and says “all done, no problems!” Do you just believe them? No — you count the boxes against your own manifest, shelf by shelf. 11,999,650 arrived? Then 350 books are missing and “no problems” was false. The counting is the trust.

7 · Necessary or Overkill? The Decision Framework

Depth: Intermediate — and the most important judgment in the article

Everything above is the full production-grade treatment. A critical piece of engineering judgment — arguably the piece — is knowing when you need it and when you emphatically don’t. Building a fault-tolerant, resumable, fully-reconciled pipeline for a file you’ll parse once and delete is its own failure mode: over-engineering wastes the very time it claims to protect.

Five questions decide it

  • Duration: seconds, or hours? Seconds-long jobs need no resumability — just re-run them.
  • Memory: does the file fit comfortably in RAM? If yes, streaming is optional.
  • Stakes: throwaway exploration, or something that a publication, product, or decision depends on?
  • Rebuildability: can you regenerate the database from an authoritative source if it’s wrong?
  • Reproducibility: must someone (including future you) reproduce this exact result later?
Table 4 · Quick script vs. production-grade — when each is right
DimensionQuick scriptProduction-grade pipeline
Right when…Small file, one-off, low stakes, rebuildableLarge file, repeated, high stakes, must be trusted & reproducible
ParsingLoad it all into memoryStream with constant memory
TransactionsAutocommit is fineBatched transactions (~1,000/commit)
ResumabilityJust re-run from scratchCheckpoint + resume + idempotency
ValidationEyeball a few rowsReconcile counts, integrity checks, sampling
ProvenanceSkip itSource, version, checksum, code commit — recorded
Overwrite protectionDon’t botherRefuse to clobber
Time to buildMinutesHours to days

Knowing what NOT to build

There is a subtler over-engineering trap: importing machinery from a different problem. One person loading a file on one laptop does not need distributed locks, ownership leases, heartbeats, multi-process coordination, or message queues. Those tools solve a problem — many machines contending for shared state — that a single-operator job simply does not have. Reaching for them “to be safe” adds complexity and delay without adding safety. A useful discipline: timebox infrastructure work in proportion to the stakes, and write down what you deliberately chose not to build. Good engineering is as much about the safeguards you consciously omit as the ones you install.

Simple example Moving one bookshelf to the next room? You don’t verify each book’s ISBN, photograph the shelf, and hire a logistics auditor — you carry the books. Moving a national library across a country, where scholars will cite its catalog for decades? Now the manifests, the checks, and the audit earn their cost. Same activity, different stakes, different engineering.

8 · How Real Projects Do It

Depth: Intermediate → Deep

The consensus SQLite bulk-load recipe

  1. Wrap inserts in batched transactions (~1,000–10,000 rows) — the single biggest win, worth up to a thousandfold in speed.
  2. Use prepared statements; bind values, don’t rebuild SQL text per row.
  3. Defer index creation until after the load.
  4. Tune PRAGMAs to the situation — aggressive throwaway-load settings only when the database is rebuildable; a safe production baseline (WAL + synchronous=NORMAL + foreign_keys=ON) for the finished product.
  5. Afterward: restore safe settings, build indexes, run integrity_check and foreign_key_check, reconcile counts, write the provenance manifest.

What the giants’ download pages teach

The big scientific data providers have learned these lessons at scale, and their distribution formats quietly encode them:

Table 5 · Real dumps and their built-in lessons
DatasetScaleThe lesson encoded in its format
DBLP~8.6M publications; ~12.8M total XML records; ~1 GB compressed, ~5 GB rawMonthly snapshots with DOIs + MD5 files → provenance and verification, institutionalized
PubMed/MEDLINE>33.8M citations; baseline ~114 GB uncompressed across hundreds of filesAnnual baseline + daily update files → load the base once, apply increments; process baselines before updates
Wikipedia (enwiki)>25 GB compressed, >105 GB decompressedMultistream format → random access without decompressing everything
OpenAlex~264M works; ~330 GB gzipped JSON Lines → ~1.6 TB rawPartitioned files + manifest.json with record counts → reconciliation built into the format
Crossref~180M records; 208 GB compressedAnnual public file, many chunks → a failed piece re-fetches without redoing the world

Notice the recurring patterns: everything ships compressed; several ship many partitioned files (batching, applied to distribution); several publish manifests with counts (reconciliation, built in); several separate baseline from updates; and several attach DOIs, checksums, and dated snapshots (reproducibility). The providers arrived at exactly this article’s checklist — from the other side.

ETL vs. ELT, demystified in one breath

Both acronyms name the same three stages — Extract (get data out of the source), Transform (clean and reshape), Load (put it in the target) — differing only in order. ETL transforms before loading: the traditional approach, right when only tidy data should land in a modest relational store. ELT loads raw data first and transforms inside a powerful cloud warehouse: the modern big-data approach, which keeps the raw around. A single-file-into-SQLite job is lightweight ETL — and the “keep the raw record” pattern from Section 4 is a nod to ELT’s core wisdom.

The provenance manifest: a lab notebook for your database

The finishing professional touch: a small record stored with the database, answering “where did this come from?” without archaeology — which source file (URL, date, checksum), which code version built it (a git commit hash), which settings (batch size, PRAGMAs, schema version), when, and how many records. Months later, when someone asks “why does this differ from the paper?”, the manifest turns a mystery into a lookup. This is why DBLP urges researchers to cite snapshots by persistent identifiers: so experiments remain reproducible in the future.

Simple example Two jars of homemade jam, unlabeled vs. labeled “strawberries from the July 13 market batch, recipe v2, made July 18.” A year later, only one of them can answer “can I still trust this, and can I make it again?” The label costs ten seconds. The manifest is the label.

9 · The Checklist

Depth: for everyone — the whole article on one page

Before you load

  • Verify the source: compute the checksum, compare against the provider’s published value; test archives with gzip -t.
  • Record provenance: source URL, download date, exact snapshot/version/DOI, checksum.
  • Get the DTD / schema and any auxiliary files the format needs.
  • Design the schema around the questions you’ll ask; store ordered relationships as explicit positions; keep a path back to the raw source.

While you load

  • Stream, don’t slurp — and release each record (clear()) after processing.
  • Batch in transactions (~1,000–10,000/commit); prepared statements; defer indexes.
  • Checkpoint for resumability; make inserts idempotent.
  • Handle both recovery layers: trust the journal for automatic rollback; write explicit logic for stale “running” state.
  • Refuse to overwrite an existing database without an explicit flag.
  • Tune PRAGMAs to the situation — relax durability only for rebuildable data.

After you load

  • Reconcile counts — independent source census vs. database, overall and per type. Exact agreement or stop.
  • Check key uniqueness and referential integrity (PRAGMA foreign_key_check).
  • Run PRAGMA integrity_check on the finished file.
  • Sample and spot-compare records against the raw source.
  • Test small first (1,000 → 100,000 → full) whenever pipeline code is new.
  • Write the provenance manifest.

Always

  • Right-size the engineering to the stakes. A throwaway exploration needs almost none of this; a reproducible research artifact or production dataset needs most of it. Don’t build distributed-systems machinery for a single-laptop job. Timebox infrastructure work.

Closing Thought

The phrase “just copy the data” hides an entire discipline. Moving twelve million records from a raw dump into a queryable database is not a copy operation; it is a small logistics campaign against three relentless adversaries — time, memory, and trust — fought with streaming parsers, batched transactions, checkpoints, checksums, and reconciliation. None of the individual ideas is hard. What is hard, and what marks the experienced engineer, is knowing which of them a given job actually needs, applying exactly those, and consciously skipping the rest. Do that well, and the library of twelve million books arrives intact, on the right shelves — with a receipt proving every one of them made it.

Further Reading

SQLite documentation — Pragma statements (journal_mode, synchronous, integrity_check, foreign_key_check) and “Write-Ahead Logging” (sqlite.org) · DBLP / Schloss Dagstuhl — “How to parse dblp.xml” and “What do I find in dblp.xml? ” FAQs; snapshot DOI announcement (dblp.org, blog.dblp.org) · OpenAlex — snapshot data format (developers.openalex.org) · Crossref — public data files (crossref.org) · NLM — MEDLINE/PubMed baseline distribution (nlm.nih.gov) · Wikipedia — “Database download”; “Billion laughs attack”; “ACID” · Stefan Behnel — “Faster XML stream processing in Python” (blog.behnel.de) · The classic “Improve INSERT-per-second performance of SQLite” benchmark (widely mirrored).

Figures in this article are original illustrations. Benchmark numbers are point measurements from the cited sources; your hardware will differ — the ratios, not the absolutes, are the lesson.

INGOAMPT · ingoampt.com — articles that make engineering understandable for everyone.

Leave a reply

Your email address will not be published. Required fields are marked *