Combine Multiple SQLite Database Files Into One Table
Two hundred .db files, one query. Query Streams unions every file in the folder table by table, opens each database read-only so nothing can be disturbed, and keeps the file each row came from.
COUNT(*)
FROM data_events
GROUP BY _source_file;
Query Streams is a secure, real-time database integration platform whose SQLite Folder connector turns a whole folder of .db files into live SQL tables you can query from Microsoft Excel, Google Sheets, Airtable and your AI assistant. Learn more at QueryStreams.com and sign up for free to query your first folder of SQLite files in minutes.
Who ends up with a folder of SQLite files
Almost nobody sets out to build a folder of SQLite databases. You end up with one because SQLite is what software reaches for when it needs local storage, and because there is usually one copy per something. Per device, per site, per tenant, per session, per backup.
- Mobile app backups — one extracted
.dbper handset, per user or per support ticket. - Per-device and per-site databases — kiosks, tills, weighbridges and line controllers that each keep their own local store and drop a copy on a share overnight.
- IoT and telemetry captures — a sensor logger that rolls to a new
.sqlitefile every hour or every day. - Per-tenant application databases — a self-hosted product that gives every customer their own file.
- Exported browser, tooling and desktop-app databases — the history, cache and settings stores that half the software on your machine keeps in SQLite.
Each file is a perfectly good database on its own, and there are plenty of tools that will open one. The awkward question is the one that matters: I have two hundred of these and I want a single query across all of them. Attaching them by hand tops out quickly, and stitching the results together in a spreadsheet is the job you were trying to avoid.
A SQLite Folder connector answers it by treating the folder as the database. Nothing is merged into a new file, no ATTACH statements are written, and no file is copied anywhere. Query Streams reads each database where it sits and presents the folder as a set of ordinary SQL tables. Getting SQLite to Excel then stops being an export chore and becomes a query you refresh.
One table per member table, unioned across every file
This is the shape of the whole connector, and it is worth getting straight before anything else. A SQLite file is not one table, it is a small database containing several. So the union does not happen at the file level — it happens at the table-name level.
Every table found inside the files is called a member table. For each distinct member-table name, Query Streams pins one output table, and every file in the folder contributes its rows to it. If your two hundred kiosk databases each hold events, sessions and config, you get three tables, each spanning all two hundred files.
Output tables are named data_ followed by the member-table name, so a member table called events arrives as data_events. There is no Table name field on the SQLite card to change that: SQLite is a multi-table driver, so the names follow the member tables recorded in the pinned manifest rather than anything you type, and the data_ prefix is fixed. Member names are sanitised for use as identifiers, and if two different member tables would sanitise to the same output name, that is a named refusal rather than a silent collision — you rename one in the file, or exclude one with Tables.
A file that lacks one of the member tables is not an error. Its rows for that table simply do not exist, and the other tables sync as normal. Similarly, when a column exists in most files but not all of them, it is pinned as nullable and reads as NULL for the files that do not have it.
_source_file naming the database file it was read from, and a _row_id that is unique across the whole table and follows the order of the source table’s rowid. With one file per device or per tenant, _source_file is usually the most useful dimension in the table — it is the device, the site or the customer.
That combination is what makes cross-file questions ordinary SQL. Comparing one device against the fleet, or finding the tenant whose numbers look wrong, is a GROUP BY rather than an afternoon of opening files.
SELECT _source_file AS device,
COUNT(*) AS events,
MIN(occurred_at) AS first_seen,
MAX(occurred_at) AS last_seen
FROM data_events
WHERE occurred_at >= DATE '2026-08-01'
GROUP BY _source_file
ORDER BY events DESC;
Because the member tables come out as separate tables, they also join to each other — and to everything else. data_events can be joined to data_sessions, and both to the customer table in your PostgreSQL instance, in one read-only statement. The mechanics of that are the same for every folder type and are covered in the File Set overview.
Type affinity: the declared type is the pin
Here is the thing that makes SQLite different from every other folder type, and it is a genuine quirk of the engine rather than a Query Streams decision.
Most databases enforce column types. SQLite has type affinity instead. The type you write in CREATE TABLE expresses a preference, not a constraint: a column declared VARCHAR(20) can legally hold the integer 42, and a column declared INTEGER can legally hold the text 'oops'. SQLite will store both without complaint. Every value carries its own storage class — INTEGER, REAL, TEXT, BLOB or NULL — and the column’s declared type only nudges what SQLite tries to convert on the way in.
That leaves any tool reading a folder of SQLite files with an unattractive choice. It can trust the declared types and hope, it can sample the values and infer, or it can give up and call everything text. The last option is the one to be most suspicious of: once a price column is text, sums are performed lexicographically and 9.99 sorts above 10.00. It is a wrong answer that looks like a right one.
Query Streams takes the first route and then verifies it. The pinned table is built from the declared types, read out of each file’s own schema, mapped to real types using SQLite’s own affinity word rules — and then every single value is checked against that pin at read time. The declaration is the promise; the check is the enforcement.
| Declared in the file | Pinned as | Why |
|---|---|---|
Anything containing INT — INTEGER, INT, BIGINT, SMALLINT | BIGINT | SQLite’s integer affinity rule, applied to the declaration. |
Anything containing CHAR, CLOB or TEXT | VARCHAR | Text affinity. VARCHAR(20) and TEXT agree. |
Anything containing REAL, FLOA or DOUB | DOUBLE | Floating point affinity. |
Anything containing BLOB | BLOB | Binary stays binary. |
DECIMAL(18,2), NUMERIC(12,4) | DECIMAL(18,2), DECIMAL(12,4) | The precision and scale you declared are kept, so money stays exact. |
BOOLEAN, DATE, TIME, DATETIME, TIMESTAMP | The matching real type | Recognised by name. |
DECIMAL or NUMERIC with no precision | Named refusal | Choosing a scale for you would be a guess about money. |
| No declared type at all | Named refusal | There is nothing to pin. Declare a type, or exclude the table. |
The two refusals at the bottom of that table are deliberate. A precision-less DECIMAL and an untyped column both mean the file has not said enough for a safe answer to exist, and the connector says so by name — naming the file, the member table and the column — rather than picking something plausible. If the table in question is not one you care about, exclude it and the rest of the folder pins normally.
Declared types must also agree across the files. If amount is INTEGER in most databases and TEXT in one, that is a refusal that names both files and both declarations, not a majority vote. Equivalent declarations are treated as equal, though: INT and INTEGER both map to BIGINT, so a folder whose schemas were written by different hands over the years does not fail on cosmetic differences.
TEXT is still salvageable. Storing prices as text is a common SQLite habit, because text round-trips byte for byte. Where a column is retyped to an exact decimal, the stored string has to genuinely parse as a decimal — and if it does not, the file parks rather than being coerced. You never get a lexicographic sum dressed up as a total.
When a value does not honour the pin
Because affinity is weak, a column declared INTEGER can contain a string, and the schema will not have warned you. This is the case the connector exists to catch, and it is checked value by value as the rows are read, not sampled.
When a value cannot honestly be the pinned type, the file is parked: its rows stay out of the tables, the reason is recorded, and every other file in the folder keeps syncing. The recorded reason is specific — the file, the member table, the column, the rowid, the pinned type, and the offending value’s storage class with a short preview of the value itself. That is normally enough to fix the row without opening the database.
| Situation | What happens |
|---|---|
| Text stored in a column pinned as an integer | The file parks, naming the column and the rowid that holds it. |
| A non-text storage class in a column pinned as text | The file parks. This is the affinity surprise in reverse, and it is treated the same way. |
| A date or timestamp string that is not in ISO form | The file parks. Only unambiguous ISO shapes are accepted, so 01/02/2026 is never silently guessed at. |
A NULL in a column pinned NOT NULL | The file parks rather than quietly relaxing the constraint. |
| A member table gained a column since the pin | Drift. The new column is named, because a column nobody has agreed to is a change worth seeing. |
| A member table lost a column, or the whole table, since the pin | Drift, named the same way — a file that had it before and does not now has changed shape. |
| A new member table appears that the pin never saw | Drift, unless you deliberately restricted the connector to a list of tables. |
| A file cannot be opened as a SQLite database at all | Parked with the real reason. On the very first sync it is reported as skipped, and the remaining files still pin. |
All of a file’s member tables are written in a single transaction, which gives you a useful guarantee: a file is either fully in or fully out. A bad value in config rolls back that file’s events rows too, so you never end up with a device that is half present across your tables. Per-file row limits are also counted across the whole file rather than per table, so a file with three large member tables is measured as one file.
SELECT relative_path, detail, occurred_at FROM files_events WHERE event_type = 'drift' ORDER BY occurred_at DESC;
Parked files are remembered, so an unchanged one is not retried on every pass. Fix or replace the file and it is picked up on the next sync. The parking and drift machinery is shared by all File Set connectors and is described in full in the overview guide.
Which tables inside the file become tables outside it
Member tables are enumerated from each database’s own schema catalogue, and the rules are narrow on purpose.
- Real tables are included, in name order, along with the columns and declared types reported by the file itself.
- Views are not included. Only entries the file records as tables are read, so a view is neither pinned nor queried — if you need what a view produces, write the equivalent SQL against the underlying tables instead.
- SQLite’s own internal tables are skipped by name, so the sequence and statistics tables SQLite maintains for itself never turn up as data.
- Virtual tables and their shadow tables are still recorded as tables by SQLite, so they are enumerated. Their columns typically carry no declared type, which makes them one of the named refusals above — the fix is to exclude them.
- A table with no columns is ignored rather than pinned as an empty table.
The card’s one format-specific setting, Tables, is the answer to most of the above: give it the member tables you want and everything else in the files is left alone. It is also the cleaner way to work with a folder of application databases where you care about two tables out of forty. Naming a table that appears in none of the sampled files is itself a refusal, because an include-list entry that quietly does nothing is indistinguishable from one that works. Any option key the driver does not recognise is rejected outright for the same reason. How to set it, and the scenarios that call for it, are covered under Every SQLite Folder setting, and when to change it below.
WITHOUT ROWID tables cannot be read. Rows are streamed in rowid order, which is what makes _row_id stable and repeatable. A member table declared WITHOUT ROWID has no such column, so it parks with that as the stated reason instead of being skipped silently. Exclude it with Tables if the rest of the database is what you are after.
Read-only, because the file is probably still in use
All File Set connectors are read-only. For SQLite that guarantee carries more weight than anywhere else, because a .db file is not an export sitting inert on a share — it is a live database that another application may have open right now.
Each member file is therefore opened in SQLite’s own read-only mode. The connector never takes a write lock, so the application that owns the file carries on unaffected. Connection pooling is switched off as well, which matters specifically in the folder case: a pooled handle stays alive after use and would keep a customer’s live file locked between syncs, across potentially hundreds of files. Every file is opened, read and released.
Two related details are worth stating plainly, because SQLite has a reputation for leaving sidecar files behind. SQLite creates -wal and -shm companions to coordinate writers; the connector is not a writer, and it does not attempt to take over recovery of a database it cannot write to. And where such companions already exist in the folder, they are never mistaken for databases in their own right: file matching is anchored to the extension, so a pattern of **/*.db does not match store.db-wal.
If a file genuinely is mid-write, or locked, when the sync reaches it, that surfaces as a park carrying SQLite’s own reason for the refusal. The sync completes, the other files are read, and the busy one is retried later. One file being in use is never allowed to wedge the connector.
SELECT, WITH, PRAGMA, DESCRIBE or EXPLAIN statement — anything that could write is rejected before it runs.
Every SQLite Folder setting, and when to change it
The SQLite Folder card carries exactly one format-specific setting. Everything else on it — root folders, recursion and max depth, exclude patterns, symlinks, the filename parse pattern, folder tokens, the scan interval, content hashing, the delete policy, event retention, the scan limits and the four ingest caps — is shared by every folder type and is documented in full in the File Set connector guide. Two of those shared fields behave differently enough on SQLite to be worth a paragraph here, so they are covered below rather than left to the overview.
Tables
Default: empty, which means every table in the pinned manifest. To narrow it, put one member-table name per line, spelled exactly as it appears inside the SQLite file. Each name you list becomes its own pinned table, unioned across every database file in scope and carrying a _source_file column.
events sessions
The scenario you would set it in is an application database. A product’s own .db file is rarely two tidy tables. It is the two tables you want plus migration history, session state, caches and framework bookkeeping — sqlite_sequence, schema_migrations, django_session, cache and job tables, and whatever else the framework keeps for itself. Left to its own devices the connector pins all of them.
Listing only the tables you actually want does three things. It keeps the query-builder tree, Schema Intelligence and Nova AI pointed at real data rather than forty tables of plumbing; it cuts the work every sync has to do, because unlisted tables are never read; and it removes whole classes of refusal in one move, since a table that is never enumerated cannot object to anything. On a folder of forty-table application databases where you care about two, this is the difference between a usable connector and a tree nobody wants to scroll.
It is also the specific fix for two situations described earlier on this page:
- Two member tables sanitise to the same output name and the connector refuses the collision. List the one you want and leave the other out.
- One problem table is blocking the pin — an untyped column, a
DECIMALwith no precision, a virtual or shadow table, aWITHOUT ROWIDtable. Name the tables you do want and the rest of the folder pins normally, instead of one table you never asked about holding up the whole connector.
What happens if you get a name wrong: you are told, immediately and by name. An entry that appears in none of the sampled files is a named refusal, not a silent no-op — the message lists the entries it could not find and then lists the member tables it did find, so a typo, a plural or a case difference is usually obvious from the error alone. Nothing is pinned until you fix it. That is deliberate: an include entry quietly doing nothing looks exactly like one that is working.
Include patterns
Default on this card: two lines, **/*.db and **/*.sqlite. This is a shared setting, but it deserves proper attention here because SQLite is the one format with no single conventional extension. .db, .sqlite, .sqlite3 and .db3 are all in common use, and a great deal of software invents its own or uses none at all. The two seeded lines cover the two most common cases and nothing else.
If your databases end in anything else, you must add a line for them or the connector finds nothing. One glob per line, and your lines replace the seeded pair rather than adding to them, so keep the ones you still need.
**/*.db **/*.sqlite **/*.sqlite3 **/*.db3 **/kiosk-*.db only the kiosk databases 2026/**/*.db only this year's tree
Two things you do not have to worry about here. Matching is anchored to the extension, so **/*.db never picks up the -wal and -shm sidecars SQLite leaves beside a live database. And one folder type means one connector: widening the pattern to catch a stray CSV in the same folder does not work, because the two formats cannot share a pinned shape.
Manifest overrides (JSON)
Hidden behind Show advanced options. This is the escape hatch for a pinned shape that is right in structure but wrong in detail — a column you want renamed, a type you want to settle, a column you would rather not have at all. It is applied when the shape is pinned, so it becomes part of the table definition rather than something re-applied on every query.
{
"renames": { "qty": "quantity" },
"retypes": { "price": "DECIMAL(18,4)" },
"exclude": [ "internal_notes" ]
}
- renames — give a column a better name outside the file than it has inside it. You cannot rename onto
_source_fileor_row_id, and two columns cannot be renamed to the same thing. - retypes — force a column’s type. On SQLite this is the direct answer to the affinity problems described in Type affinity: the declared type is the pin above. A price column the file declares
TEXT, or aDECIMALdeclared with no precision that refused, is settled with oneretypesentry naming the type you actually want. A type set here is treated as a deliberate instruction, so it is applied without the strict per-value check that would otherwise park the file. - exclude — leave a column out of the pinned table entirely. Useful for a free-text notes column nobody should be querying, or a large blob you do not want moving on every sync.
Emptying the box clears your overrides and returns the tables to the shape that was sampled. Only plain type names with optional precision are accepted — DECIMAL(18,2), VARCHAR, BIGINT, DATE — and anything unusual is rejected when you save rather than at sync time.
Two fields this card deliberately does not have
Both exist on other folder cards, so their absence here is worth stating rather than leaving you to hunt for them.
- There is no Table name field. The single-table folders — CSV, Parquet and JSONL — ask for one because everything unions into one table and something has to name it. SQLite is a multi-table driver: the names come from the member tables recorded in the pinned manifest. What you control is which member tables get pinned, using Tables, not what the resulting tables are called.
- There is no Table layout option. The “one table per layout” escape hatch is offered on the Excel, CSV and JSONL cards only — the formats humans tend to assemble by hand. SQLite already derives its tables from the pinned manifest, so the option does not apply and is refused by name if it ever reaches the agent. When files genuinely disagree on a declared type, that is a refusal naming both files and both declarations; the fix is the file itself, a
retypesoverride, or leaving that table out of Tables — never a layout mode.
Setting up a SQLite Folder connector
You need a Query Streams account and the Network Agent installed on a machine that can see the folder — a server, a workstation with the share mapped, or your own laptop. SQLite Folder connectors require Network Agent 2.6 or newer.
- Install the agent from the download page if it is not already running.
- In the portal, add a data connector and choose SQLite Folder.
- Enter the folder path as the agent’s machine sees it. Include patterns arrives seeded with
**/*.dband**/*.sqlite— add a line if your databases use another extension, or narrow it to something like**/kiosk-*.db. - If the files hold more tables than you need, list the ones you want in Tables. This is also how you exclude an untyped or virtual table that would otherwise refuse.
- Save. The agent samples up to 64 files, pins one table per member table and reports what it found — including any file it could not open.
- Open the query builder, pick the connector and query the combined tables — or ask Nova AI to write the SQL for you.
From there the tables behave like any other connector’s. They can be read from the Microsoft Excel and Google Sheets add-ins, pushed on a schedule into Airtable, Smartsheet, Baserow, SeaTable or Anvil, queried by Claude, Cursor, ChatGPT or Grok through the Query Streams MCP server, or published as a REST endpoint.
\\fileserver\backups\devices, or a local path on the agent’s own machine, and check that the account the agent runs under can read the files.
Frequently Asked Questions
How do I merge multiple SQLite databases into one query? +
**/*.db. Nothing is merged into a new file and no ATTACH statements are needed — you get one SQL table per member-table name, with rows from every file in the folder, and files added later are included on the next sync.
Do all the .db files need identical schemas? +
NULL for that file. What they cannot do is disagree: a column declared INTEGER in one file and TEXT in another is a named refusal listing both files and both declarations, rather than a majority vote you would never see.
Why was my column with declared type NUMERIC refused? +
NUMERIC and DECIMAL with no precision do not say how many decimal places the values have, and inventing a scale for a column that is probably money would be a guess with financial consequences. Declare it as DECIMAL(18,2) in the file and the precision is honoured exactly, settle it with a retypes manifest override, or exclude that table with the Tables setting. Untyped columns — no declared type at all — are refused for the same reason.
Can Query Streams write to, lock or corrupt my SQLite files? +
SELECT, WITH, PRAGMA, DESCRIBE or EXPLAIN statement, and your files are never written, moved or renamed.
What happens if an application is writing to a file during the sync? +
Are views and virtual tables included? +
A file was parked and the message mentions a rowid. What does that mean? +
INTEGER, which SQLite’s affinity rules permit. The recorded reason names the file, the member table, the column, the rowid, the pinned type and the offending value, so you can go straight to the row. The file contributes no rows to any table until it is fixed; everything else keeps syncing.
Can I query only some of the tables inside the files? +
How do I get SQLite data into Microsoft Excel or Google Sheets? +
How many SQLite files can one connector combine? +
Get Started
Query every .db file at once
Point the agent at the folder and get one SQL table per table name, spanning every SQLite database in it — readable from Microsoft Excel, Google Sheets, Airtable or your AI assistant. Opened read-only, cached on your own machine, never uploaded.
Related guides: How to query a folder of files with SQL | Combine multiple CSV files into one table | Download the Query Streams Agent | All File Set Connector guides
Category: File Set Connectors
Tags: merge sqlite databases, combine sqlite files, query multiple sqlite files, sqlite folder, sqlite to excel, sqlite type affinity, sqlite connector
Meta Description: Query a folder of SQLite files as one table per table name. Read-only, nothing uploaded.

