Copias de seguridad

sqlite3 .backup frente a cp con la base de datos en uso: cuántas copias salen corruptas

Publicado el 29 de octubre de 2026Última revisión: 29 de octubre de 20267 min de lectura

Resultado de la prueba

copiando con la base en uso, cp dio 1 de 30 copias corruptas en modo journal y 6 de 30 en modo WAL (en otra ejecución del mismo script, 17 de 30); .backup dio 0 de 30 en ambos. En WAL las copias con cp que pasaban integrity_check tenían menos filas que la base. Con un escritor casi continuo, .backup falló con "database is locked" tras 5 s y dejó un fichero vacío de 0 bytes.

Contenido

Este artículo no contiene enlaces de afiliado.

Copiar con cp el fichero de una base SQLite que está en uso es habitual en scripts de copia. ¿Cuántas veces sale mal? Se midió con un proceso que escribe sin parar mientras se copia, y se comprobó cada copia.

Condiciones: SQLite 3.45.1 (el sqlite3 de Ubuntu 24.04) en Linux arm64. Base de 20 MB con una tabla de unas 20 000 filas de 1 KB y un índice. El escritor es un script de Python que, en cada transacción, hace 100 inserciones y 100 actualizaciones de filas al azar y luego espera 0,2 s. Se hicieron 30 copias con cp y 30 con sqlite3 base ".backup copia" en dos modos de la base: journal_mode=delete (el de por defecto) y journal_mode=wal. Cada copia se comprobó con PRAGMA integrity_check y con SELECT count(*). Disco local, un solo escritor. No se probó con varios escritores, con bases grandes, con VACUUM INTO, con .dump, con copias de -wal y -shm junto al fichero, con un disco de red ni con otras versiones. Que cp falle más o menos veces depende de la carga: los porcentajes son de esta prueba, no una probabilidad general. Script completo al final.

Resultado de las 30 copias

Modo cp: copias con integrity_check distinto de ok .backup: copias con integrity_check distinto de ok
journal_mode=delete 1 de 30 0 de 30
journal_mode=wal 6 de 30 0 de 30

Ningún comando .backup terminó con error. La base original, tras parar el escritor, pasó integrity_check en los dos modos.

Primera línea de integrity_check en las copias malas con cp: *** in database main *** en modo journal y en WAL, y row 35 missing from index tk en WAL. En una ejecución anterior del mismo script, con las mismas condiciones, salieron 17 de 30 con cp en WAL, así que el número varía mucho entre ejecuciones.

Las filas: el cp en WAL pasa la comprobación y está desfasado

En modo WAL las copias con cp iban por detrás de la base. Es lo esperable si lo reciente está en el fichero -wal, que cp de la base no copia, pero no se inspeccionó su contenido. Las tres primeras pruebas:

Prueba Filas en la copia con cp Filas en la copia con .backup
1 20000 20100
2 20000 20200
3 20000 20400

Las copias con cp que pasaron integrity_check tenían 20 000 filas, las del momento de crear la base, mientras que .backup tenía todo lo confirmado hasta ese instante. Es decir, integrity_check = ok no significa que la copia esté al día. En modo journal, las tres primeras pruebas dieron las mismas filas con ambos métodos (20100, 20200 y 20300).

Cuando el escritor casi no descansa

Con la pausa entre transacciones reducida a 0,02 s (modo journal), un solo .backup con .timeout 5000 falló las tres veces:

Error: database is locked
exit=1 en 5.7 s
tamaño de esa copia: 0 bytes; integrity_check: ok

El fichero de copia quedó con 0 bytes y PRAGMA integrity_check lo dio por bueno (una base vacía es válida). No se probó con un .timeout mayor ni con un escritor que haga transacciones más cortas.

Detalles que importan

Script completo

#!/bin/bash
# sqlite3 .backup vs cp of a database being written to concurrently; integrity_check + row count on each copy.
# Usage: test-sqlite-backup-vs-cp.sh WORKDIR [TRIALS]
W=$1; N=${2:-30}; rm -rf "$W"; mkdir -p "$W"
sqlite3 --version | cut -d' ' -f1-2
python3 -c 'import sqlite3,sys; print("python sqlite3 module", sqlite3.sqlite_version)'
cat > "$W/writer.py" <<'PY'
import sqlite3, sys, random, time
db, mode, pause = sys.argv[1], sys.argv[2], float(sys.argv[3])
c = sqlite3.connect(db, timeout=30, isolation_level=None)
c.execute(f"PRAGMA journal_mode={mode}").fetchone()
c.execute("CREATE TABLE IF NOT EXISTS t(id INTEGER PRIMARY KEY, k INTEGER, v BLOB)")
c.execute("CREATE INDEX IF NOT EXISTS tk ON t(k)")
if c.execute("SELECT count(*) FROM t").fetchone()[0] == 0:
    c.execute("BEGIN")
    c.executemany("INSERT INTO t(k,v) VALUES(?,randomblob(1000))", [(random.randrange(10**6),) for _ in range(20000)])
    c.execute("COMMIT")
print("ready", flush=True)
while True:
    c.execute("BEGIN")
    for _ in range(100):
        c.execute("INSERT INTO t(k,v) VALUES(?,randomblob(1000))", (random.randrange(10**6),))
        c.execute("UPDATE t SET k=?, v=randomblob(1000) WHERE id=?", (random.randrange(10**6), random.randrange(1, 20000)))
    c.execute("COMMIT")
    time.sleep(pause)  # pause between transactions
PY
for MODE in delete wal; do
  echo "=================== journal_mode=$MODE"
  D="$W/$MODE"; mkdir -p "$D"
  python3 "$W/writer.py" "$D/live.db" $MODE 0.2 > "$D/writer.out" 2>&1 &
  WP=$!
  while ! grep -q ready "$D/writer.out" 2>/dev/null; do sleep 0.2; done
  ls -l "$D/live.db" | awk '{print "tamaño de la base al empezar: " $5 " bytes"}'
  cp_bad=0; bk_bad=0; cp_err=0; bk_err=0
  for i in $(seq 1 $N); do
    cp "$D/live.db" "$D/cp$i.db"
    r=$(sqlite3 "$D/cp$i.db" "PRAGMA integrity_check;" 2>&1 | head -1)
    rows=$(sqlite3 "$D/cp$i.db" "SELECT count(*) FROM t;" 2>&1 | head -1)
    [ "$r" = "ok" ] || { cp_bad=$((cp_bad+1)); [ $cp_bad -le 2 ] && echo "cp #$i: integrity_check=[$r] filas=[$rows]"; }
    sqlite3 "$D/live.db" ".timeout 30000" ".backup '$D/bk$i.db'" >/dev/null 2>"$D/bk$i.err" || bk_err=$((bk_err+1))
    r=$(sqlite3 "$D/bk$i.db" "PRAGMA integrity_check;" 2>&1 | head -1)
    rows2=$(sqlite3 "$D/bk$i.db" "SELECT count(*) FROM t;" 2>&1 | head -1)
    [ "$r" = "ok" ] || { bk_bad=$((bk_bad+1)); echo ".backup #$i: integrity_check=[$r] filas=[$rows2]"; }
    [ $i -le 3 ] && echo "prueba $i: filas en cp=$rows, filas en .backup=$rows2"
  done
  kill $WP; wait $WP 2>/dev/null
  echo "cp: $cp_bad de $N copias con integrity_check distinto de ok"
  echo ".backup: $bk_bad de $N copias con integrity_check distinto de ok (comandos .backup con error: $bk_err)"
  echo "-- la base original tras parar el escritor:"
  sqlite3 "$D/live.db" "PRAGMA integrity_check;" | head -1
  [ $MODE = wal ] && { ls "$D" | grep -E 'live.db-(wal|shm)' | tr '\n' ' '; echo; }
done
echo "=================== escritor casi continuo (pausa de 0.02 s entre transacciones), journal_mode=delete, un solo .backup con .timeout 5000"
D="$W/busy"; mkdir -p "$D"
python3 "$W/writer.py" "$D/live.db" delete 0.02 > "$D/writer.out" 2>&1 &
WP=$!
while ! grep -q ready "$D/writer.out" 2>/dev/null; do sleep 0.2; done
for i in 1 2 3; do
  s0=$(date +%s.%N)
  sqlite3 "$D/live.db" ".timeout 5000" ".backup '$D/bk$i.db'" 2>&1 | head -1; echo "exit=${PIPESTATUS[0]} en $(echo "$s0 $(date +%s.%N)" | awk '{printf "%.1f", $2-$1}') s"
  echo "tamaño de esa copia: $(stat -c %s "$D/bk$i.db") bytes; integrity_check: $(sqlite3 "$D/bk$i.db" 'PRAGMA integrity_check;' 2>&1 | head -1)"
done
kill $WP; wait $WP 2>/dev/null