Keep the record count and total size in a totals table (closes #146)
check / check (push) Canceled after 0s

info counted the records and added up their sizes with a query that
reads every record, which takes minutes on a large database. The
schema now has a totals table of one row, kept current by three
triggers on files, in the transaction of each insert, delete and size
update. info, scan's load phase and the read phases of report and
trees read that row instead of counting over files. scan writes files
as before.

Model: opus-5-5
This commit is contained in:
2026-10-27 16:53:53 +00:00
committed by sneak
parent ee09cda355
commit 8b2efcecb7
7 changed files with 294 additions and 66 deletions
+60 -27
View File
@@ -393,6 +393,20 @@ All four subcommands operate on a single SQLite database file:
content BLOB NOT NULL -- raw 32-byte SHA-256; whole file under 100 MiB, else samples
) WITHOUT ROWID;
CREATE INDEX files_signature ON files (size, head, tail, content);
CREATE TABLE totals (
records INTEGER NOT NULL, -- number of records in files
size INTEGER NOT NULL -- sizes of all records in files added up
);
INSERT INTO totals (records, size) VALUES (0, 0);
CREATE TRIGGER totals_after_insert AFTER INSERT ON files BEGIN
UPDATE totals SET records = records + 1, size = size + NEW.size;
END;
CREATE TRIGGER totals_after_delete AFTER DELETE ON files BEGIN
UPDATE totals SET records = records - 1, size = size - OLD.size;
END;
CREATE TRIGGER totals_after_update AFTER UPDATE OF size ON files BEGIN
UPDATE totals SET size = size + NEW.size - OLD.size;
END;
```
Paths are stored as BLOBs because Unix paths are raw bytes, not guaranteed
@@ -406,6 +420,13 @@ All four subcommands operate on a single SQLite database file:
`files_signature` index lets `report` read the records in signature order
without sorting the table.
`totals` holds one row: the number of records in `files` and the sum of
their sizes. Its three triggers keep that row current. Each runs in the
transaction of the change to `files` that fires it, so the row and the
records always agree. `info` and the progress displays read the number of
records from this row. Counting them in `files` would read every record,
which takes minutes on a database of tens of millions.
### Duplicate detection
Two files are duplicates when they have the same size and the same three hashes.
@@ -688,14 +709,18 @@ Processing:
database state.
`report` runs in two phases, each with its own progress display on stderr (see
"Progress"): `open` opens the database; `read` counts the records, then reads
them in signature order, largest size first. That is the order of the
`files_signature` index read backwards, so SQLite sorts nothing and returns the
first record at once. The records sharing a signature come together, and so do
the records of one size, so `report` holds only the duplicate groups of the size
it is reading, and writes them, ordered by `first` path, once it reaches a
smaller size. Its memory holds at most the duplicate paths of one size at a
time, never every record.
"Progress"):
- `open` opens the database.
- `read` gets the number of records from `totals`, then reads the records in
signature order, largest size first.
Signature order, largest size first, is the `files_signature` index read
backwards, so SQLite sorts nothing and returns the first record at once. The
records sharing a signature come together, and so do the records of one size, so
`report` holds only the duplicate groups of the size it is reading, and writes
them, ordered by `first` path, once it reaches a smaller size. Its memory holds
at most the duplicate paths of one size at a time, never every record.
#### Report output format
@@ -769,10 +794,13 @@ Processing:
ascending. Output must be fully deterministic for a given input.
`trees` runs in four phases, each with its own progress display on stderr (see
"Progress"): `open` opens the database; `read` counts the records, then reads
them in path order, building the hierarchy and computing the digests as it goes;
`group` groups the directories by digest and picks the maximal groups; `write`
writes the groups to stdout.
"Progress"):
- `open` opens the database.
- `read` gets the number of records from `totals`, then reads the records in
path order, building the hierarchy and computing the digests as it goes.
- `group` groups the directories by digest and picks the maximal groups.
- `write` writes the groups to stdout.
#### Trees output format
@@ -823,11 +851,15 @@ size of recorded files 150000000000000 (136.4 TiB)
of all records, in bytes, then in human units.
`info` runs in two phases, each with its own progress display on stderr (see
"Progress"): `open` opens the database; `count` reads the schema version and
counts the records with one query, a single pass over the table, both in one
read-only transaction, so the numbers agree with each other while a scan is
committing. `info` writes its lines once the `count` display has ended, so on a
terminal they come after the displays.
"Progress"):
- `open` opens the database.
- `count` reads the schema version and the row of `totals` (see "Database").
Both reads run in one read-only transaction, so the numbers agree with each
other while a scan is committing.
`info` writes its lines once the `count` display has ended, so on a terminal
they come after the displays.
### Progress
@@ -843,8 +875,9 @@ progress, its elapsed time and its time remaining.
A scan has four phases (`[1/4] load`, `[2/4] walk`, `[3/4] hash`,
`[4/4] update`):
- `load` counts the records first, then reads them against that count. While the
count runs, the display says it is counting the records.
- `load` gets the number of records from `totals` first, then reads the records
against that number. Until it has the number, the display says it is counting
the records.
- `walk` has no exact total. Its estimate is the number of records the last scan
left under the operands, known once `load` is done and shown with a `~`. The
time remaining is unknown once the walk passes the estimate, and when there
@@ -857,8 +890,9 @@ A scan has four phases (`[1/4] load`, `[2/4] walk`, `[3/4] hash`,
- `open` has no total. While it opens the database, waiting on a busy one
included, the display says it is opening the database.
- `read` counts the records first, then reads them against that count. While the
count runs, the display says it is counting the records.
- `read` gets the number of records from `totals` first, then reads the records
against that number. Until it has the number, the display says it is counting
the records.
- `group` has the directories `read` found as its total, and `write` the
duplicate-tree groups.
@@ -866,17 +900,16 @@ A scan has four phases (`[1/4] load`, `[2/4] walk`, `[3/4] hash`,
- `open` has no total. While it opens the database, waiting on a busy one
included, the display says it is opening the database.
- `read` counts the records first, then reads them against that count, writing
the rows to stdout as it goes. While the count runs, the display says it is
counting the records.
- `read` gets the number of records from `totals` first, then reads the records
against that number, writing the rows to stdout as it goes. Until it has the
number, the display says it is counting the records.
`info` has two phases (`[1/2] open`, `[2/2] count`, see "`info` mode"):
- `open` has no total. While it opens the database, waiting on a busy one
included, the display says it is opening the database.
- `count` has no total either, because one query counts everything: the display
says it is counting the records until the query returns, and its time
remaining is unknown.
- `count` has no total either, because it reads one row: the display says it is
counting the records until it has the row, and its time remaining is unknown.
Each display is one line holding: