Initial commit
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
---
|
||||
title: Optimize SQLite for Production
|
||||
impact: LOW-MEDIUM
|
||||
impactDescription: Better performance and reliability for SQLite database
|
||||
tags: production, sqlite, database, performance
|
||||
---
|
||||
|
||||
## Optimize SQLite for Production
|
||||
|
||||
PocketBase uses SQLite with optimized defaults. Understanding its characteristics helps optimize performance and avoid common pitfalls. PocketBase uses two separate databases: `data.db` (application data) and `auxiliary.db` (logs and ephemeral data), which reduces write contention.
|
||||
|
||||
**Incorrect (ignoring SQLite characteristics):**
|
||||
|
||||
```javascript
|
||||
// Heavy concurrent writes - SQLite bottleneck
|
||||
async function bulkInsert(items) {
|
||||
// Parallel writes cause lock contention
|
||||
await Promise.all(items.map(item =>
|
||||
pb.collection('items').create(item)
|
||||
));
|
||||
}
|
||||
|
||||
// Not using transactions for batch operations
|
||||
async function updateMany(items) {
|
||||
for (const item of items) {
|
||||
await pb.collection('items').update(item.id, item);
|
||||
}
|
||||
// Each write is a separate transaction - slow!
|
||||
}
|
||||
|
||||
// Large text fields without consideration
|
||||
const schema = [{
|
||||
name: 'content',
|
||||
type: 'text' // Could be megabytes - affects all queries
|
||||
}];
|
||||
```
|
||||
|
||||
**Correct (SQLite-optimized patterns):**
|
||||
|
||||
```javascript
|
||||
// Use batch operations for multiple writes
|
||||
async function bulkInsert(items) {
|
||||
const batch = pb.createBatch();
|
||||
items.forEach(item => {
|
||||
batch.collection('items').create(item);
|
||||
});
|
||||
await batch.send(); // Single transaction, much faster
|
||||
}
|
||||
|
||||
// Batch updates
|
||||
async function updateMany(items) {
|
||||
const batch = pb.createBatch();
|
||||
items.forEach(item => {
|
||||
batch.collection('items').update(item.id, item);
|
||||
});
|
||||
await batch.send();
|
||||
}
|
||||
|
||||
// For very large batches, chunk them
|
||||
async function bulkInsertLarge(items, chunkSize = 100) {
|
||||
for (let i = 0; i < items.length; i += chunkSize) {
|
||||
const chunk = items.slice(i, i + chunkSize);
|
||||
const batch = pb.createBatch();
|
||||
chunk.forEach(item => batch.collection('items').create(item));
|
||||
await batch.send();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Schema considerations:**
|
||||
|
||||
```javascript
|
||||
// Separate large content into dedicated collection
|
||||
const postsSchema = [
|
||||
{ name: 'title', type: 'text' },
|
||||
{ name: 'summary', type: 'text', options: { maxLength: 500 } },
|
||||
{ name: 'author', type: 'relation' }
|
||||
// Content in separate collection
|
||||
];
|
||||
|
||||
const postContentsSchema = [
|
||||
{ name: 'post', type: 'relation', required: true },
|
||||
{ name: 'content', type: 'editor' } // Large HTML content
|
||||
];
|
||||
|
||||
// Fetch content only when needed
|
||||
async function getPostList() {
|
||||
return pb.collection('posts').getList(1, 20); // Fast, no content
|
||||
}
|
||||
|
||||
async function getPostWithContent(id) {
|
||||
const post = await pb.collection('posts').getOne(id);
|
||||
const content = await pb.collection('post_contents').getFirstListItem(
|
||||
pb.filter('post = {:id}', { id })
|
||||
);
|
||||
return { ...post, content: content.content };
|
||||
}
|
||||
```
|
||||
|
||||
**PocketBase default PRAGMA settings:**
|
||||
|
||||
PocketBase already configures optimal SQLite settings. You do not need to set these manually unless using a custom SQLite driver:
|
||||
|
||||
```sql
|
||||
PRAGMA busy_timeout = 10000; -- Wait 10s for locks instead of failing immediately
|
||||
PRAGMA journal_mode = WAL; -- Write-Ahead Logging: concurrent reads during writes
|
||||
PRAGMA journal_size_limit = 200000000; -- Limit WAL file to ~200MB
|
||||
PRAGMA synchronous = NORMAL; -- Balanced durability/performance (safe with WAL)
|
||||
PRAGMA foreign_keys = ON; -- Enforce relation integrity
|
||||
PRAGMA temp_store = MEMORY; -- Temp tables in memory (faster sorts/joins)
|
||||
PRAGMA cache_size = -32000; -- 32MB page cache
|
||||
```
|
||||
|
||||
WAL mode is the most impactful setting -- it allows multiple concurrent readers while a single writer is active, which is critical for PocketBase's concurrent API request handling.
|
||||
|
||||
**Index optimization:**
|
||||
|
||||
```sql
|
||||
-- Create indexes for commonly filtered/sorted fields
|
||||
CREATE INDEX idx_posts_author ON posts(author);
|
||||
CREATE INDEX idx_posts_created ON posts(created DESC);
|
||||
CREATE INDEX idx_posts_status_created ON posts(status, created DESC);
|
||||
|
||||
-- Verify indexes are being used
|
||||
EXPLAIN QUERY PLAN
|
||||
SELECT * FROM posts WHERE author = 'xxx' ORDER BY created DESC;
|
||||
-- Should show: "USING INDEX idx_posts_author"
|
||||
```
|
||||
|
||||
**SQLite limitations and workarounds:**
|
||||
|
||||
| Limitation | Workaround |
|
||||
|------------|------------|
|
||||
| Single writer | Use batch operations, queue writes |
|
||||
| No full-text by default | Use view collections with FTS5 |
|
||||
| File-based | SSD storage, avoid network mounts |
|
||||
| Memory for large queries | Pagination, limit result sizes |
|
||||
|
||||
**Performance monitoring:**
|
||||
|
||||
```javascript
|
||||
// Monitor slow queries via hooks (requires custom PocketBase build)
|
||||
// Or use SQLite's built-in profiling
|
||||
|
||||
// From sqlite3 CLI:
|
||||
// .timer on
|
||||
// SELECT * FROM posts WHERE author = 'xxx';
|
||||
// Run Time: real 0.003 user 0.002 sys 0.001
|
||||
|
||||
// Check database size
|
||||
// ls -lh pb_data/data.db
|
||||
|
||||
// Vacuum to reclaim space after deletes
|
||||
// sqlite3 pb_data/data.db "VACUUM;"
|
||||
```
|
||||
|
||||
**When to consider alternatives:**
|
||||
|
||||
Consider migrating from single PocketBase if:
|
||||
- Write throughput consistently > 1000/sec needed
|
||||
- Database size > 100GB
|
||||
- Complex transactions across tables
|
||||
- Multi-region deployment required
|
||||
|
||||
**Custom SQLite driver (advanced):**
|
||||
|
||||
PocketBase supports custom SQLite drivers via `DBConnect`. The CGO driver (`mattn/go-sqlite3`) can offer better performance for some workloads and enables extensions like ICU and FTS5. This requires a custom PocketBase build:
|
||||
|
||||
```go
|
||||
// main.go (custom PocketBase build with CGO driver)
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase"
|
||||
_ "github.com/mattn/go-sqlite3" // CGO SQLite driver
|
||||
)
|
||||
|
||||
func main() {
|
||||
app := pocketbase.NewWithConfig(pocketbase.Config{
|
||||
// Called twice: once for data.db, once for auxiliary.db
|
||||
DBConnect: func(dbPath string) (*dbx.DB, error) {
|
||||
return dbx.Open("sqlite3", dbPath)
|
||||
},
|
||||
})
|
||||
|
||||
if err := app.Start(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
// Build with: CGO_ENABLED=1 go build
|
||||
```
|
||||
|
||||
Note: CGO requires C compiler toolchain and cannot be cross-compiled as easily as pure Go.
|
||||
|
||||
**Scaling options:**
|
||||
1. **Read replicas**: Litestream for SQLite replication
|
||||
2. **Sharding**: Multiple PocketBase instances by tenant/feature
|
||||
3. **Caching**: Redis/Memcached for read-heavy loads
|
||||
4. **Alternative backend**: If requirements exceed SQLite, evaluate PostgreSQL-based frameworks
|
||||
|
||||
Reference: [PocketBase Going to Production](https://pocketbase.io/docs/going-to-production/)
|
||||
Reference in New Issue
Block a user