View Categories

Combine Multiple SQLite Database Files Into One Table

27 min read

SQLITE FOLDER CONNECTOR

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.

One table per table name Opened read-only Declared types enforced Row traced to its file
\\backups\devices
kiosk-014.db kiosk-015.db kiosk-016.db + 197 more
one table per table name
SELECT _source_file,
       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 .db per 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 .sqlite file 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.

A folder of .db files
kiosk-014.db → events, sessions, config kiosk-015.db → events, sessions, config kiosk-016.db → events, sessions tomorrow’s file
Three SQL tables
data_events data_sessions data_config already included
kiosk-016 has no config table — it simply contributes no rows there.

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.

Every row still knows where it came from. Each row carries _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.

One query across every .db file in the folder
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 filePinned asWhy
Anything containing INTINTEGER, INT, BIGINT, SMALLINTBIGINTSQLite’s integer affinity rule, applied to the declaration.
Anything containing CHAR, CLOB or TEXTVARCHARText affinity. VARCHAR(20) and TEXT agree.
Anything containing REAL, FLOA or DOUBDOUBLEFloating point affinity.
Anything containing BLOBBLOBBinary 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, TIMESTAMPThe matching real typeRecognised by name.
DECIMAL or NUMERIC with no precisionNamed refusalChoosing a scale for you would be a guess about money.
No declared type at allNamed refusalThere 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.

A money column declared 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.

SituationWhat happens
Text stored in a column pinned as an integerThe file parks, naming the column and the rowid that holds it.
A non-text storage class in a column pinned as textThe 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 formThe file parks. Only unambiguous ISO shapes are accepted, so 01/02/2026 is never silently guessed at.
A NULL in a column pinned NOT NULLThe file parks rather than quietly relaxing the constraint.
A member table gained a column since the pinDrift. 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 pinDrift, 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 sawDrift, unless you deliberately restricted the connector to a list of tables.
A file cannot be opened as a SQLite database at allParked 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.

Which database files were held back, and why
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.

Nothing is uploaded, and nothing is written. The databases stay where they are. Parsed rows are cached encrypted on the same machine as the Network Agent, and only the rows a query returns leave your network. Queries must be a single 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.

Several settings are hidden until you tick “Show advanced options”. Hashing, the delete policy, event retention, the scan limits, the four ingest caps and manifest overrides all live behind that toggle. If a setting named here is not on screen, that is why.

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.

Tables — one member-table name per line
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 DECIMAL with no precision, a virtual or shadow table, a WITHOUT ROWID table. 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.

Include patterns — widen for other extensions, or narrow to a subset
**/*.db
**/*.sqlite
**/*.sqlite3
**/*.db3

**/kiosk-*.db       only the kiosk databases
2026/**/*.db        only this year's tree
An empty SQLite Folder connector is almost always the include pattern. If the first sync reports that no files matched the configured roots and patterns, check the extension before you check anything else — that message reads like a permissions or path problem and usually is not. Add the extension your software actually writes, save, and the sniff runs again.

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.

Manifest overrides
{
  "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_file or _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 a DECIMAL declared with no precision that refused, is settled with one retypes entry 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 retypes override, or leaving that table out of Tables — never a layout mode.
Shaping the connector is the intended workflow, not copying files out first. Because every database is opened read-only, you can point a connector straight at files an application is still using — which is precisely why Include patterns and Tables are the right tools here. Narrow the connector to the files and tables you want; there is no need to stage copies somewhere safe.

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.

  1. Install the agent from the download page if it is not already running.
  2. In the portal, add a data connector and choose SQLite Folder.
  3. Enter the folder path as the agent’s machine sees it. Include patterns arrives seeded with **/*.db and **/*.sqlite — add a line if your databases use another extension, or narrow it to something like **/kiosk-*.db.
  4. 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.
  5. 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.
  6. 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.

Use the path the agent sees. A drive letter mapped on your own desktop will not resolve on a server running the agent. Use a UNC path such as \\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? +
Install the Query Streams Network Agent on a machine that can see the folder, add a SQLite Folder connector, and give it the folder path plus a pattern such as **/*.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? +
They need to agree where they overlap. A file may be missing a whole member table or an individual column — those are pinned as nullable and simply read as 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? +
Because 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? +
No. Every member database is opened in SQLite’s read-only mode, so no write lock is ever taken and the application that owns the file is unaffected. Connection pooling is disabled so no handle lingers holding a file open between syncs. Queries themselves are restricted to a single 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? +
That file is parked with SQLite’s own reason for the refusal — typically that the database is busy — and the rest of the folder syncs normally. It is retried on a later pass. A single file being in use never fails the sync or wedges the connector, which is the whole point of parking rather than aborting.
Are views and virtual tables included? +
Views are not — only entries the file records as real tables are read, so a view is neither pinned nor queried. Virtual tables and their shadow tables are recorded as tables by SQLite and so get enumerated, but their columns usually have no declared type, which makes them a named refusal. Exclude them with the Tables setting and the rest of the folder pins normally. SQLite’s own internal tables are skipped automatically.
A file was parked and the message mentions a rowid. What does that mean? +
It means one stored value could not honestly be the pinned type — the classic case being text sitting in a column declared 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? +
Yes, and with application databases you usually should. The Tables setting takes the member-table names you want, one per line, and everything else in the files is left alone — no pinning, no reading. Naming a table that exists in none of the sampled files is a refusal rather than a silent no-op, so a typo tells you immediately.
How do I get SQLite data into Microsoft Excel or Google Sheets? +
Save a query against the folder, then run it from the Excel add-in or the Google Sheets add-on and the rows land in the sheet, refreshed whenever you ask. Because the connector combines every file, one saved query covers the whole fleet of databases rather than one file at a time — and because it is read-only, sharing the query never means sharing the files.
How many SQLite files can one connector combine? +
100,000 files by default, raisable to 2,000,000, across 8 folder levels by default and up to 64. Individual files are read up to 512 MB and 2,000,000 rows by default, with higher ceilings available, and the per-file row budget covers all of that file’s member tables together. Full details are in the File Set overview.

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.

Updated on August 27, 2026

Powered by BetterDocs