How to solve "Error executing purge" during repack

When I try to launch a repack I get:

Error executing purge: (sqlite3.OperationalError) database or disk is full \[SQL: VACUUM\] (Background on this error at: https://sqlalche.me/e/20/e3q8)

I have the default retention days, but some energy price entities have huge attributes and an integration I now removed was spamming hugely.
The database is right now 12.8 GB.

When using the integration “FileShell” I can start a terminal session and the disk space available is plenty:

Filesystem                Size      Used Available Use% Mounted on
overlay                  97.8G     91.6G      1.2G  99% /
tmpfs                    64.0M         0     64.0M   0% /dev
shm                      64.0M      8.0K     64.0M   0% /dev/shm
ssd_data/home-assistant
                        592.4G     11.5G    580.9G   2% /config
/dev/md126p1             97.8G     91.6G      1.2G  99% /etc/localtime
tmpfs                     3.1G      5.8M      3.1G   0% /run/dbus
/dev/md126p1             97.8G     91.6G      1.2G  99% /etc/resolv.conf
/dev/md126p1             97.8G     91.6G      1.2G  99% /etc/hostname
/dev/md126p1             97.8G     91.6G      1.2G  99% /etc/hosts

The database resides in /config which has still 580 GB free.

How to somehow slim down the database?

I am open to launch SQL queries manually to it while HA is offline because I have SSH access to the server, that’s totally fine, but which queries should I use?

Claude Opus 5 explains this procedure, but I’d like someone to verify it.

Me: which SQL queries should I run to manually remove specific entities from the sqlite database, while home assistant is taken offline?

With HA stopped this is straightforward, but there are three traps: state_attributes rows are shared and orphan themselves silently, states.old_state_id self-references, and the statistics tables use a different metadata table than states. Here’s the full sequence.

0. Before you touch anything

ha core stop            # or: docker stop homeassistant / systemctl stop ...
cp home-assistant_v2.db{,-wal,-shm} /backup/    # -wal/-shm may not exist; that's fine
sqlite3 home-assistant_v2.db

Make sure HA is genuinely stopped, not just unresponsive — a live writer plus a CLI session is how you get a corrupt file. Then confirm the schema matches what’s below, since HA does change it:

.schema states
.schema statistics_meta

1. Pick your targets

Build a scratch table so every later statement refers to the same set:

CREATE TEMP TABLE victims AS
SELECT metadata_id, entity_id FROM states_meta
WHERE entity_id IN ('sensor.foo', 'sensor.bar')
   OR entity_id GLOB 'sensor.p1_*';

SELECT * FROM victims;

Read that output carefully — a sloppy GLOB is the main way this goes wrong. Then see what you’re about to remove:

SELECT v.entity_id, COUNT(*) AS rows
FROM states s JOIN victims v ON s.metadata_id = v.metadata_id
GROUP BY v.entity_id ORDER BY rows DESC;

2. Clear dangling self-references

PRAGMA foreign_keys = OFF;

UPDATE states SET old_state_id = NULL
WHERE old_state_id IN (
  SELECT state_id FROM states WHERE metadata_id IN (SELECT metadata_id FROM victims)
);

If you’re deleting an entity’s entire history this is a no-op, because the chain only ever links rows of the same entity. Run it anyway — it’s cheap insurance, and mandatory if you ever add a last_updated_ts bound to the delete.

3. Delete the states, in batches

One DELETE of twenty million rows builds a rollback journal roughly the size of the data. Chunk it:

DELETE FROM states WHERE state_id IN (
  SELECT state_id FROM states
  WHERE metadata_id IN (SELECT metadata_id FROM victims)
  LIMIT 200000
);

Re-run until it reports 0 changes. From the shell you can loop it:

while [ "$(sqlite3 home-assistant_v2.db "SELECT changes();" ...)" ]; do ...; done

but honestly just pressing up-arrow-enter a few dozen times in the CLI is fine.

4. Reap the orphaned attributes

This is where most of your 10 GB actually lives, and nothing deletes it automatically:

DELETE FROM state_attributes
WHERE NOT EXISTS (
  SELECT 1 FROM states WHERE states.attributes_id = state_attributes.attributes_id
);

Without an index on states.attributes_id this degenerates into a full scan per row and will run for hours. Check and, if needed, add a throwaway index:

PRAGMA index_list('states');
CREATE INDEX IF NOT EXISTS tmp_ix_attr ON states(attributes_id);
-- run the DELETE above --
DROP INDEX tmp_ix_attr;

5. Long-term statistics

Separate tables, separate metadata table, and note that statistics.metadata_id points at statistics_meta.id — not at states_meta:

CREATE TEMP TABLE stat_victims AS
SELECT id FROM statistics_meta
WHERE statistic_id IN (SELECT entity_id FROM victims);

DELETE FROM statistics            WHERE metadata_id IN (SELECT id FROM stat_victims);
DELETE FROM statistics_short_term WHERE metadata_id IN (SELECT id FROM stat_victims);
DELETE FROM statistics_meta       WHERE id IN (SELECT id FROM stat_victims);

Only do this if you actually want the history gone. Statistics are compact (one row per hour and per 5 minutes) and they’re what feeds the Energy dashboard and long-range history graphs — for power and energy sensors you almost always want to keep them while dropping the raw states.

6. Metadata rows

DELETE FROM states_meta WHERE metadata_id IN (SELECT metadata_id FROM victims);

Safe: if the entity still exists in HA, recorder just allocates a fresh metadata_id on the next state change. Leave recorder_runs, schema_changes, migration_changes and statistics_runs alone.

7. Reclaim the space

PRAGMA integrity_check;
VACUUM;

VACUUM rewrites the whole file, so you need free disk space equal to the current size, and it will take a good while on 10 GB. Nothing shrinks before this step. Then restart HA and check the log for recorder errors on the first few state writes.


One thing worth weighing: recorder.purge_entities does steps 2–6 correctly while HA is running (keep_days: 0 on a list of entity_id or entity_globs), followed by recorder.purge with repack: true for the vacuum. It’s slower and it does not touch statistics, but it can’t leave you with an inconsistent database. The manual route is worth it mainly when you’re deleting most of a very large file and want it done in one pass — which, at 10 GB, you probably are.

I downloaded the DB to play with it locally and offline using DB Browser.
From 12.8 GB, after an VACUUM I get 12.5 GB.
After removing the most prominent spammers using the procedure suggested (and it took a LONG time for the final PRAGMA integrity_check; VACUUM;) I get 3.8 GB.

Maybe I could do the same on the server, after stopping HA and making a backup.

I’m not sure why, but after adding the spammy entities to the exclude list, and restarting HA, the purge and compaction worked. Now I’m at 3.9 GB.

The restart cleared the tmp files, and there was enough room on your drive to preform the operation.
You need a bigger drive…

I restarted Home assistant, not the computer. Also, there were 580 GB free in /config at the time the issue was presenting itself, so probably not the cause?

When you restart HQ then the old log.1 file is deleted and the current log is then renamed log.1.
Finally a new lognis created.
Log files can easily be several hundred megabytes or even some gigabytes large, so a restart will clear space.

A database that is not repackee will have spot in it that contains nothing, but still occupy bytes, because there are data later in the file that is still live.

Repacking the database file removes those spots with nothing in them, but it is done by making a new database and then write all the love data into that right after each other.
First when the new database file is completely written will the old one be deleted, so you might need up to the size of your database as free when you start the repacking.

Yes but it’s not much relevant here since I had 580 GB free

Yes before repacking I purged entities which I knew (thanks to the analysis of the sqlite database!) to be huge.

It would be nice to have a built-in tool to give stats about the space taken by the entities, with simple check boxes to exclude them from the recorder