From c79059455f0d65fe79db3ecd7182c95592bd77b2 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Tue, 20 Oct 2026 12:26:45 +0200 Subject: [PATCH 01/18] Draft: Direct tables --- architecture/client-architecture.mdx | 1 + .../advanced/performance-optimization.mdx | 28 +++++++++++++++++++ client-sdks/advanced/raw-tables.mdx | 8 ++---- client-sdks/cascading-delete.mdx | 5 ++-- client-sdks/high-performance-diffs.mdx | 2 ++ 5 files changed, 37 insertions(+), 7 deletions(-) diff --git a/architecture/client-architecture.mdx b/architecture/client-architecture.mdx index 7d425064d..f9b3ac8b9 100644 --- a/architecture/client-architecture.mdx +++ b/architecture/client-architecture.mdx @@ -36,6 +36,7 @@ The PowerSync Client SDK automatically maintains the following tables in the SQL | `ps_data__` | This contains the data for each "table", in JSON format. Since JSON is being used, this table's schema does not change when columns are added, removed or changed in the Sync Streams and client-side schema. | | `ps_data_local__
` | Same as the previous point, but for [local-only](/client-sdks/advanced/local-only-usage) tables. | | `
` (`VIEW`) | These are views on the above `ps_data` tables, with each defined column in the client-side schema extracted from the JSON. For example, a `description` text column would be `CAST(data ->> '$.description' as TEXT)`. | +| `
` (`TABLE`) | For [direct tables](/client-sdks/advanced/performance-optimization#direct-tables), PowerSync replaces the `ps_data__` storage table and view with a singl table matching your schema. | | `ps_untyped` | Any synced table that is not defined in the client-side schema is placed here. If the table is added to the schema at a later point, the data is then migrated to `ps_data__
`. | | `ps_oplog` | This is the operation history data as received from the [PowerSync Service](/architecture/powersync-service), grouped per bucket. | | `ps_crud` | The client-side upload queue (see [Writing Data](#writing-data-via-sqlite-database-and-upload-queue) below) | diff --git a/client-sdks/advanced/performance-optimization.mdx b/client-sdks/advanced/performance-optimization.mdx index b48b3e07f..11e2402e4 100644 --- a/client-sdks/advanced/performance-optimization.mdx +++ b/client-sdks/advanced/performance-optimization.mdx @@ -50,6 +50,34 @@ If this reveals it took too long for a read connection to become available, cons For contention on the write connection, note that the PowerSync client processes changes from the PowerSync Service in a single write transaction after they've been downloaded. Due to consistency requirements, this transaction cannot be split into multiple steps, and especially for a large initial sync it can hold a write lock for several seconds. +## Direct tables + + + **Availability** + + Direct tables described in this section were introduced in the following versions of our client SDKs: + + - **JavaScript** (Node: `todo`, React-Native: `todo`, Web: `todo`) + - **Dart**: Version `todo` of `package:powersync`. + - **Kotlin**: Version todo. + - **Swift**: Version todo. + - **Rust**: Version todo. + - **.NET**: Version todo + + +By default, PowerSync stores all data as [JSON rows](/architecture/client-architecture#client-side-schema-and-sqlite-database-structure): Synced rows are copied into local tables without modifications, and local updates modify an existing JSON object. This format is very highly flexible and avoids schema migrations, but comes with a performance cost: + +- Every column read forces SQLite to decode the entire JSON object, making queries slower than queries on "real" SQLite tables. +- JSON objects occupy more storage space than a native SQLite row, making databases slightly larger and the initial sync slower. + +[Raw tables](/client-sdks/advanced/raw-tables) fix this by making PowerSync into tables you define. With raw tables, you are responsible for creating and migrating them. +Direct tables are raw tables that PowerSync can manage for you, like those based on views. In supported SDKs, direct tables +can be enabled with a simple flag: + +TODO: snippets + +TODO: Limitations (no view name override, no high-performance diffs, eperimental) + ## Fixing Slow Queries If a query itself is expensive and takes a long time to run, several options can improve performance. Some of these require a restructuring of your app's schema. diff --git a/client-sdks/advanced/raw-tables.mdx b/client-sdks/advanced/raw-tables.mdx index 4a3a55e03..1f9a9b89f 100644 --- a/client-sdks/advanced/raw-tables.mdx +++ b/client-sdks/advanced/raw-tables.mdx @@ -4,10 +4,6 @@ description: "Access raw SQLite tables to bypass JSON view limitations for nativ sidebarTitle: "Raw SQLite Tables" --- - - Raw tables are not currently compatible with the [High Performance Diffs](/client-sdks/high-performance-diffs) feature available in our JavaScript SDKs. We have a [public proposal](https://docs.google.com/document/d/12bvZDJF2aaTOqkxHyo3W-TYGQTaS9FQFdu-MX2yOuqI/edit) open to discuss this addition - your feedback is welcome! - - By default, PowerSync uses a [JSON-based view system](/architecture/client-architecture#client-side-schema-and-sqlite-database-structure) where data is stored schemalessly in JSON format and then presented through SQLite views based on the client-side schema. Raw tables allow you to define native SQLite tables in the client-side schema, bypassing this. This eliminates overhead associated with extracting values from the JSON data and provides access to advanced SQLite features like foreign key constraints and custom indexes. @@ -32,7 +28,9 @@ Consider raw tables when you need: - **Indexes** - PowerSync's default schema has basic support for indexes on columns, while raw tables give you complete control to create indexes on expressions, use `GENERATED` columns, etc. - **Improved performance** for complex queries (e.g., `SELECT SUM(value) FROM transactions`) - raw tables more efficiently get these values directly from the SQLite column, instead of extracting the value from the JSON object on every row. - **Reduced storage overhead** - eliminate JSON object overhead for each row in `ps_data__
.data` column. -- **To manually create tables** - Sometimes you need full control over table creation, for example when implementing custom triggers. +- **To manually create tables** - Sometimes you need full control over table creation, for example when implementing custom triggers or if you use an existing library like [Room](/client-sdks/orms/kotlin/room) managing the schema for you. + +If you only need raw tables for their performance and reduced storage overhead, [direct tables](/client-sdks/advanced/performance-optimization#direct-tables) (experimental) may be a better option. These use the same architecture as a raw tables, but are fully managed by PowerSync (so no explicit migrations are necessary). **Advanced SQLite features** like `FOREIGN KEY` and `ON DELETE CASCADE` constraints need [special consideration](#using-foreign-keys). diff --git a/client-sdks/cascading-delete.mdx b/client-sdks/cascading-delete.mdx index 4d01e366e..d5fa08aaa 100644 --- a/client-sdks/cascading-delete.mdx +++ b/client-sdks/cascading-delete.mdx @@ -7,9 +7,9 @@ keywords: - "delete" --- -PowerSync [uses SQLite views](/architecture/client-architecture#client-side-schema-and-sqlite-database-structure) instead of standard tables, so SQLite features like foreign key constraints and cascading deletes are not available. +By default, PowerSync [uses SQLite views](/architecture/client-architecture#client-side-schema-and-sqlite-database-structure) instead of standard tables, so SQLite features like foreign key constraints and cascading deletes are not available. -There's no built-in support for cascading deletes on the client, but you can achieve this in two ways: +There's no built-in support for cascading deletes on the client, but you can achieve this in three ways: 1. Manual deletion in a transaction - Delete all related records in a single transaction (recommended for most cases) @@ -19,6 +19,7 @@ There's no built-in support for cascading deletes on the client, but you can ach You create triggers on the internal tables (not the views defined by the client schema), similar to what is done [here](https://github.com/powersync-ja/powersync-js/blob/e77b1abfbed91988de1f4c707c24855cd66b2219/demos/react-supabase-todolist/src/app/utils/fts_setup.ts#L50). +3. Raw tables - PowerSync can sync into existing tables defined by your app, and those tables can use [foreign key constraints](/client-sdks/advanced/raw-tables#using-foreign-keys). ## Example: Manual Transaction diff --git a/client-sdks/high-performance-diffs.mdx b/client-sdks/high-performance-diffs.mdx index b8e1265a2..6a6f9a1fc 100644 --- a/client-sdks/high-performance-diffs.mdx +++ b/client-sdks/high-performance-diffs.mdx @@ -22,6 +22,8 @@ While [basic/incremental watch queries](/client-sdks/watch-queries) enable react - API design and developer experience - Additional features or optimizations needed + The API does not support [raw](/client-sdks/advanced/raw-tables) or [direct](/client-sdks/advanced/performance-optimization#direct-tables) tables yet. We have a [public proposal](https://docs.google.com/document/d/12bvZDJF2aaTOqkxHyo3W-TYGQTaS9FQFdu-MX2yOuqI/edit) open to discuss this addition - your feedback is welcome! + Join our [Discord community](https://discord.gg/powersync) to share your experience and get help. From 0490b3f3f7172d5954756a552088a152dd534b2f Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Tue, 20 Oct 2026 15:59:17 +0200 Subject: [PATCH 02/18] AI feedback --- architecture/client-architecture.mdx | 2 +- .../advanced/performance-optimization.mdx | 69 +++++++++++++++---- client-sdks/advanced/raw-tables.mdx | 2 +- 3 files changed, 56 insertions(+), 17 deletions(-) diff --git a/architecture/client-architecture.mdx b/architecture/client-architecture.mdx index f9b3ac8b9..00e49a9f5 100644 --- a/architecture/client-architecture.mdx +++ b/architecture/client-architecture.mdx @@ -36,7 +36,7 @@ The PowerSync Client SDK automatically maintains the following tables in the SQL | `ps_data__
` | This contains the data for each "table", in JSON format. Since JSON is being used, this table's schema does not change when columns are added, removed or changed in the Sync Streams and client-side schema. | | `ps_data_local__
` | Same as the previous point, but for [local-only](/client-sdks/advanced/local-only-usage) tables. | | `
` (`VIEW`) | These are views on the above `ps_data` tables, with each defined column in the client-side schema extracted from the JSON. For example, a `description` text column would be `CAST(data ->> '$.description' as TEXT)`. | -| `
` (`TABLE`) | For [direct tables](/client-sdks/advanced/performance-optimization#direct-tables), PowerSync replaces the `ps_data__` storage table and view with a singl table matching your schema. | +| `
` (`TABLE`) | For [direct tables](/client-sdks/advanced/performance-optimization#direct-tables), PowerSync replaces the `ps_data__` storage table and view with a single table matching your schema. | | `ps_untyped` | Any synced table that is not defined in the client-side schema is placed here. If the table is added to the schema at a later point, the data is then migrated to `ps_data__
`. | | `ps_oplog` | This is the operation history data as received from the [PowerSync Service](/architecture/powersync-service), grouped per bucket. | | `ps_crud` | The client-side upload queue (see [Writing Data](#writing-data-via-sqlite-database-and-upload-queue) below) | diff --git a/client-sdks/advanced/performance-optimization.mdx b/client-sdks/advanced/performance-optimization.mdx index 11e2402e4..e4a23bfdf 100644 --- a/client-sdks/advanced/performance-optimization.mdx +++ b/client-sdks/advanced/performance-optimization.mdx @@ -50,33 +50,72 @@ If this reveals it took too long for a read connection to become available, cons For contention on the write connection, note that the PowerSync client processes changes from the PowerSync Service in a single write transaction after they've been downloaded. Due to consistency requirements, this transaction cannot be split into multiple steps, and especially for a large initial sync it can hold a write lock for several seconds. -## Direct tables +## Direct Tables **Availability** - Direct tables described in this section were introduced in the following versions of our client SDKs: + Direct tables are experimental. They were introduced in the following versions of our client SDKs: - - **JavaScript** (Node: `todo`, React-Native: `todo`, Web: `todo`) - - **Dart**: Version `todo` of `package:powersync`. - - **Kotlin**: Version todo. - - **Swift**: Version todo. - - **Rust**: Version todo. - - **.NET**: Version todo + - **JavaScript** (React Native: `2.3.4`, Web: `2.6.0`, Node.js: `1.2.1`) + - **Dart**: Version `2.6.0`. + - **Kotlin**: Version `1.16.0`. + - **Swift**: Version `1.17.0`. + - **.NET**: Not supported yet. + - **Rust**: Version `0.1.2`. -By default, PowerSync stores all data as [JSON rows](/architecture/client-architecture#client-side-schema-and-sqlite-database-structure): Synced rows are copied into local tables without modifications, and local updates modify an existing JSON object. This format is very highly flexible and avoids schema migrations, but comes with a performance cost: +By default, PowerSync stores all data as [JSON rows](/architecture/client-architecture#client-side-schema-and-sqlite-database-structure): synced rows are copied into local tables without modifications, and local updates modify an existing JSON object. This format is highly flexible and avoids schema migrations, but comes with a performance cost: - Every column read forces SQLite to decode the entire JSON object, making queries slower than queries on "real" SQLite tables. -- JSON objects occupy more storage space than a native SQLite row, making databases slightly larger and the initial sync slower. +- JSON objects occupy more storage space than a native SQLite row, making databases slightly larger and increasing writes during large sync downloads. -[Raw tables](/client-sdks/advanced/raw-tables) fix this by making PowerSync into tables you define. With raw tables, you are responsible for creating and migrating them. -Direct tables are raw tables that PowerSync can manage for you, like those based on views. In supported SDKs, direct tables -can be enabled with a simple flag: +[Raw tables](/client-sdks/advanced/raw-tables) fix this by letting PowerSync sync into actual tables you define. With raw tables, you are responsible for creating and migrating them. Direct tables are raw tables that PowerSync manages for you, like the default JSON-based tables. In supported SDKs, you enable direct tables with a flag on the table definition: -TODO: snippets + + +```typescript TypeScript +const schema = new Schema({ + users: new Table({ name: column.text }, { direct: true }) +}); +``` + +```dart Dart/Flutter +const schema = Schema([ + Table.direct('users', [Column.text('name')]) +]); +``` + +```kotlin Kotlin +val schema = Schema( + Table("users", listOf(Column.text("name")), direct = true) +) +``` + +```swift Swift +let schema = Schema( + Table(name: "users", columns: [.text("name")], direct: true) +) +``` + +```rust Rust +let mut schema = Schema::default(); +schema + .tables + .push(Table::create("users", vec![Column::text("name")], |tbl| { + tbl.direct = true + })); +``` + + + +Direct tables aren't based on views, so you can't set the `viewName` option on them. In the JavaScript SDKs, [High Performance Diffs](/client-sdks/high-performance-diffs) don't support direct tables yet. + +### Migrations + +To switch a table between the JSON-based format and a direct table, change the flag in your schema. PowerSync SDKs then migrate the existing data automatically. Schema changes on direct tables can take longer than on JSON-based tables: PowerSync must migrate the table itself, whereas JSON-based tables only need their views re-created. -TODO: Limitations (no view name override, no high-performance diffs, eperimental) +When you add or remove columns on an existing direct table, PowerSync detects the change and runs `ALTER TABLE` statements automatically. New columns contain `NULL` values until the next completed sync. ## Fixing Slow Queries diff --git a/client-sdks/advanced/raw-tables.mdx b/client-sdks/advanced/raw-tables.mdx index 1f9a9b89f..70cebf669 100644 --- a/client-sdks/advanced/raw-tables.mdx +++ b/client-sdks/advanced/raw-tables.mdx @@ -30,7 +30,7 @@ Consider raw tables when you need: - **Reduced storage overhead** - eliminate JSON object overhead for each row in `ps_data__
.data` column. - **To manually create tables** - Sometimes you need full control over table creation, for example when implementing custom triggers or if you use an existing library like [Room](/client-sdks/orms/kotlin/room) managing the schema for you. -If you only need raw tables for their performance and reduced storage overhead, [direct tables](/client-sdks/advanced/performance-optimization#direct-tables) (experimental) may be a better option. These use the same architecture as a raw tables, but are fully managed by PowerSync (so no explicit migrations are necessary). +If you only need raw tables for their performance and reduced storage overhead, [direct tables](/client-sdks/advanced/performance-optimization#direct-tables) (experimental) may be a better option. They use the same storage format as raw tables, but PowerSync creates and migrates them for you. **Advanced SQLite features** like `FOREIGN KEY` and `ON DELETE CASCADE` constraints need [special consideration](#using-foreign-keys). From 5088b6578f8648a0110b23bed8273accd186fd1e Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Tue, 27 Oct 2026 11:45:33 +0100 Subject: [PATCH 03/18] Add performance numbers --- client-sdks/advanced/performance-optimization.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client-sdks/advanced/performance-optimization.mdx b/client-sdks/advanced/performance-optimization.mdx index e4a23bfdf..66f4c0581 100644 --- a/client-sdks/advanced/performance-optimization.mdx +++ b/client-sdks/advanced/performance-optimization.mdx @@ -67,8 +67,8 @@ For contention on the write connection, note that the PowerSync client processes By default, PowerSync stores all data as [JSON rows](/architecture/client-architecture#client-side-schema-and-sqlite-database-structure): synced rows are copied into local tables without modifications, and local updates modify an existing JSON object. This format is highly flexible and avoids schema migrations, but comes with a performance cost: -- Every column read forces SQLite to decode the entire JSON object, making queries slower than queries on "real" SQLite tables. -- JSON objects occupy more storage space than a native SQLite row, making databases slightly larger and increasing writes during large sync downloads. +- Every column read forces SQLite to decode the entire JSON object, making queries slower than queries on "real" SQLite tables. In our tests, we've seen a 2x improvement for `SELECT *` queries, and a 5x improvement for aggregates, filters and ordering on columns without an index. +- JSON objects occupy more storage space than a native SQLite row, making databases slightly larger and increasing writes during large sync downloads. In our tests, direct tables decrease the file size by around 20%. [Raw tables](/client-sdks/advanced/raw-tables) fix this by letting PowerSync sync into actual tables you define. With raw tables, you are responsible for creating and migrating them. Direct tables are raw tables that PowerSync manages for you, like the default JSON-based tables. In supported SDKs, you enable direct tables with a flag on the table definition: From 26cc09d79396190edb288d226cdbeb89ecca6ed5 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Tue, 27 Oct 2026 12:08:46 +0100 Subject: [PATCH 04/18] Note query and migration time --- client-sdks/advanced/performance-optimization.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client-sdks/advanced/performance-optimization.mdx b/client-sdks/advanced/performance-optimization.mdx index 66f4c0581..78b21efb5 100644 --- a/client-sdks/advanced/performance-optimization.mdx +++ b/client-sdks/advanced/performance-optimization.mdx @@ -113,7 +113,7 @@ Direct tables aren't based on views, so you can't set the `viewName` option on t ### Migrations -To switch a table between the JSON-based format and a direct table, change the flag in your schema. PowerSync SDKs then migrate the existing data automatically. Schema changes on direct tables can take longer than on JSON-based tables: PowerSync must migrate the table itself, whereas JSON-based tables only need their views re-created. +To switch a table between the JSON-based format and a direct table, change the flag in your schema. PowerSync SDKs then migrate the existing data automatically. Schema changes on direct tables can take longer than on JSON-based tables: PowerSync must migrate the table itself. If you expect to run migrations on large tables, expect them to take a few seconds per million rows. JSON-based tables only need their views re-created, which is very instant regardless of the data size. When you add or remove columns on an existing direct table, PowerSync detects the change and runs `ALTER TABLE` statements automatically. New columns contain `NULL` values until the next completed sync. From 42aca50ce6b88090b4ff479339718a933e0a899f Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Tue, 27 Oct 2026 14:37:48 +0100 Subject: [PATCH 05/18] AI feedback --- client-sdks/advanced/performance-optimization.mdx | 4 ++-- client-sdks/advanced/raw-tables.mdx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client-sdks/advanced/performance-optimization.mdx b/client-sdks/advanced/performance-optimization.mdx index 78b21efb5..c8cb78a22 100644 --- a/client-sdks/advanced/performance-optimization.mdx +++ b/client-sdks/advanced/performance-optimization.mdx @@ -57,7 +57,7 @@ For contention on the write connection, note that the PowerSync client processes Direct tables are experimental. They were introduced in the following versions of our client SDKs: - - **JavaScript** (React Native: `2.3.4`, Web: `2.6.0`, Node.js: `1.2.1`) + - **JavaScript** (Web: `2.6.0`, React Native: `2.3.4`, Node.js: `1.2.1`) - **Dart**: Version `2.6.0`. - **Kotlin**: Version `1.16.0`. - **Swift**: Version `1.17.0`. @@ -113,7 +113,7 @@ Direct tables aren't based on views, so you can't set the `viewName` option on t ### Migrations -To switch a table between the JSON-based format and a direct table, change the flag in your schema. PowerSync SDKs then migrate the existing data automatically. Schema changes on direct tables can take longer than on JSON-based tables: PowerSync must migrate the table itself. If you expect to run migrations on large tables, expect them to take a few seconds per million rows. JSON-based tables only need their views re-created, which is very instant regardless of the data size. +To switch a table between the JSON-based format and a direct table, change the flag in your schema. PowerSync SDKs then migrate the existing data automatically. Schema changes on direct tables can take longer than on JSON-based tables: PowerSync must migrate the table itself. If you expect to run migrations on large tables, expect them to take a few seconds per million rows. JSON-based tables only need their views re-created, which is near-instant regardless of the data size. When you add or remove columns on an existing direct table, PowerSync detects the change and runs `ALTER TABLE` statements automatically. New columns contain `NULL` values until the next completed sync. diff --git a/client-sdks/advanced/raw-tables.mdx b/client-sdks/advanced/raw-tables.mdx index 70cebf669..6151b57b8 100644 --- a/client-sdks/advanced/raw-tables.mdx +++ b/client-sdks/advanced/raw-tables.mdx @@ -28,7 +28,7 @@ Consider raw tables when you need: - **Indexes** - PowerSync's default schema has basic support for indexes on columns, while raw tables give you complete control to create indexes on expressions, use `GENERATED` columns, etc. - **Improved performance** for complex queries (e.g., `SELECT SUM(value) FROM transactions`) - raw tables more efficiently get these values directly from the SQLite column, instead of extracting the value from the JSON object on every row. - **Reduced storage overhead** - eliminate JSON object overhead for each row in `ps_data__
.data` column. -- **To manually create tables** - Sometimes you need full control over table creation, for example when implementing custom triggers or if you use an existing library like [Room](/client-sdks/orms/kotlin/room) managing the schema for you. +- **To manually create tables** - Sometimes you need full control over table creation, for example when implementing custom triggers or if you use an existing library like [Room](/client-sdks/orms/kotlin/room) that manages the schema for you. If you only need raw tables for their performance and reduced storage overhead, [direct tables](/client-sdks/advanced/performance-optimization#direct-tables) (experimental) may be a better option. They use the same storage format as raw tables, but PowerSync creates and migrates them for you. From 73f0b14da245a9a106ebb1d1f554c05073b93cd1 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Wed, 28 Oct 2026 12:01:32 +0100 Subject: [PATCH 06/18] Move benchmarks to the end --- .../advanced/performance-optimization.mdx | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/client-sdks/advanced/performance-optimization.mdx b/client-sdks/advanced/performance-optimization.mdx index c8cb78a22..9f6e135c5 100644 --- a/client-sdks/advanced/performance-optimization.mdx +++ b/client-sdks/advanced/performance-optimization.mdx @@ -65,12 +65,11 @@ For contention on the write connection, note that the PowerSync client processes - **Rust**: Version `0.1.2`. -By default, PowerSync stores all data as [JSON rows](/architecture/client-architecture#client-side-schema-and-sqlite-database-structure): synced rows are copied into local tables without modifications, and local updates modify an existing JSON object. This format is highly flexible and avoids schema migrations, but comes with a performance cost: +PowerSync supports three types of table structures to sync data into: -- Every column read forces SQLite to decode the entire JSON object, making queries slower than queries on "real" SQLite tables. In our tests, we've seen a 2x improvement for `SELECT *` queries, and a 5x improvement for aggregates, filters and ordering on columns without an index. -- JSON objects occupy more storage space than a native SQLite row, making databases slightly larger and increasing writes during large sync downloads. In our tests, direct tables decrease the file size by around 20%. - -[Raw tables](/client-sdks/advanced/raw-tables) fix this by letting PowerSync sync into actual tables you define. With raw tables, you are responsible for creating and migrating them. Direct tables are raw tables that PowerSync manages for you, like the default JSON-based tables. In supported SDKs, you enable direct tables with a flag on the table definition: +1. Managed views, storing data as [JSON rows](/architecture/client-architecture#client-side-schema-and-sqlite-database-structure). Synced rows are copied into local tables without modifications, and local updates modify an existing JSON object. This format is highly flexible and avoids schema migrations, but comes with a performance cost because SQLite needs to decode JSON for each query. +2. [Raw tables](/client-sdks/advanced/raw-tables), allowing you to define tables to sync into. Raw tables allow faster queries, but make you responsible for creating and migrating them. +3. Managed tables, enabled by setting the `direct` flag on `Table` options. These match the query performance of raw tables while still allowing PowerSync SDKs to create and migrate them automatically. @@ -109,7 +108,12 @@ schema -Direct tables aren't based on views, so you can't set the `viewName` option on them. In the JavaScript SDKs, [High Performance Diffs](/client-sdks/high-performance-diffs) don't support direct tables yet. +In our testing, enabling the `direct` flag had significant performance benefits: + +- Not decoding JSON on reads improves query performance, we've seen a 2x improvement for `SELECT *` queries, and a 5x improvement for aggregates, filters and ordering on columns without an index. +- By avoiding the overhead of storing JSON, direct tables decrease the file size by around 20%. This depends on the data stored, the overhead is lower for large string values for example. + +Queries and mutating SQL statements work the same for managed views and managed tables. Direct tables aren't based on views, so you can't set the `viewName` option on them. In the JavaScript SDKs, [High Performance Diffs](/client-sdks/high-performance-diffs) don't support direct tables yet. ### Migrations From 70591a374760cd22eda84137aa7e93136bc28dab Mon Sep 17 00:00:00 2001 From: Benita Volkmann Date: Wed, 28 Oct 2026 13:50:58 +0200 Subject: [PATCH 07/18] Mark managed tables as alpha --- resources/feature-status.mdx | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/feature-status.mdx b/resources/feature-status.mdx index 8244c2c54..8bfdd85bf 100644 --- a/resources/feature-status.mdx +++ b/resources/feature-status.mdx @@ -69,6 +69,7 @@ Below is a summary of the current main PowerSync features and their release stat | **Client SDKs** | | | High Performance Diffs | Experimental | | Checkpoint Requests | Alpha | +| Managed Tables | Alpha | | Tauri SDK | Alpha | | TanStack Query | Alpha | | Rust SDK | Beta | From 1b538702923e27079c562ba87b2a81f42b2fd8d1 Mon Sep 17 00:00:00 2001 From: Benita Volkmann Date: Wed, 28 Oct 2026 13:51:29 +0200 Subject: [PATCH 08/18] Wording polish and rename to managed tables --- architecture/client-architecture.mdx | 6 +++--- client-sdks/advanced/raw-tables.mdx | 2 +- client-sdks/high-performance-diffs.mdx | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/architecture/client-architecture.mdx b/architecture/client-architecture.mdx index 00e49a9f5..d6173ae69 100644 --- a/architecture/client-architecture.mdx +++ b/architecture/client-architecture.mdx @@ -27,7 +27,7 @@ The app UI can also use [Live Queries / Watch Queries](/client-sdks/watch-querie When you implement the PowerSync Client SDK in your application, you need to define a [client-side schema](/intro/setup-guide#define-your-client-side-schema) with tables, columns and indexes that correspond to your [Sync Streams](/sync/streams/overview). You provide this schema when the PowerSync-managed SQLite database is [instantiated](/intro/setup-guide#instantiate-the-powersync-database). -The tables defined in your client-side schema are usable in SQL queries as if they were actual SQLite tables, while in reality they are created as _SQLite views_ based on the schemaless JSON data being synced (see [PowerSync Protocol](/architecture/powersync-protocol)). +By default, the tables defined in your client-side schema are usable in SQL queries as if they were actual SQLite tables, while in reality they are created as _SQLite views_ based on the schemaless JSON data being synced (see [PowerSync Protocol](/architecture/powersync-protocol)). The PowerSync Client SDK automatically maintains the following tables in the SQLite database: @@ -36,7 +36,7 @@ The PowerSync Client SDK automatically maintains the following tables in the SQL | `ps_data__
` | This contains the data for each "table", in JSON format. Since JSON is being used, this table's schema does not change when columns are added, removed or changed in the Sync Streams and client-side schema. | | `ps_data_local__
` | Same as the previous point, but for [local-only](/client-sdks/advanced/local-only-usage) tables. | | `
` (`VIEW`) | These are views on the above `ps_data` tables, with each defined column in the client-side schema extracted from the JSON. For example, a `description` text column would be `CAST(data ->> '$.description' as TEXT)`. | -| `
` (`TABLE`) | For [direct tables](/client-sdks/advanced/performance-optimization#direct-tables), PowerSync replaces the `ps_data__` storage table and view with a single table matching your schema. | +| `
` (`TABLE`) | For [managed tables](/client-sdks/advanced/performance-optimization#managed-tables), PowerSync replaces the `ps_data__` storage table and view with a single table matching your schema. | | `ps_untyped` | Any synced table that is not defined in the client-side schema is placed here. If the table is added to the schema at a later point, the data is then migrated to `ps_data__
`. | | `ps_oplog` | This is the operation history data as received from the [PowerSync Service](/architecture/powersync-service), grouped per bucket. | | `ps_crud` | The client-side upload queue (see [Writing Data](#writing-data-via-sqlite-database-and-upload-queue) below) | @@ -51,7 +51,7 @@ The copy of the row in `ps_oplog` may be newer than the one in `ps_data__
It is possible for different [buckets](/architecture/powersync-service#bucket-system) to include overlapping data (for example, if multiple buckets contain data from the same table). If rows with the same table and ID have been synced via multiple buckets, it may be present multiple times in `ps_oplog`, but only one will be preserved in the `ps_data__
` table (the one with the highest `op_id`). - **Raw Tables Instead of JSON-Backed SQLite Views**: If you run into limitations with the above JSON-based SQLite view system, check out the [Raw Tables feature](/client-sdks/advanced/raw-tables) which allows you to define and manage raw SQLite tables to work around some of the limitations of PowerSync's default JSON-backed SQLite views system. + **Alternatives to JSON-based views:** [Managed tables](/client-sdks/advanced/performance-optimization#managed-tables) (alpha) store data in SQLite columns for faster queries, and PowerSync still creates and migrates them. [Raw tables](/client-sdks/advanced/raw-tables) give you full control over the SQLite tables, for example to use foreign keys, and you create and migrate them yourself. To compare the options, see [Choosing a Table Type](/client-sdks/advanced/performance-optimization#choosing-a-table-type). ## Writing Data (via SQLite Database and Upload Queue) diff --git a/client-sdks/advanced/raw-tables.mdx b/client-sdks/advanced/raw-tables.mdx index 6151b57b8..f48a67cbc 100644 --- a/client-sdks/advanced/raw-tables.mdx +++ b/client-sdks/advanced/raw-tables.mdx @@ -30,7 +30,7 @@ Consider raw tables when you need: - **Reduced storage overhead** - eliminate JSON object overhead for each row in `ps_data__
.data` column. - **To manually create tables** - Sometimes you need full control over table creation, for example when implementing custom triggers or if you use an existing library like [Room](/client-sdks/orms/kotlin/room) that manages the schema for you. -If you only need raw tables for their performance and reduced storage overhead, [direct tables](/client-sdks/advanced/performance-optimization#direct-tables) (experimental) may be a better option. They use the same storage format as raw tables, but PowerSync creates and migrates them for you. +If you only need raw tables for faster queries and less storage, consider [managed tables](/client-sdks/advanced/performance-optimization#managed-tables) (alpha) instead. They also store data in SQLite columns, but PowerSync creates and migrates them for you. To compare the options, see [Choosing a Table Type](/client-sdks/advanced/performance-optimization#choosing-a-table-type). **Advanced SQLite features** like `FOREIGN KEY` and `ON DELETE CASCADE` constraints need [special consideration](#using-foreign-keys). diff --git a/client-sdks/high-performance-diffs.mdx b/client-sdks/high-performance-diffs.mdx index 6a6f9a1fc..dc0583827 100644 --- a/client-sdks/high-performance-diffs.mdx +++ b/client-sdks/high-performance-diffs.mdx @@ -22,7 +22,7 @@ While [basic/incremental watch queries](/client-sdks/watch-queries) enable react - API design and developer experience - Additional features or optimizations needed - The API does not support [raw](/client-sdks/advanced/raw-tables) or [direct](/client-sdks/advanced/performance-optimization#direct-tables) tables yet. We have a [public proposal](https://docs.google.com/document/d/12bvZDJF2aaTOqkxHyo3W-TYGQTaS9FQFdu-MX2yOuqI/edit) open to discuss this addition - your feedback is welcome! + The API does not support [raw tables](/client-sdks/advanced/raw-tables) or [managed tables](/client-sdks/advanced/performance-optimization#managed-tables) yet. We have a [public proposal](https://docs.google.com/document/d/12bvZDJF2aaTOqkxHyo3W-TYGQTaS9FQFdu-MX2yOuqI/edit) open to discuss this addition - your feedback is welcome! Join our [Discord community](https://discord.gg/powersync) to share your experience and get help. From aaddf731e29ec52550df8f9e353eec140e2a2edf Mon Sep 17 00:00:00 2001 From: Benita Volkmann Date: Wed, 28 Oct 2026 13:53:33 +0200 Subject: [PATCH 09/18] Reorganize managed tables under qeury perforamnce fixes; and wording polish --- .../advanced/performance-optimization.mdx | 143 ++++++++++-------- 1 file changed, 84 insertions(+), 59 deletions(-) diff --git a/client-sdks/advanced/performance-optimization.mdx b/client-sdks/advanced/performance-optimization.mdx index 9f6e135c5..935cd1d66 100644 --- a/client-sdks/advanced/performance-optimization.mdx +++ b/client-sdks/advanced/performance-optimization.mdx @@ -50,26 +50,95 @@ If this reveals it took too long for a read connection to become available, cons For contention on the write connection, note that the PowerSync client processes changes from the PowerSync Service in a single write transaction after they've been downloaded. Due to consistency requirements, this transaction cannot be split into multiple steps, and especially for a large initial sync it can hold a write lock for several seconds. -## Direct Tables +## Fixing Slow Queries + +If a query itself is expensive and takes a long time to run, several options can improve performance. Some of these require a restructuring of your app's schema. + +1. For auto-updating watched queries on frequently-changed tables, queries might run very often. Watched queries should typically be cheap to run, as they otherwise risk blocking SQLite connections for too long. For more expensive queries that still need to be watched, consider increasing their throttle to run them less often. +2. As an alternative to regular watched queries, use [High Performance Diffs](/client-sdks/high-performance-diffs), which use triggers internally to only report changed rows back to your app. +3. Where possible, filter on the `id` column of tables to reduce the number of rows read, which makes queries more efficient. When filtering on other columns of large tables, make sure these columns are covered by indexes declared in your app's schema. +4. By default, PowerSync tables are views over JSON data that extract columns by parsing from JSON each time a row is accessed. While SQLite has a cache for parsed JSON, this can still be inefficient for queries computing on columns. [Managed tables](#managed-tables) store data in SQLite columns instead, at the cost of slower schema changes. + +### Prepared Statement Cache + +For cheap statements that run frequently, the cost of preparing a statement (making SQLite parse the SQL text and come up with a query plan based on available indexes) can make up a substantial chunk of the total query runtime. + +In the JavaScript Web and Dart SDKs, enable a cache of prepared statements when opening the database: + + + +```typescript TypeScript +const db = new PowerSyncDatabase({ + schema, + database: { + dbFilename: 'my_database.db', + // Cache up to 64 prepared statements + preparedStatementsCache: 64, + }, +}); +``` + +```dart Dart +final db = PowerSyncDatabase( + schema: Schema([...]), + path: 'my_database.db', + sqliteOptions: SqliteOptions( + // Cache up to 64 prepared statements + preparedStatementCacheSize: 64, + ), +); +``` + + + +In both SDKs, each connection uses its own independent cache and the maximum size applies to those caches. When the cache is full, the least-recently-used statement is evicted. + +For more information, see the `preparedStatementsCache` option [for JavaScript](https://powersync-ja.github.io/powersync-js/web-sdk/globals#webspecificopenoptions) or [for Dart](https://pub.dev/documentation/powersync/latest/sqlite_async/SqliteOptions/preparedStatementCacheSize.html). + +{/* SDK doc comments and changelogs link to #direct-tables, the earlier name of this section. Keep this anchor. */} + + + +## Managed Tables **Availability** - Direct tables are experimental. They were introduced in the following versions of our client SDKs: + Managed tables are currently in [alpha](/resources/feature-status), so their API may change based on feedback. They were introduced in the following versions of our client SDKs: - - **JavaScript** (Web: `2.6.0`, React Native: `2.3.4`, Node.js: `1.2.1`) + - **JavaScript** (React Native: `2.3.4`, Web: `2.6.0`, Node.js: `1.2.1`, Capacitor: `0.9.5`) - **Dart**: Version `2.6.0`. - **Kotlin**: Version `1.16.0`. - **Swift**: Version `1.17.0`. - **.NET**: Not supported yet. - - **Rust**: Version `0.1.2`. + - **Rust**: Version `0.2.0`. -PowerSync supports three types of table structures to sync data into: +By default, each table in your client-side schema is a **managed view**. PowerSync stores synced rows as [JSON in an internal table](/architecture/client-architecture#client-side-schema-and-sqlite-database-structure) and creates a SQLite view that extracts your columns from it. Queries on the view decode the JSON of every row they read. + +A **managed table** stores rows in a SQLite table with a column for each column in your schema, so queries read values directly. You query and write to it with the same SQL statements as a managed view. PowerSync still creates and migrates the table, and it still adds local writes to the upload queue. + +In our tests on a table with 1 million rows, `SELECT *` queries were about 2x faster on a managed table. Filtering, sorting or aggregating on a column without an index was about 6x faster. The database file was about 20% smaller. How much storage you save depends on your data. Rows with large text values save less, because the JSON overhead is a smaller part of each row. + +These numbers come from simulated benchmarks, so results in your app depend on your data and queries. If you try managed tables, let us know how they perform in your app. -1. Managed views, storing data as [JSON rows](/architecture/client-architecture#client-side-schema-and-sqlite-database-structure). Synced rows are copied into local tables without modifications, and local updates modify an existing JSON object. This format is highly flexible and avoids schema migrations, but comes with a performance cost because SQLite needs to decode JSON for each query. -2. [Raw tables](/client-sdks/advanced/raw-tables), allowing you to define tables to sync into. Raw tables allow faster queries, but make you responsible for creating and migrating them. -3. Managed tables, enabled by setting the `direct` flag on `Table` options. These match the query performance of raw tables while still allowing PowerSync SDKs to create and migrate them automatically. +### Choosing a Table Type + +The table types differ in two ways: who creates and migrates the table, and whether queries read JSON or SQLite columns. + +| | Managed views (default) | Managed tables | Raw tables | +| --- | --- | --- | --- | +| Created and migrated by | PowerSync | PowerSync | You | +| Queries read | JSON, through a view | SQLite columns | SQLite columns | +| Schema changes | Near-instant | Take longer as the table grows | Migrations you write | + +- **Managed views:** Use these by default: schema changes are near-instant, because PowerSync only re-creates the views. Queries that read many rows of a large table can be slower than on a managed table. +- **Managed tables (alpha):** Switch a table to a managed table if its queries are measurably too slow. This is typically a large table that you filter, sort or aggregate on columns without an index. You set this per table, so the other tables can stay managed views. Schema changes on a managed table take longer as the table grows. +- **Raw tables:** Use [raw tables](/client-sdks/advanced/raw-tables) when you want full control over the SQLite table, for example to add foreign keys, custom triggers or expression indexes, or to use a library such as [Room](/client-sdks/orms/kotlin/room) that manages the schema. You then create and migrate the table yourself. + +### Enabling Managed Tables + +Set the `direct` flag on the table definition. The flag name refers to queries reading the table directly, with no view in between. @@ -108,60 +177,16 @@ schema -In our testing, enabling the `direct` flag had significant performance benefits: - -- Not decoding JSON on reads improves query performance, we've seen a 2x improvement for `SELECT *` queries, and a 5x improvement for aggregates, filters and ordering on columns without an index. -- By avoiding the overhead of storing JSON, direct tables decrease the file size by around 20%. This depends on the data stored, the overhead is lower for large string values for example. +Before you switch a table, check how managed tables differ from managed views: -Queries and mutating SQL statements work the same for managed views and managed tables. Direct tables aren't based on views, so you can't set the `viewName` option on them. In the JavaScript SDKs, [High Performance Diffs](/client-sdks/high-performance-diffs) don't support direct tables yet. +- **Column types:** Managed views cast each value to the column type in your schema. Managed tables store values with the type they were synced or written with, so a value synced as text into an `integer` column stays text. Filters and sorting on that column can then return different results than on a managed view. +- **View names:** Managed tables aren't views, so you can't set the `viewName` option. If you use `viewName` to [move data from a local-only table to a synced table](/client-sdks/advanced/local-only-usage#moving-data-to-synced-tables), keep that table as a managed view. +- **High Performance Diffs:** In the JavaScript SDKs, [High Performance Diffs](/client-sdks/high-performance-diffs) don't support managed tables yet. Keep tables that you watch with High Performance Diffs as managed views. ### Migrations -To switch a table between the JSON-based format and a direct table, change the flag in your schema. PowerSync SDKs then migrate the existing data automatically. Schema changes on direct tables can take longer than on JSON-based tables: PowerSync must migrate the table itself. If you expect to run migrations on large tables, expect them to take a few seconds per million rows. JSON-based tables only need their views re-created, which is near-instant regardless of the data size. +To switch a table between a managed view and a managed table, change the `direct` flag. PowerSync moves the existing rows when it applies the new schema, without downloading them again. -When you add or remove columns on an existing direct table, PowerSync detects the change and runs `ALTER TABLE` statements automatically. New columns contain `NULL` values until the next completed sync. +When you add or remove columns, PowerSync runs the `ALTER TABLE` statements for you. These changes process every row in the table, whereas a managed view only needs its view re-created. In our tests on a table with 1 million rows, switching between a managed view and a managed table took a few seconds. Adding or dropping a column took up to about a second. On a managed view, both are near-instant. -## Fixing Slow Queries - -If a query itself is expensive and takes a long time to run, several options can improve performance. Some of these require a restructuring of your app's schema. - -1. For auto-updating watched queries on frequently-changed tables, queries might run very often. Watched queries should typically be cheap to run, as they otherwise risk blocking SQLite connections for too long. For more expensive queries that still need to be watched, consider increasing their throttle to run them less often. -2. As an alternative to regular watched queries, use [High Performance Diffs](/client-sdks/high-performance-diffs), which use triggers internally to only report changed rows back to your app. -3. Where possible, filter on the `id` column of tables to reduce the number of rows read, which makes queries more efficient. When filtering on other columns of large tables, make sure these columns are covered by indexes declared in your app's schema. -4. PowerSync tables are views over JSON data that extract columns by parsing from JSON each time a row is accessed. While SQLite has a cache for parsed JSON, this can still be inefficient for queries computing on columns. [Raw Tables](/client-sdks/advanced/raw-tables) let you use plain SQLite tables instead, which are faster to query but require special consideration for migrations. - -### Prepared Statement Cache - -For cheap statements that run frequently, the cost of preparing a statement (making SQLite parse the SQL text and come up with a query plan based on available indexes) can make up a substantial chunk of the total query runtime. - -In the JavaScript Web and Dart SDKs, enable a cache of prepared statements when opening the database: - - - -```typescript TypeScript -const db = new PowerSyncDatabase({ - schema, - database: { - dbFilename: 'my_database.db', - // Cache up to 64 prepared statements - preparedStatementsCache: 64, - }, -}); -``` - -```dart Dart -final db = PowerSyncDatabase( - schema: Schema([...]), - path: 'my_database.db', - sqliteOptions: SqliteOptions( - // Cache up to 64 prepared statements - preparedStatementCacheSize: 64, - ), -); -``` - - - -In both SDKs, each connection uses its own independent cache and the maximum size applies to those caches. When the cache is full, the least-recently-used statement is evicted. - -For more information, see the `preparedStatementsCache` option [for JavaScript](https://powersync-ja.github.io/powersync-js/web-sdk/globals#webspecificopenoptions) or [for Dart](https://pub.dev/documentation/powersync/latest/sqlite_async/SqliteOptions/preparedStatementCacheSize.html). +A new column on a managed table is `NULL` until the next completed sync. On a device that is offline after an app update, the column stays `NULL` until the device reconnects. After the sync, PowerSync fills the column from synced data that is already on the device, so nothing downloads again. This copy adds to the time that adding a column takes. From 86b2b5637c05f7ab2d49d36ac4e65feb928d85db Mon Sep 17 00:00:00 2001 From: Benita Volkmann Date: Wed, 28 Oct 2026 14:07:10 +0200 Subject: [PATCH 10/18] Also mention prepared statement cache in the lsit of options --- client-sdks/advanced/performance-optimization.mdx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/client-sdks/advanced/performance-optimization.mdx b/client-sdks/advanced/performance-optimization.mdx index 935cd1d66..1a3fdbe58 100644 --- a/client-sdks/advanced/performance-optimization.mdx +++ b/client-sdks/advanced/performance-optimization.mdx @@ -1,5 +1,5 @@ --- -title: "SQLite Performance Optimization" +title: "SQLite Performance Optimizations" description: "Optimize query performance in PowerSync client SDKs" --- @@ -54,10 +54,11 @@ For contention on the write connection, note that the PowerSync client processes If a query itself is expensive and takes a long time to run, several options can improve performance. Some of these require a restructuring of your app's schema. -1. For auto-updating watched queries on frequently-changed tables, queries might run very often. Watched queries should typically be cheap to run, as they otherwise risk blocking SQLite connections for too long. For more expensive queries that still need to be watched, consider increasing their throttle to run them less often. -2. As an alternative to regular watched queries, use [High Performance Diffs](/client-sdks/high-performance-diffs), which use triggers internally to only report changed rows back to your app. -3. Where possible, filter on the `id` column of tables to reduce the number of rows read, which makes queries more efficient. When filtering on other columns of large tables, make sure these columns are covered by indexes declared in your app's schema. +1. Where possible, filter on the `id` column of tables to reduce the number of rows read, which makes queries more efficient. When filtering on other columns of large tables, make sure these columns are covered by indexes declared in your app's schema. +2. For auto-updating watched queries on frequently-changed tables, queries might run very often. Watched queries should typically be cheap to run, as they otherwise risk blocking SQLite connections for too long. For more expensive queries that still need to be watched, consider increasing their throttle to run them less often. +3. For cheap statements that run frequently, preparing each statement can take up a large part of the total query time. In the JavaScript Web and Dart SDKs, enable a [prepared statement cache](#prepared-statement-cache). 4. By default, PowerSync tables are views over JSON data that extract columns by parsing from JSON each time a row is accessed. While SQLite has a cache for parsed JSON, this can still be inefficient for queries computing on columns. [Managed tables](#managed-tables) store data in SQLite columns instead, at the cost of slower schema changes. +5. In the JavaScript SDKs, you can replace regular watched queries with [High Performance Diffs](/client-sdks/high-performance-diffs). These use triggers internally to report only changed rows back to your app. ### Prepared Statement Cache From 13512483b4422bb6a51a0b35a13b20feca2fe842 Mon Sep 17 00:00:00 2001 From: Benita Volkmann Date: Wed, 28 Oct 2026 14:19:37 +0200 Subject: [PATCH 11/18] Swap managed tables and cached statements --- client-sdks/advanced/performance-optimization.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client-sdks/advanced/performance-optimization.mdx b/client-sdks/advanced/performance-optimization.mdx index 1a3fdbe58..fdcd80cfd 100644 --- a/client-sdks/advanced/performance-optimization.mdx +++ b/client-sdks/advanced/performance-optimization.mdx @@ -56,8 +56,8 @@ If a query itself is expensive and takes a long time to run, several options can 1. Where possible, filter on the `id` column of tables to reduce the number of rows read, which makes queries more efficient. When filtering on other columns of large tables, make sure these columns are covered by indexes declared in your app's schema. 2. For auto-updating watched queries on frequently-changed tables, queries might run very often. Watched queries should typically be cheap to run, as they otherwise risk blocking SQLite connections for too long. For more expensive queries that still need to be watched, consider increasing their throttle to run them less often. -3. For cheap statements that run frequently, preparing each statement can take up a large part of the total query time. In the JavaScript Web and Dart SDKs, enable a [prepared statement cache](#prepared-statement-cache). -4. By default, PowerSync tables are views over JSON data that extract columns by parsing from JSON each time a row is accessed. While SQLite has a cache for parsed JSON, this can still be inefficient for queries computing on columns. [Managed tables](#managed-tables) store data in SQLite columns instead, at the cost of slower schema changes. +3. By default, PowerSync tables are views over JSON data that extract columns by parsing from JSON each time a row is accessed. While SQLite has a cache for parsed JSON, this can still be inefficient for queries computing on columns. [Managed tables](#managed-tables) store data in SQLite columns instead, at the cost of slower schema changes. +4. If your app runs the same quick queries many times, SQLite spends part of each run reading the SQL and planning the query again. In the JavaScript Web and Dart SDKs, a [prepared statement cache](#prepared-statement-cache) keeps recently planned queries so that SQLite can reuse them. 5. In the JavaScript SDKs, you can replace regular watched queries with [High Performance Diffs](/client-sdks/high-performance-diffs). These use triggers internally to report only changed rows back to your app. ### Prepared Statement Cache From 3fd9030d26dee25486bd09caf5b099ed0dfd833f Mon Sep 17 00:00:00 2001 From: Benita Volkmann Date: Wed, 28 Oct 2026 15:46:17 +0200 Subject: [PATCH 12/18] Make tradeoffs more explicit --- architecture/client-architecture.mdx | 2 +- client-sdks/advanced/performance-optimization.mdx | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/architecture/client-architecture.mdx b/architecture/client-architecture.mdx index d6173ae69..f14a8808a 100644 --- a/architecture/client-architecture.mdx +++ b/architecture/client-architecture.mdx @@ -51,7 +51,7 @@ The copy of the row in `ps_oplog` may be newer than the one in `ps_data__
It is possible for different [buckets](/architecture/powersync-service#bucket-system) to include overlapping data (for example, if multiple buckets contain data from the same table). If rows with the same table and ID have been synced via multiple buckets, it may be present multiple times in `ps_oplog`, but only one will be preserved in the `ps_data__
` table (the one with the highest `op_id`). - **Alternatives to JSON-based views:** [Managed tables](/client-sdks/advanced/performance-optimization#managed-tables) (alpha) store data in SQLite columns for faster queries, and PowerSync still creates and migrates them. [Raw tables](/client-sdks/advanced/raw-tables) give you full control over the SQLite tables, for example to use foreign keys, and you create and migrate them yourself. To compare the options, see [Choosing a Table Type](/client-sdks/advanced/performance-optimization#choosing-a-table-type). + **Alternatives to JSON-based views:** [Managed tables](/client-sdks/advanced/performance-optimization#managed-tables) (alpha) store data in SQLite columns for faster queries, and PowerSync still creates and migrates them. [Raw tables](/client-sdks/advanced/raw-tables) give you full control over the SQLite tables, for example to use foreign keys, but you have to create and migrate them yourself. To compare the options, see [Choosing a Table Type](/client-sdks/advanced/performance-optimization#choosing-a-table-type). ## Writing Data (via SQLite Database and Upload Queue) diff --git a/client-sdks/advanced/performance-optimization.mdx b/client-sdks/advanced/performance-optimization.mdx index fdcd80cfd..03a84b6a0 100644 --- a/client-sdks/advanced/performance-optimization.mdx +++ b/client-sdks/advanced/performance-optimization.mdx @@ -134,8 +134,8 @@ The table types differ in two ways: who creates and migrates the table, and whet | Schema changes | Near-instant | Take longer as the table grows | Migrations you write | - **Managed views:** Use these by default: schema changes are near-instant, because PowerSync only re-creates the views. Queries that read many rows of a large table can be slower than on a managed table. -- **Managed tables (alpha):** Switch a table to a managed table if its queries are measurably too slow. This is typically a large table that you filter, sort or aggregate on columns without an index. You set this per table, so the other tables can stay managed views. Schema changes on a managed table take longer as the table grows. -- **Raw tables:** Use [raw tables](/client-sdks/advanced/raw-tables) when you want full control over the SQLite table, for example to add foreign keys, custom triggers or expression indexes, or to use a library such as [Room](/client-sdks/orms/kotlin/room) that manages the schema. You then create and migrate the table yourself. +- **Managed tables (alpha):** Switch a table to a managed table if its queries are measurably too slow. This is typically a large table that you filter, sort or aggregate on columns without an index. You set this per table, so the other tables can stay managed views. The trade-off is that schema changes on a managed table take longer as the table grows. +- **Raw tables:** Use [raw tables](/client-sdks/advanced/raw-tables) when you want full control over the SQLite table, for example to add foreign keys, custom triggers or expression indexes, or to use a library such as [Room](/client-sdks/orms/kotlin/room) that manages the schema. The trade-off is that you have to create and migrate the table yourself. ### Enabling Managed Tables @@ -180,7 +180,7 @@ schema Before you switch a table, check how managed tables differ from managed views: -- **Column types:** Managed views cast each value to the column type in your schema. Managed tables store values with the type they were synced or written with, so a value synced as text into an `integer` column stays text. Filters and sorting on that column can then return different results than on a managed view. +- **Column types:** Managed views cast each value to the column type in your schema, both when the view reads from the JSON and when its triggers write to it. Managed tables have no view or casting triggers, so they store values with the type they were synced or written with, so a value synced as text into an `integer` column stays text. Filters and sorting on that column can then return different results than on a managed view. - **View names:** Managed tables aren't views, so you can't set the `viewName` option. If you use `viewName` to [move data from a local-only table to a synced table](/client-sdks/advanced/local-only-usage#moving-data-to-synced-tables), keep that table as a managed view. - **High Performance Diffs:** In the JavaScript SDKs, [High Performance Diffs](/client-sdks/high-performance-diffs) don't support managed tables yet. Keep tables that you watch with High Performance Diffs as managed views. @@ -190,4 +190,4 @@ To switch a table between a managed view and a managed table, change the `direct When you add or remove columns, PowerSync runs the `ALTER TABLE` statements for you. These changes process every row in the table, whereas a managed view only needs its view re-created. In our tests on a table with 1 million rows, switching between a managed view and a managed table took a few seconds. Adding or dropping a column took up to about a second. On a managed view, both are near-instant. -A new column on a managed table is `NULL` until the next completed sync. On a device that is offline after an app update, the column stays `NULL` until the device reconnects. After the sync, PowerSync fills the column from synced data that is already on the device, so nothing downloads again. This copy adds to the time that adding a column takes. +If an app update adds an already-synced column to your client schema, a managed view shows its values immediately, even offline. A managed table shows `NULL` in that column until the next completed sync, so offline devices see `NULL` until they reconnect. New columns are usually already synced, because the recommended order for [additive schema changes](/maintenance-ops/deploying-schema-changes) publishes the app last. After the sync, PowerSync fills the column from synced data that is already on the device, so nothing downloads again. This copy adds to the time that adding a column takes. From d4184a86cfc10dbe73fcb71ba22af1fc630e615e Mon Sep 17 00:00:00 2001 From: Benita Volkmann Date: Wed, 28 Oct 2026 16:12:32 +0200 Subject: [PATCH 13/18] Additional clarification and polish --- .../advanced/performance-optimization.mdx | 16 +++++++++++----- client-sdks/advanced/raw-tables.mdx | 2 +- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/client-sdks/advanced/performance-optimization.mdx b/client-sdks/advanced/performance-optimization.mdx index 03a84b6a0..1ea5308d7 100644 --- a/client-sdks/advanced/performance-optimization.mdx +++ b/client-sdks/advanced/performance-optimization.mdx @@ -117,15 +117,21 @@ For more information, see the `preparedStatementsCache` option [for JavaScript]( By default, each table in your client-side schema is a **managed view**. PowerSync stores synced rows as [JSON in an internal table](/architecture/client-architecture#client-side-schema-and-sqlite-database-structure) and creates a SQLite view that extracts your columns from it. Queries on the view decode the JSON of every row they read. -A **managed table** stores rows in a SQLite table with a column for each column in your schema, so queries read values directly. You query and write to it with the same SQL statements as a managed view. PowerSync still creates and migrates the table, and it still adds local writes to the upload queue. +Queries on a **managed table** run faster than on a managed view. A managed table stores rows in a SQLite table with a column for each column in your schema, so queries read values directly instead of decoding JSON. You query and write to it with the same SQL statements as a managed view. PowerSync still creates and migrates the table, and it still adds local writes to the upload queue. -In our tests on a table with 1 million rows, `SELECT *` queries were about 2x faster on a managed table. Filtering, sorting or aggregating on a column without an index was about 6x faster. The database file was about 20% smaller. How much storage you save depends on your data. Rows with large text values save less, because the JSON overhead is a smaller part of each row. + + **Benchmarks** -These numbers come from simulated benchmarks, so results in your app depend on your data and queries. If you try managed tables, let us know how they perform in your app. + In our tests on a table with 1 million rows, `SELECT *` queries were about 2x faster on a managed table. Filtering, sorting or aggregating on a column without an index was about 6x faster. + + These numbers come from simulated benchmarks, so results in your app depend on your data and queries. If you try managed tables, let us know how they perform in your app. + + Managed tables can also make the database file slightly smaller, because rows don't repeat the column names that JSON stores. How much depends on your data. For example, rows with large text values save less. + ### Choosing a Table Type -The table types differ in two ways: who creates and migrates the table, and whether queries read JSON or SQLite columns. +Start with managed views. Change a table's type only if you hit a limit: its queries are too slow, or you need SQLite features that the client-side schema can't define. You choose the type for each table, so one schema can mix all three. | | Managed views (default) | Managed tables | Raw tables | | --- | --- | --- | --- | @@ -134,7 +140,7 @@ The table types differ in two ways: who creates and migrates the table, and whet | Schema changes | Near-instant | Take longer as the table grows | Migrations you write | - **Managed views:** Use these by default: schema changes are near-instant, because PowerSync only re-creates the views. Queries that read many rows of a large table can be slower than on a managed table. -- **Managed tables (alpha):** Switch a table to a managed table if its queries are measurably too slow. This is typically a large table that you filter, sort or aggregate on columns without an index. You set this per table, so the other tables can stay managed views. The trade-off is that schema changes on a managed table take longer as the table grows. +- **Managed tables (alpha):** Switch a table to a managed table if its queries are measurably too slow. This is typically a large table that you filter, sort or aggregate on columns without an index. The trade-off is that schema changes on a managed table take longer as the table grows. - **Raw tables:** Use [raw tables](/client-sdks/advanced/raw-tables) when you want full control over the SQLite table, for example to add foreign keys, custom triggers or expression indexes, or to use a library such as [Room](/client-sdks/orms/kotlin/room) that manages the schema. The trade-off is that you have to create and migrate the table yourself. ### Enabling Managed Tables diff --git a/client-sdks/advanced/raw-tables.mdx b/client-sdks/advanced/raw-tables.mdx index f48a67cbc..3ee8dd9be 100644 --- a/client-sdks/advanced/raw-tables.mdx +++ b/client-sdks/advanced/raw-tables.mdx @@ -30,7 +30,7 @@ Consider raw tables when you need: - **Reduced storage overhead** - eliminate JSON object overhead for each row in `ps_data__
.data` column. - **To manually create tables** - Sometimes you need full control over table creation, for example when implementing custom triggers or if you use an existing library like [Room](/client-sdks/orms/kotlin/room) that manages the schema for you. -If you only need raw tables for faster queries and less storage, consider [managed tables](/client-sdks/advanced/performance-optimization#managed-tables) (alpha) instead. They also store data in SQLite columns, but PowerSync creates and migrates them for you. To compare the options, see [Choosing a Table Type](/client-sdks/advanced/performance-optimization#choosing-a-table-type). +If you only need raw tables for faster queries, consider [managed tables](/client-sdks/advanced/performance-optimization#managed-tables) (alpha) instead. They also store data in SQLite columns, but PowerSync creates and migrates them for you. To compare the options, see [Choosing a Table Type](/client-sdks/advanced/performance-optimization#choosing-a-table-type). **Advanced SQLite features** like `FOREIGN KEY` and `ON DELETE CASCADE` constraints need [special consideration](#using-foreign-keys). From 2d9d1624c625d875eadfa8025daf392f119500e6 Mon Sep 17 00:00:00 2001 From: Benita Volkmann Date: Wed, 28 Oct 2026 16:52:15 +0200 Subject: [PATCH 14/18] Clarify migration implications --- client-sdks/advanced/performance-optimization.mdx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/client-sdks/advanced/performance-optimization.mdx b/client-sdks/advanced/performance-optimization.mdx index 1ea5308d7..ec25dd13e 100644 --- a/client-sdks/advanced/performance-optimization.mdx +++ b/client-sdks/advanced/performance-optimization.mdx @@ -186,14 +186,14 @@ schema Before you switch a table, check how managed tables differ from managed views: -- **Column types:** Managed views cast each value to the column type in your schema, both when the view reads from the JSON and when its triggers write to it. Managed tables have no view or casting triggers, so they store values with the type they were synced or written with, so a value synced as text into an `integer` column stays text. Filters and sorting on that column can then return different results than on a managed view. +- **Column types:** Managed views convert each value to the column type in your schema. Managed tables store each value with the type it was synced or written with. For example, a value synced as text into an `integer` column stays text. Filters and sorting on that column can then return different results than on a managed view. - **View names:** Managed tables aren't views, so you can't set the `viewName` option. If you use `viewName` to [move data from a local-only table to a synced table](/client-sdks/advanced/local-only-usage#moving-data-to-synced-tables), keep that table as a managed view. - **High Performance Diffs:** In the JavaScript SDKs, [High Performance Diffs](/client-sdks/high-performance-diffs) don't support managed tables yet. Keep tables that you watch with High Performance Diffs as managed views. ### Migrations -To switch a table between a managed view and a managed table, change the `direct` flag. PowerSync moves the existing rows when it applies the new schema, without downloading them again. +PowerSync migrates managed tables for you, so you don't write migration code. It runs these migrations when your app opens the database with the new schema, before the database is ready for queries. Before you release an app update that changes a managed table, consider these points: -When you add or remove columns, PowerSync runs the `ALTER TABLE` statements for you. These changes process every row in the table, whereas a managed view only needs its view re-created. In our tests on a table with 1 million rows, switching between a managed view and a managed table took a few seconds. Adding or dropping a column took up to about a second. On a managed view, both are near-instant. - -If an app update adds an already-synced column to your client schema, a managed view shows its values immediately, even offline. A managed table shows `NULL` in that column until the next completed sync, so offline devices see `NULL` until they reconnect. New columns are usually already synced, because the recommended order for [additive schema changes](/maintenance-ops/deploying-schema-changes) publishes the app last. After the sync, PowerSync fills the column from synced data that is already on the device, so nothing downloads again. This copy adds to the time that adding a column takes. +- **Switching to or from a managed table:** Change the `direct` flag. PowerSync copies the table's rows into the new format on the device, without downloading them again. The copy takes longer as the table grows. In our tests, a table with 1 million rows took a few seconds. +- **Adding or removing a column:** PowerSync runs the `ALTER TABLE` statements. These changes take longer as the table grows. In our tests on a table with 1 million rows, each change took up to about a second. On a managed view, the same changes are near-instant, because PowerSync only re-creates the view. +- **New columns are empty until the next sync:** If an app update adds a column that is already synced, a managed view shows its values right away, even offline. A managed table shows `NULL` in that column until the next completed sync, so offline devices see `NULL` until they reconnect. PowerSync then fills the column from synced data already on the device, without downloading it again. Note that new columns are usually already synced when the app update ships, because our recommended order for [additive schema changes](/maintenance-ops/deploying-schema-changes) publishes the app last. From de360c8887256b1cf02fa2d3196cbd8229b4541d Mon Sep 17 00:00:00 2001 From: Benita Volkmann Date: Wed, 28 Oct 2026 17:32:11 +0200 Subject: [PATCH 15/18] Also make "when to use raw tabes" clearer --- client-sdks/advanced/raw-tables.mdx | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/client-sdks/advanced/raw-tables.mdx b/client-sdks/advanced/raw-tables.mdx index 3ee8dd9be..8171c8e61 100644 --- a/client-sdks/advanced/raw-tables.mdx +++ b/client-sdks/advanced/raw-tables.mdx @@ -23,14 +23,13 @@ This eliminates overhead associated with extracting values from the JSON data an ## When to Use Raw Tables -Consider raw tables when you need: +Use raw tables when you need control over the SQLite table that the client-side schema doesn't give you: -- **Indexes** - PowerSync's default schema has basic support for indexes on columns, while raw tables give you complete control to create indexes on expressions, use `GENERATED` columns, etc. -- **Improved performance** for complex queries (e.g., `SELECT SUM(value) FROM transactions`) - raw tables more efficiently get these values directly from the SQLite column, instead of extracting the value from the JSON object on every row. -- **Reduced storage overhead** - eliminate JSON object overhead for each row in `ps_data__
.data` column. -- **To manually create tables** - Sometimes you need full control over table creation, for example when implementing custom triggers or if you use an existing library like [Room](/client-sdks/orms/kotlin/room) that manages the schema for you. +- **Constraints:** `FOREIGN KEY` and `ON DELETE CASCADE` constraints. +- **Custom columns and indexes:** `GENERATED` columns, or indexes on expressions. The client-side schema only supports indexes on plain columns. +- **Custom table setup:** Custom triggers, or a library such as [Room](/client-sdks/orms/kotlin/room) that creates the schema for you. -If you only need raw tables for faster queries, consider [managed tables](/client-sdks/advanced/performance-optimization#managed-tables) (alpha) instead. They also store data in SQLite columns, but PowerSync creates and migrates them for you. To compare the options, see [Choosing a Table Type](/client-sdks/advanced/performance-optimization#choosing-a-table-type). +Raw tables also make queries that read many rows faster, such as `SELECT SUM(value) FROM transactions`, because SQLite reads values directly from the columns instead of extracting them from JSON. If faster queries are your only reason, consider [managed tables](/client-sdks/advanced/performance-optimization#managed-tables) (alpha) instead. They also store data in SQLite columns, but PowerSync creates and migrates them for you. To compare the options, see [Choosing a Table Type](/client-sdks/advanced/performance-optimization#choosing-a-table-type). **Advanced SQLite features** like `FOREIGN KEY` and `ON DELETE CASCADE` constraints need [special consideration](#using-foreign-keys). From bb7e33159492c4d14f97c1ae41f12824aba318d4 Mon Sep 17 00:00:00 2001 From: Benita Volkmann Date: Wed, 28 Oct 2026 17:34:46 +0200 Subject: [PATCH 16/18] More specific query benefit explanation --- .../advanced/performance-optimization.mdx | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/client-sdks/advanced/performance-optimization.mdx b/client-sdks/advanced/performance-optimization.mdx index ec25dd13e..57307a710 100644 --- a/client-sdks/advanced/performance-optimization.mdx +++ b/client-sdks/advanced/performance-optimization.mdx @@ -56,7 +56,7 @@ If a query itself is expensive and takes a long time to run, several options can 1. Where possible, filter on the `id` column of tables to reduce the number of rows read, which makes queries more efficient. When filtering on other columns of large tables, make sure these columns are covered by indexes declared in your app's schema. 2. For auto-updating watched queries on frequently-changed tables, queries might run very often. Watched queries should typically be cheap to run, as they otherwise risk blocking SQLite connections for too long. For more expensive queries that still need to be watched, consider increasing their throttle to run them less often. -3. By default, PowerSync tables are views over JSON data that extract columns by parsing from JSON each time a row is accessed. While SQLite has a cache for parsed JSON, this can still be inefficient for queries computing on columns. [Managed tables](#managed-tables) store data in SQLite columns instead, at the cost of slower schema changes. +3. By default, PowerSync tables are views over JSON data that extract columns by parsing from JSON each time a row is accessed. While SQLite has a cache for parsed JSON, this is still slow for queries that read many rows, for example to filter, sort or aggregate. [Managed tables](#managed-tables) store data in SQLite columns instead, at the cost of slower schema changes. 4. If your app runs the same quick queries many times, SQLite spends part of each run reading the SQL and planning the query again. In the JavaScript Web and Dart SDKs, a [prepared statement cache](#prepared-statement-cache) keeps recently planned queries so that SQLite can reuse them. 5. In the JavaScript SDKs, you can replace regular watched queries with [High Performance Diffs](/client-sdks/high-performance-diffs). These use triggers internally to report only changed rows back to your app. @@ -115,9 +115,17 @@ For more information, see the `preparedStatementsCache` option [for JavaScript]( - **Rust**: Version `0.2.0`. -By default, each table in your client-side schema is a **managed view**. PowerSync stores synced rows as [JSON in an internal table](/architecture/client-architecture#client-side-schema-and-sqlite-database-structure) and creates a SQLite view that extracts your columns from it. Queries on the view decode the JSON of every row they read. +By default, each table in your client-side schema is a **managed view**. PowerSync stores synced rows as [JSON in an internal table](/architecture/client-architecture#client-side-schema-and-sqlite-database-structure) and creates a SQLite view that extracts your columns from the JSON. Queries on the view decode the JSON of every row they read. -Queries on a **managed table** run faster than on a managed view. A managed table stores rows in a SQLite table with a column for each column in your schema, so queries read values directly instead of decoding JSON. You query and write to it with the same SQL statements as a managed view. PowerSync still creates and migrates the table, and it still adds local writes to the upload queue. +Queries on a **managed table** run faster than on a managed view, because they read values directly instead of decoding JSON. A managed table stores rows in a SQLite table with a column for each column in your schema. You query and write to it with the same SQL statements as a managed view. PowerSync still creates and migrates the table and adds local writes to the upload queue. + +How much faster depends on how many rows a query reads: + +- **Most gain:** Queries that filter, sort or aggregate across many rows, such as `SELECT sum(hours) FROM time_entries`. +- **Some gain:** Queries that return many rows, such as `SELECT * FROM tasks` on a large table. +- **Little gain:** Queries that look up a few rows by `id` or through an index. + +Indexes speed up lookups on managed views, so keep indexes on join keys and on columns you often filter by. It isn't practical to index every column you query, though. An index also doesn't help a query that returns many rows. Managed tables make these queries faster without extra indexes. **Benchmarks** @@ -139,8 +147,8 @@ Start with managed views. Change a table's type only if you hit a limit: its que | Queries read | JSON, through a view | SQLite columns | SQLite columns | | Schema changes | Near-instant | Take longer as the table grows | Migrations you write | -- **Managed views:** Use these by default: schema changes are near-instant, because PowerSync only re-creates the views. Queries that read many rows of a large table can be slower than on a managed table. -- **Managed tables (alpha):** Switch a table to a managed table if its queries are measurably too slow. This is typically a large table that you filter, sort or aggregate on columns without an index. The trade-off is that schema changes on a managed table take longer as the table grows. +- **Managed views:** Use these by default: schema changes are near-instant, because PowerSync only re-creates the views. The trade-off is slower queries that read many rows, such as aggregates and long lists. +- **Managed tables (alpha):** Switch a table to a managed table if its queries are measurably too slow. This is typically a large table whose queries read many rows, for example to filter, sort, aggregate or list them. The trade-off is that schema changes on a managed table take longer as the table grows. - **Raw tables:** Use [raw tables](/client-sdks/advanced/raw-tables) when you want full control over the SQLite table, for example to add foreign keys, custom triggers or expression indexes, or to use a library such as [Room](/client-sdks/orms/kotlin/room) that manages the schema. The trade-off is that you have to create and migrate the table yourself. ### Enabling Managed Tables From f31e6f4f54bc3fad33e1fac22672dc62285af532 Mon Sep 17 00:00:00 2001 From: Benita Volkmann Date: Thu, 29 Oct 2026 09:48:54 +0200 Subject: [PATCH 17/18] Managed views vs managed tables header --- architecture/client-architecture.mdx | 4 ++-- client-sdks/advanced/performance-optimization.mdx | 4 ++-- client-sdks/advanced/raw-tables.mdx | 2 +- client-sdks/high-performance-diffs.mdx | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/architecture/client-architecture.mdx b/architecture/client-architecture.mdx index f14a8808a..0bc248624 100644 --- a/architecture/client-architecture.mdx +++ b/architecture/client-architecture.mdx @@ -36,7 +36,7 @@ The PowerSync Client SDK automatically maintains the following tables in the SQL | `ps_data__
` | This contains the data for each "table", in JSON format. Since JSON is being used, this table's schema does not change when columns are added, removed or changed in the Sync Streams and client-side schema. | | `ps_data_local__
` | Same as the previous point, but for [local-only](/client-sdks/advanced/local-only-usage) tables. | | `
` (`VIEW`) | These are views on the above `ps_data` tables, with each defined column in the client-side schema extracted from the JSON. For example, a `description` text column would be `CAST(data ->> '$.description' as TEXT)`. | -| `
` (`TABLE`) | For [managed tables](/client-sdks/advanced/performance-optimization#managed-tables), PowerSync replaces the `ps_data__` storage table and view with a single table matching your schema. | +| `
` (`TABLE`) | For [managed tables](/client-sdks/advanced/performance-optimization#managed-views-vs-managed-tables), PowerSync replaces the `ps_data__` storage table and view with a single table matching your schema. | | `ps_untyped` | Any synced table that is not defined in the client-side schema is placed here. If the table is added to the schema at a later point, the data is then migrated to `ps_data__
`. | | `ps_oplog` | This is the operation history data as received from the [PowerSync Service](/architecture/powersync-service), grouped per bucket. | | `ps_crud` | The client-side upload queue (see [Writing Data](#writing-data-via-sqlite-database-and-upload-queue) below) | @@ -51,7 +51,7 @@ The copy of the row in `ps_oplog` may be newer than the one in `ps_data__
It is possible for different [buckets](/architecture/powersync-service#bucket-system) to include overlapping data (for example, if multiple buckets contain data from the same table). If rows with the same table and ID have been synced via multiple buckets, it may be present multiple times in `ps_oplog`, but only one will be preserved in the `ps_data__
` table (the one with the highest `op_id`). - **Alternatives to JSON-based views:** [Managed tables](/client-sdks/advanced/performance-optimization#managed-tables) (alpha) store data in SQLite columns for faster queries, and PowerSync still creates and migrates them. [Raw tables](/client-sdks/advanced/raw-tables) give you full control over the SQLite tables, for example to use foreign keys, but you have to create and migrate them yourself. To compare the options, see [Choosing a Table Type](/client-sdks/advanced/performance-optimization#choosing-a-table-type). + **Alternatives to JSON-based views:** [Managed tables](/client-sdks/advanced/performance-optimization#managed-views-vs-managed-tables) (alpha) store data in SQLite columns for faster queries, and PowerSync still creates and migrates them. [Raw tables](/client-sdks/advanced/raw-tables) give you full control over the SQLite tables, for example to use foreign keys, but you have to create and migrate them yourself. To compare the options, see [Choosing a Table Type](/client-sdks/advanced/performance-optimization#choosing-a-table-type). ## Writing Data (via SQLite Database and Upload Queue) diff --git a/client-sdks/advanced/performance-optimization.mdx b/client-sdks/advanced/performance-optimization.mdx index 57307a710..e1dd74201 100644 --- a/client-sdks/advanced/performance-optimization.mdx +++ b/client-sdks/advanced/performance-optimization.mdx @@ -56,7 +56,7 @@ If a query itself is expensive and takes a long time to run, several options can 1. Where possible, filter on the `id` column of tables to reduce the number of rows read, which makes queries more efficient. When filtering on other columns of large tables, make sure these columns are covered by indexes declared in your app's schema. 2. For auto-updating watched queries on frequently-changed tables, queries might run very often. Watched queries should typically be cheap to run, as they otherwise risk blocking SQLite connections for too long. For more expensive queries that still need to be watched, consider increasing their throttle to run them less often. -3. By default, PowerSync tables are views over JSON data that extract columns by parsing from JSON each time a row is accessed. While SQLite has a cache for parsed JSON, this is still slow for queries that read many rows, for example to filter, sort or aggregate. [Managed tables](#managed-tables) store data in SQLite columns instead, at the cost of slower schema changes. +3. By default, PowerSync tables are views over JSON data that extract columns by parsing from JSON each time a row is accessed. While SQLite has a cache for parsed JSON, this is still slow for queries that read many rows, for example to filter, sort or aggregate. [Managed tables](#managed-views-vs-managed-tables) store data in SQLite columns instead, at the cost of slower schema changes. 4. If your app runs the same quick queries many times, SQLite spends part of each run reading the SQL and planning the query again. In the JavaScript Web and Dart SDKs, a [prepared statement cache](#prepared-statement-cache) keeps recently planned queries so that SQLite can reuse them. 5. In the JavaScript SDKs, you can replace regular watched queries with [High Performance Diffs](/client-sdks/high-performance-diffs). These use triggers internally to report only changed rows back to your app. @@ -100,7 +100,7 @@ For more information, see the `preparedStatementsCache` option [for JavaScript]( -## Managed Tables +## Managed Views vs. Managed Tables **Availability** diff --git a/client-sdks/advanced/raw-tables.mdx b/client-sdks/advanced/raw-tables.mdx index 8171c8e61..0e4b448cb 100644 --- a/client-sdks/advanced/raw-tables.mdx +++ b/client-sdks/advanced/raw-tables.mdx @@ -29,7 +29,7 @@ Use raw tables when you need control over the SQLite table that the client-side - **Custom columns and indexes:** `GENERATED` columns, or indexes on expressions. The client-side schema only supports indexes on plain columns. - **Custom table setup:** Custom triggers, or a library such as [Room](/client-sdks/orms/kotlin/room) that creates the schema for you. -Raw tables also make queries that read many rows faster, such as `SELECT SUM(value) FROM transactions`, because SQLite reads values directly from the columns instead of extracting them from JSON. If faster queries are your only reason, consider [managed tables](/client-sdks/advanced/performance-optimization#managed-tables) (alpha) instead. They also store data in SQLite columns, but PowerSync creates and migrates them for you. To compare the options, see [Choosing a Table Type](/client-sdks/advanced/performance-optimization#choosing-a-table-type). +Raw tables also make queries that read many rows faster, such as `SELECT SUM(value) FROM transactions`, because SQLite reads values directly from the columns instead of extracting them from JSON. If faster queries are your only reason, consider [managed tables](/client-sdks/advanced/performance-optimization#managed-views-vs-managed-tables) (alpha) instead. They also store data in SQLite columns, but PowerSync creates and migrates them for you. To compare the options, see [Choosing a Table Type](/client-sdks/advanced/performance-optimization#choosing-a-table-type). **Advanced SQLite features** like `FOREIGN KEY` and `ON DELETE CASCADE` constraints need [special consideration](#using-foreign-keys). diff --git a/client-sdks/high-performance-diffs.mdx b/client-sdks/high-performance-diffs.mdx index dc0583827..16cd50a91 100644 --- a/client-sdks/high-performance-diffs.mdx +++ b/client-sdks/high-performance-diffs.mdx @@ -22,7 +22,7 @@ While [basic/incremental watch queries](/client-sdks/watch-queries) enable react - API design and developer experience - Additional features or optimizations needed - The API does not support [raw tables](/client-sdks/advanced/raw-tables) or [managed tables](/client-sdks/advanced/performance-optimization#managed-tables) yet. We have a [public proposal](https://docs.google.com/document/d/12bvZDJF2aaTOqkxHyo3W-TYGQTaS9FQFdu-MX2yOuqI/edit) open to discuss this addition - your feedback is welcome! + The API does not support [raw tables](/client-sdks/advanced/raw-tables) or [managed tables](/client-sdks/advanced/performance-optimization#managed-views-vs-managed-tables) yet. We have a [public proposal](https://docs.google.com/document/d/12bvZDJF2aaTOqkxHyo3W-TYGQTaS9FQFdu-MX2yOuqI/edit) open to discuss this addition - your feedback is welcome! Join our [Discord community](https://discord.gg/powersync) to share your experience and get help. From d83c8626e88e5b8f5ecbaef6f02c1f49dadd0d2c Mon Sep 17 00:00:00 2001 From: Benita Volkmann Date: Thu, 29 Oct 2026 10:09:09 +0200 Subject: [PATCH 18/18] Reshuffle things a bit --- .../advanced/performance-optimization.mdx | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/client-sdks/advanced/performance-optimization.mdx b/client-sdks/advanced/performance-optimization.mdx index e1dd74201..6f0975a6c 100644 --- a/client-sdks/advanced/performance-optimization.mdx +++ b/client-sdks/advanced/performance-optimization.mdx @@ -102,22 +102,9 @@ For more information, see the `preparedStatementsCache` option [for JavaScript]( ## Managed Views vs. Managed Tables - - **Availability** - - Managed tables are currently in [alpha](/resources/feature-status), so their API may change based on feedback. They were introduced in the following versions of our client SDKs: - - - **JavaScript** (React Native: `2.3.4`, Web: `2.6.0`, Node.js: `1.2.1`, Capacitor: `0.9.5`) - - **Dart**: Version `2.6.0`. - - **Kotlin**: Version `1.16.0`. - - **Swift**: Version `1.17.0`. - - **.NET**: Not supported yet. - - **Rust**: Version `0.2.0`. - - By default, each table in your client-side schema is a **managed view**. PowerSync stores synced rows as [JSON in an internal table](/architecture/client-architecture#client-side-schema-and-sqlite-database-structure) and creates a SQLite view that extracts your columns from the JSON. Queries on the view decode the JSON of every row they read. -Queries on a **managed table** run faster than on a managed view, because they read values directly instead of decoding JSON. A managed table stores rows in a SQLite table with a column for each column in your schema. You query and write to it with the same SQL statements as a managed view. PowerSync still creates and migrates the table and adds local writes to the upload queue. +A **managed table** stores rows in a SQLite table, with a column for each column in your schema. You query and write to it with the same SQL statements as a managed view. PowerSync still creates and migrates the table and adds local writes to the upload queue. The difference is that queries read values directly instead of decoding JSON, so they run faster. How much faster depends on how many rows a query reads: @@ -153,6 +140,19 @@ Start with managed views. Change a table's type only if you hit a limit: its que ### Enabling Managed Tables + + **Availability** + + Managed tables are currently in [alpha](/resources/feature-status), so their API may change based on feedback. They were introduced in the following versions of our client SDKs: + + - **JavaScript** (React Native: `2.3.4`, Web: `2.6.0`, Node.js: `1.2.1`, Capacitor: `0.9.5`) + - **Dart**: Version `2.6.0`. + - **Kotlin**: Version `1.16.0`. + - **Swift**: Version `1.17.0`. + - **.NET**: Not supported yet. + - **Rust**: Version `0.2.0`. + + Set the `direct` flag on the table definition. The flag name refers to queries reading the table directly, with no view in between.