Copy SQLite Tables with Indexes

Originally Published: January 4, 2024

I recently had to copy some tables from one SQLite database to another. The tables had indexes and foreign key constraints on them, and I wanted to copy those as well. I found a few different ways to do this, but none of them worked perfectly for me. I ended up writing a tiny script to do it, and I thought I’d share it here.

All the examples below assume we have two databases, old.db and new.db, and we want to copy the posts table from old.db to new.db.

Disclaimer: Please only run these commands on a copy of the database. I am not responsible for any data loss or corruption.

Table of Contents

TL;DR

# Copying table `posts` from `old.db` to `new.db`.
(
  echo "begin;"
  sqlite3 old.db ".schema posts"
  sqlite3 old.db ".dump posts --data-only"
  echo "commit;"
) | sqlite3 new.db

The Simple Way

We can attach both the databases into a sqlite session and then create a table in the new database with a select * query from the old one. This will copy the rows but not the primary key, indexes, or constraints.

sqlite3 new.db "attach 'old.db' as old; create table posts as select * from old.posts;"

Using .dump

The method I found on StackOverflow was to use the .dump command to dump the table as SQL statements and pipe that into the new database. This will copy the primary key and the unique indexes defined as constraints, but not any other indexes.

sqlite3 old.db ".dump posts" | sqlite3 new.db

Using .schema + .dump

The method I ended up using was to use the .schema command to recreate the table structure as well as indexes, and then use .dump to get the data. This will copy the primary key, unique indexes, and any other indexes defined on the table.

The only problem with this method is that .dump also includes a CREATE TABLE statement which will fail because the table is already created by the .schema command. To get around this, I piped the output of .schema into sed to replace CREATE TABLE with CREATE TABLE IF NOT EXISTS.

sqlite3 old.db ".schema posts" | sqlite3 new.db
sqlite3 old.db ".dump posts" | sed 's/^CREATE TABLE /CREATE TABLE IF NOT EXISTS /' | sqlite3 new.db

We can further wrap this up in a transaction so that if any of the statements fail, the whole thing will be rolled back.

(
  echo "begin;"
  sqlite3 old.db ".schema posts"
  sqlite3 old.db ".dump posts" | sed 's/^CREATE TABLE /CREATE TABLE IF NOT EXISTS /'
  echo "commit;"
) | sqlite3 new.db

Alternative Method Using .schema + .dump

I found this method shortly after writing this blog post.

.dump supports a --data-only flag that only outputs INSERT statements for each row in the table.

$ sqlite
SQLite version 3.44.2 2023-11-24 11:41:44
sqlite> .help dump
.dump ?OBJECTS?          Render database content as SQL
   Options:
     --data-only            Output only INSERT statements
     --newlines             Allow unescaped newline characters in output
     --nosys                Omit system tables (ex: "sqlite_stat1")
     --preserve-rowids      Include ROWID values in the output
   OBJECTS is a LIKE pattern for tables, indexes, triggers or views to dump
   Additional LIKE patterns can be given in subsequent arguments

We can combine this with .schema and wrap everything in a transaction to get the same result as above.

(
  echo "begin;"
  sqlite3 old.db ".schema posts"
  sqlite3 old.db ".dump posts --data-only"
  echo "commit;"
) | sqlite3 new.db

This method is more straightforward and doesn’t require any sed magic, but it might be a little slower, especially if you have many indexes on the table, since the indexes are created before INSERT in this method.

I would personally go with this method.

Final Step

Since foreign key constraint checks are disabled by default in SQLite, it’s always a good idea to run a full integrity check on the new database to make sure everything is in order.

sqlite3 new.db "pragma integrity_check;"

If this prints ok, then we’re good to go!