Database Configuration
Fine-tuning your Drift database for optimal performance and behavior
What is it?
Database Configuration refers to all the settings, PRAGMA statements, and options you can set on your Drift database to control its behavior, performance, and features. This includes everything from journal modes to cache sizes, foreign key constraints, and custom functions.
Think of Database Configuration like "tuning a race car" – you adjust various parameters to get the best performance for your specific use case.
// Complete database configuration example
class AppDatabase extends _$AppDatabase {
AppDatabase([QueryExecutor? executor]) : super(executor ?? _openConnection());
@override
int get schemaVersion => 1;
@override
Future<void> beforeOpen() async {
await super.beforeOpen();
// 👇 All the configuration settings
await customSelect('PRAGMA journal_mode = WAL').get();
await customSelect('PRAGMA synchronous = NORMAL').get();
await customSelect('PRAGMA foreign_keys = ON').get();
await customSelect('PRAGMA cache_size = -10000').get();
await customSelect('PRAGMA temp_store = MEMORY').get();
await customSelect('PRAGMA mmap_size = 268435456').get();
await customSelect('PRAGMA auto_vacuum = INCREMENTAL').get();
}
static QueryExecutor _openConnection() {
return driftDatabase(name: 'my_app');
}
}
What's happening here? - PRAGMA statements – Configure database behavior - Journal mode – Controls how changes are written - Synchronous – Controls durability vs performance - Cache size – Memory allocation for performance - Foreign keys – Enable referential integrity - MMAP size – Memory-mapped I/O for faster reads
Why does it exist?
- Performance – Optimize for speed or memory
- Durability – Balance data safety vs performance
- Concurrency – Enable multiple readers/writers
- Integrity – Enforce foreign key constraints
- Features – Enable specific SQLite capabilities
- Customization – Tune for specific use cases
Key Configuration Options
All the important PRAGMA statements you can use
1. Journal Mode
Controls how SQLite handles transaction logging
// lib/database/database.dart
@DriftDatabase(tables: [Users])
class AppDatabase extends _$AppDatabase {
AppDatabase([QueryExecutor? executor]) : super(executor ?? _openConnection());
@override
int get schemaVersion => 1;
@override
Future<void> beforeOpen() async {
await super.beforeOpen();
// 👇 Choose your journal mode
// Option 1: WAL (Write-Ahead Logging) - RECOMMENDED
await customSelect('PRAGMA journal_mode = WAL').get();
// ✅ Concurrent reads and writes
// ✅ Faster performance
// ✅ Better for multi-isolate
// Option 2: DELETE (Default)
// await customSelect('PRAGMA journal_mode = DELETE').get();
// ✅ Traditional, widely compatible
// ❌ Slower for concurrent access
// Option 3: MEMORY
// await customSelect('PRAGMA journal_mode = MEMORY').get();
// ✅ Fastest
// ❌ Data loss on crash
// Option 4: OFF
// await customSelect('PRAGMA journal_mode = OFF').get();
// ✅ Maximum speed
// ❌ No crash recovery
}
static QueryExecutor _openConnection() {
return driftDatabase(name: 'my_app');
}
}
Key insights: - WAL mode – Best for most apps, enables concurrent access - DELETE mode – Traditional, safe but slower - MEMORY mode – Fast but risks data loss - OFF mode – Maximum speed, no recovery
2. Synchronous Mode
Controls how aggressively SQLite writes to disk
class AppDatabase extends _$AppDatabase {
AppDatabase([QueryExecutor? executor]) : super(executor ?? _openConnection());
@override
int get schemaVersion => 1;
@override
Future<void> beforeOpen() async {
await super.beforeOpen();
// 👇 Choose your synchronous mode
// Option 1: FULL (Safest - RECOMMENDED for financial data)
await customSelect('PRAGMA synchronous = FULL').get();
// ✅ Maximum durability
// ❌ Slower performance
// Option 2: NORMAL (Balanced - RECOMMENDED for most apps)
await customSelect('PRAGMA synchronous = NORMAL').get();
// ✅ Good performance
// ✅ Good durability
// Option 3: OFF (Fastest - Use with caution)
// await customSelect('PRAGMA synchronous = OFF').get();
// ✅ Maximum speed
// ❌ Risk of corruption on crash
}
static QueryExecutor _openConnection() {
return driftDatabase(name: 'my_app');
}
}
Key insights: - FULL mode – Every write is synced to disk, safest - NORMAL mode – Good balance, recommended for most apps - OFF mode – Fastest but risky, not recommended
3. Cache Size
Controls memory allocated for caching
class AppDatabase extends _$AppDatabase {
AppDatabase([QueryExecutor? executor]) : super(executor ?? _openConnection());
@override
int get schemaVersion => 1;
@override
Future<void> beforeOpen() async {
await super.beforeOpen();
// 👇 Configure cache size
// Option 1: Set to specific number of pages
await customSelect('PRAGMA cache_size = 10000').get();
// 10,000 pages × 4KB = 40MB cache
// Option 2: Use negative number (KB)
await customSelect('PRAGMA cache_size = -20000').get();
// 20MB cache
// Option 3: Large cache for performance
// await customSelect('PRAGMA cache_size = -50000').get();
// 50MB cache
}
static QueryExecutor _openConnection() {
return driftDatabase(name: 'my_app');
}
}
Key insights: - Positive numbers – Pages (typically 4KB each) - Negative numbers – Kilobytes (KB) - Larger cache – Better performance, more memory - Balance – Cache size vs memory usage
4. Foreign Keys
Enforce referential integrity
class AppDatabase extends _$AppDatabase {
AppDatabase([QueryExecutor? executor]) : super(executor ?? _openConnection());
@override
int get schemaVersion => 1;
@override
Future<void> beforeOpen() async {
await super.beforeOpen();
// 👇 Always enable foreign keys (RECOMMENDED)
await customSelect('PRAGMA foreign_keys = ON').get();
// This ensures:
// - Cannot delete a user if they have posts
// - Cannot insert a post with invalid user_id
// - Cascade deletes work properly
}
static QueryExecutor _openConnection() {
return driftDatabase(name: 'my_app');
}
}
Key insights: - Always enable – Prevents orphaned records - ON by default – In modern SQLite versions - Required – For foreign key constraints to work
5. Memory-Mapped I/O
Use memory mapping for faster reads
class AppDatabase extends _$AppDatabase {
AppDatabase([QueryExecutor? executor]) : super(executor ?? _openConnection());
@override
int get schemaVersion => 1;
@override
Future<void> beforeOpen() async {
await super.beforeOpen();
// 👇 Enable memory-mapped I/O
// Option 1: Standard size
await customSelect('PRAGMA mmap_size = 268435456').get();
// 256MB memory map
// Option 2: Large database
// await customSelect('PRAGMA mmap_size = 1073741824').get();
// 1GB memory map
// Option 3: Disable
// await customSelect('PRAGMA mmap_size = 0').get();
}
static QueryExecutor _openConnection() {
return driftDatabase(name: 'my_app');
}
}
Key insights: - Memory mapping – Maps database file to memory - Faster reads – Avoids system calls - Requires enough memory – Don't exceed available RAM - Database size – Set based on expected database size
6. Auto Vacuum
Controls database space management
class AppDatabase extends _$AppDatabase {
AppDatabase([QueryExecutor? executor]) : super(executor ?? _openConnection());
@override
int get schemaVersion => 1;
@override
Future<void> beforeOpen() async {
await super.beforeOpen();
// 👇 Choose auto-vacuum mode
// Option 1: NONE (Default)
// await customSelect('PRAGMA auto_vacuum = NONE').get();
// ❌ Database never shrinks
// Option 2: FULL (Shrink after each transaction)
// await customSelect('PRAGMA auto_vacuum = FULL').get();
// ✅ Keeps database small
// ❌ Slower transactions
// Option 3: INCREMENTAL (Gradual shrink - RECOMMENDED)
await customSelect('PRAGMA auto_vacuum = INCREMENTAL').get();
// ✅ Gradual space recovery
// ✅ Good performance
}
static QueryExecutor _openConnection() {
return driftDatabase(name: 'my_app');
}
}
Key insights: - INCREMENTAL – Best balance for most apps - FULL – Smallest database, slower operations - NONE – Fastest, but database never shrinks
7. Temp Store
Where to store temporary tables and indices
class AppDatabase extends _$AppDatabase {
AppDatabase([QueryExecutor? executor]) : super(executor ?? _openConnection());
@override
int get schemaVersion => 1;
@override
Future<void> beforeOpen() async {
await super.beforeOpen();
// 👇 Choose temp store location
// Option 1: DEFAULT (Use default location)
// await customSelect('PRAGMA temp_store = DEFAULT').get();
// Option 2: FILE (Use filesystem - safer)
// await customSelect('PRAGMA temp_store = FILE').get();
// ✅ Data persists if memory is low
// Option 3: MEMORY (Fastest - RECOMMENDED)
await customSelect('PRAGMA temp_store = MEMORY').get();
// ✅ Faster
// ❌ Uses memory
}
static QueryExecutor _openConnection() {
return driftDatabase(name: 'my_app');
}
}
Key insights: - MEMORY – Fastest, recommended for performance - FILE – Safer, uses less memory - DEFAULT – Platform default
8. Journal Size Limit
Control the size of the WAL journal file
class AppDatabase extends _$AppDatabase {
AppDatabase([QueryExecutor? executor]) : super(executor ?? _openConnection());
@override
int get schemaVersion => 1;
@override
Future<void> beforeOpen() async {
await super.beforeOpen();
// 👇 Set journal size limit
// Option 1: Standard limit
await customSelect('PRAGMA journal_size_limit = 67108864').get();
// 64MB limit
// Option 2: Large database
// await customSelect('PRAGMA journal_size_limit = 268435456').get();
// 256MB limit
// Option 3: Unlimited
// await customSelect('PRAGMA journal_size_limit = -1').get();
}
static QueryExecutor _openConnection() {
return driftDatabase(name: 'my_app');
}
}
Key insights: - Limits WAL file growth – Prevents disk space issues - Balance – Larger for more concurrent transactions - Smaller – Saves disk space
9. Page Size
Set the database page size
class AppDatabase extends _$AppDatabase {
AppDatabase([QueryExecutor? executor]) : super(executor ?? _openConnection());
@override
int get schemaVersion => 1;
@override
Future<void> beforeOpen() async {
await super.beforeOpen();
// 👇 Set page size (must be set before creating tables)
// Option 1: Default (4096)
// await customSelect('PRAGMA page_size = 4096').get();
// Option 2: Optimized for large records
// await customSelect('PRAGMA page_size = 8192').get();
// Option 3: Optimized for small records
// await customSelect('PRAGMA page_size = 2048').get();
}
static QueryExecutor _openConnection() {
return driftDatabase(name: 'my_app');
}
}
Key insights: - Default is 4096 – Works for most apps - Larger page – Better for large records - Smaller page – Better for many small records - Must be set – Before creating tables
Complete Configuration Example
All settings combined for production use
// lib/database/database.dart
import 'package:drift/drift.dart';
import 'package:drift_flutter/drift_flutter.dart';
import 'package:path_provider/path_provider.dart';
import 'tables/users.dart';
import 'tables/posts.dart';
import 'tables/comments.dart';
part 'database.g.dart';
@DriftDatabase(tables: [Users, Posts, Comments])
class AppDatabase extends _$AppDatabase {
AppDatabase([QueryExecutor? executor]) : super(executor ?? _openConnection());
@override
int get schemaVersion => 1;
// 👇 Complete database configuration
@override
Future<void> beforeOpen() async {
await super.beforeOpen();
print('⚙️ Configuring database...');
// 1️⃣ Journal Mode - Enable WAL for concurrent access
await customSelect('PRAGMA journal_mode = WAL').get();
print('✅ Journal mode: WAL');
// 2️⃣ Synchronous - Balance performance and durability
await customSelect('PRAGMA synchronous = NORMAL').get();
print('✅ Synchronous: NORMAL');
// 3️⃣ Foreign Keys - Enforce referential integrity
await customSelect('PRAGMA foreign_keys = ON').get();
print('✅ Foreign keys: ON');
// 4️⃣ Cache Size - Allocate 10MB for cache
await customSelect('PRAGMA cache_size = -10000').get();
print('✅ Cache size: 10MB');
// 5️⃣ Temp Store - Use memory for temp tables
await customSelect('PRAGMA temp_store = MEMORY').get();
print('✅ Temp store: MEMORY');
// 6️⃣ MMAP Size - Memory map 256MB for faster reads
await customSelect('PRAGMA mmap_size = 268435456').get();
print('✅ MMAP size: 256MB');
// 7️⃣ Auto Vacuum - Incremental cleanup
await customSelect('PRAGMA auto_vacuum = INCREMENTAL').get();
print('✅ Auto vacuum: INCREMENTAL');
// 8️⃣ Journal Size Limit - Prevent WAL file from growing too large
await customSelect('PRAGMA journal_size_limit = 67108864').get();
print('✅ Journal size limit: 64MB');
// 9️⃣ Optimize for performance
await customSelect('PRAGMA optimize').get();
print('✅ Database optimized');
// 🔟 Integrity check (optional)
final result = await customSelect('PRAGMA integrity_check').get();
final status = result.first.data['integrity_check'] as String;
if (status == 'ok') {
print('✅ Integrity check passed');
} else {
print('⚠️ Integrity check warning: $status');
}
print('✅ Database configuration complete!');
}
static QueryExecutor _openConnection() {
return driftDatabase(
name: 'my_app',
native: const DriftNativeOptions(
databaseDirectory: getApplicationSupportDirectory,
enableBackgroundIsolate: true,
),
web: DriftWebOptions(
sqlite3Wasm: Uri.parse('sqlite3.wasm'),
driftWorker: Uri.parse('drift_worker.js'),
),
);
}
}
Key insights: - All settings – Applied in
beforeOpen()- Logging – Track what's being configured - Integrity check – Verify database health - Optimization – RunPRAGMA optimize- Platform awareness – Different settings for different platforms
Platform-Specific Configuration
Different settings for different platforms
// lib/database/database_config.dart
import 'dart:io';
import 'package:drift/drift.dart';
import 'package:drift_flutter/drift_flutter.dart';
class DatabaseConfig {
// 👇 Platform-specific configuration
static Future<void> configure(AppDatabase db) async {
if (Platform.isAndroid) {
await _configureAndroid(db);
} else if (Platform.isIOS) {
await _configureIOS(db);
} else if (Platform.isMacOS || Platform.isWindows || Platform.isLinux) {
await _configureDesktop(db);
} else if (Platform.isBrowser) {
await _configureWeb(db);
}
}
// Android configuration
static Future<void> _configureAndroid(AppDatabase db) async {
print('📱 Configuring for Android...');
await db.customSelect('PRAGMA journal_mode = WAL').get();
await db.customSelect('PRAGMA synchronous = NORMAL').get();
await db.customSelect('PRAGMA foreign_keys = ON').get();
await db.customSelect('PRAGMA cache_size = -10000').get(); // 10MB
// Android-specific optimizations
await db.customSelect('PRAGMA mmap_size = 134217728').get(); // 128MB
}
// iOS configuration
static Future<void> _configureIOS(AppDatabase db) async {
print('📱 Configuring for iOS...');
await db.customSelect('PRAGMA journal_mode = WAL').get();
await db.customSelect('PRAGMA synchronous = FULL').get(); // iOS prefers durability
await db.customSelect('PRAGMA foreign_keys = ON').get();
await db.customSelect('PRAGMA cache_size = -15000').get(); // 15MB
// iOS-specific optimizations
await db.customSelect('PRAGMA mmap_size = 268435456').get(); // 256MB
}
// Desktop configuration
static Future<void> _configureDesktop(AppDatabase db) async {
print('💻 Configuring for Desktop...');
await db.customSelect('PRAGMA journal_mode = WAL').get();
await db.customSelect('PRAGMA synchronous = NORMAL').get();
await db.customSelect('PRAGMA foreign_keys = ON').get();
await db.customSelect('PRAGMA cache_size = -50000').get(); // 50MB
// Desktop-specific optimizations
await db.customSelect('PRAGMA mmap_size = 536870912').get(); // 512MB
await db.customSelect('PRAGMA journal_size_limit = 268435456').get(); // 256MB
}
// Web configuration
static Future<void> _configureWeb(AppDatabase db) async {
print('🌐 Configuring for Web...');
// Web has limited memory, use conservative settings
await db.customSelect('PRAGMA journal_mode = MEMORY').get();
await db.customSelect('PRAGMA synchronous = NORMAL').get();
await db.customSelect('PRAGMA foreign_keys = ON').get();
await db.customSelect('PRAGMA cache_size = -5000').get(); // 5MB
await db.customSelect('PRAGMA temp_store = MEMORY').get();
}
}
Key insights: - Platform detection – Configure based on platform - Android – Focus on performance - iOS – Focus on durability - Desktop – More memory available - Web – Conservative settings for browser
Real-World Example
Production database configuration with monitoring
// lib/database/database.dart
import 'package:drift/drift.dart';
import 'package:drift_flutter/drift_flutter.dart';
import 'package:path_provider/path_provider.dart';
import 'dart:developer' as dev;
import 'tables/users.dart';
import 'tables/posts.dart';
import 'tables/comments.dart';
part 'database.g.dart';
@DriftDatabase(tables: [Users, Posts, Comments])
class AppDatabase extends _$AppDatabase {
AppDatabase([QueryExecutor? executor]) : super(executor ?? _openConnection());
@override
int get schemaVersion => 1;
// 👇 Production configuration
@override
Future<void> beforeOpen() async {
await super.beforeOpen();
try {
// 1️⃣ Transaction for atomic configuration
await transaction(() async {
// Performance settings
await customSelect('PRAGMA journal_mode = WAL').get();
await customSelect('PRAGMA synchronous = NORMAL').get();
await customSelect('PRAGMA foreign_keys = ON').get();
await customSelect('PRAGMA cache_size = -20000').get(); // 20MB
await customSelect('PRAGMA temp_store = MEMORY').get();
await customSelect('PRAGMA mmap_size = 268435456').get(); // 256MB
await customSelect('PRAGMA auto_vacuum = INCREMENTAL').get();
await customSelect('PRAGMA journal_size_limit = 67108864').get(); // 64MB
// 2️⃣ Optimize
await customSelect('PRAGMA optimize').get();
});
// 3️⃣ Health check
await _performHealthCheck();
dev.log('✅ Database configured successfully');
} catch (e) {
dev.log('❌ Database configuration failed: $e');
rethrow;
}
}
// 👇 Health check
Future<void> _performHealthCheck() async {
// Check integrity
final integrity = await customSelect('PRAGMA integrity_check').get();
final status = integrity.first.data['integrity_check'] as String;
if (status != 'ok') {
throw Exception('Database integrity check failed: $status');
}
// Check foreign keys
final fk = await customSelect('PRAGMA foreign_keys').get();
if (fk.first.data['foreign_keys'] != 1) {
dev.log('⚠️ Foreign keys not enabled');
}
// Check journal mode
final journal = await customSelect('PRAGMA journal_mode').get();
dev.log('📝 Journal mode: ${journal.first.data['journal_mode']}');
// Check cache size
final cache = await customSelect('PRAGMA cache_size').get();
dev.log('💾 Cache size: ${cache.first.data['cache_size']}');
}
// 👇 Get configuration report
Future<Map<String, dynamic>> getConfigurationReport() async {
final journal = await customSelect('PRAGMA journal_mode').get();
final sync = await customSelect('PRAGMA synchronous').get();
final fk = await customSelect('PRAGMA foreign_keys').get();
final cache = await customSelect('PRAGMA cache_size').get();
final mmap = await customSelect('PRAGMA mmap_size').get();
final vacuum = await customSelect('PRAGMA auto_vacuum').get();
final pageSize = await customSelect('PRAGMA page_size').get();
return {
'journal_mode': journal.first.data['journal_mode'],
'synchronous': sync.first.data['synchronous'],
'foreign_keys': fk.first.data['foreign_keys'] == 1,
'cache_size': cache.first.data['cache_size'],
'mmap_size': mmap.first.data['mmap_size'],
'auto_vacuum': vacuum.first.data['auto_vacuum'],
'page_size': pageSize.first.data['page_size'],
};
}
static QueryExecutor _openConnection() {
return driftDatabase(
name: 'my_app',
native: const DriftNativeOptions(
databaseDirectory: getApplicationSupportDirectory,
enableBackgroundIsolate: true,
),
web: DriftWebOptions(
sqlite3Wasm: Uri.parse('sqlite3.wasm'),
driftWorker: Uri.parse('drift_worker.js'),
),
);
}
}
// lib/main.dart
import 'package:flutter/material.dart';
import 'database/database.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final db = AppDatabase();
// Check configuration
final config = await db.getConfigurationReport();
print('📊 Database Configuration:');
config.forEach((key, value) {
print(' $key: $value');
});
runApp(MyApp(db: db));
}
class MyApp extends StatelessWidget {
final AppDatabase db;
const MyApp({required this.db});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: HomePage(db: db),
);
}
}
class HomePage extends StatelessWidget {
final AppDatabase db;
const HomePage({required this.db});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Database Demo')),
body: FutureBuilder(
future: db.getConfigurationReport(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Center(child: CircularProgressIndicator());
}
final config = snapshot.data!;
return ListView(
children: config.entries.map((entry) {
return ListTile(
title: Text(entry.key.replaceAll('_', ' ').toUpperCase()),
subtitle: Text(entry.value.toString()),
);
}).toList(),
);
},
),
);
}
}
Benefits of this approach: - Atomic configuration – All settings applied together - Health monitoring – Check database status - Configuration report – View current settings - Error handling – Catch configuration errors - Logging – Track configuration process - Testing – Verify settings in tests
Best Practices
- Apply settings in beforeOpen() – Before database is used
- Use WAL mode – For concurrent access
- Enable foreign keys – Maintain data integrity
- Set appropriate cache size – Balance performance and memory
- Use memory temp store – For better performance
- Run PRAGMA optimize – Periodically for performance
- Check integrity – Verify database health
- Test configuration – Verify settings in tests
- Log configuration – Track what's applied
- Platform-specific – Different settings for different platforms
Common Mistakes
Mistake 1: Setting PRAGMA after tables are created
Wrong:
// 🚫 Page size must be set before tables
@override
Future<void> beforeOpen() async {
await customSelect('PRAGMA page_size = 8192').get();
// Tables already created with default page size
}
Correct:
// ✅ Set page size at creation time
@override
MigrationStrategy get migration => MigrationStrategy(
onCreate: (migrator) async {
await customSelect('PRAGMA page_size = 8192').get();
await migrator.createAll();
},
);
Mistake 2: Not enabling foreign keys
Wrong:
// 🚫 Foreign key constraints ignored
@override
Future<void> beforeOpen() async {
// Missing foreign_keys = ON
}
Correct:
// ✅ Always enable foreign keys
@override
Future<void> beforeOpen() async {
await customSelect('PRAGMA foreign_keys = ON').get();
}
Mistake 3: Using incompatible settings
Wrong:
// 🚫 readPool requires WAL mode
@override
Future<void> beforeOpen() async {
await customSelect('PRAGMA journal_mode = DELETE').get();
}
// readPool: 4 // Will cause "database locked" errors
Correct:
// ✅ Enable WAL for readPool
@override
Future<void> beforeOpen() async {
await customSelect('PRAGMA journal_mode = WAL').get();
}
Summary
| Setting | Recommended | Purpose |
|---|---|---|
| Journal Mode | WAL | Concurrent access |
| Synchronous | NORMAL | Performance vs durability |
| Foreign Keys | ON | Data integrity |
| Cache Size | -10000 or more | Performance |
| Temp Store | MEMORY | Faster temp operations |
| MMAP Size | 256MB+ | Faster reads |
| Auto Vacuum | INCREMENTAL | Space management |
| Journal Limit | 64MB+ | Prevent file growth |
Next Steps
Now you understand database configuration, let's dive deeper:
- Multiple Databases – Working with multiple databases
- Tables Deep Dive – Advanced table definitions
- Performance – Advanced performance optimization
Did You Know?
-
WAL mode enables up to 50% better performance – For concurrent reads and writes
-
PRAGMA optimize analyzes all queries – And creates indexes automatically
-
MMAP size can be larger than the database – It maps the entire file
-
Cache size uses negative numbers for KB – Positive numbers are pages
-
Foreign keys are OFF by default – You must enable them
-
Auto vacuum can be run manually – With
VACUUMcommand -
PRAGMA integrity_check – Can detect corruption early
-
Settings persist – They stay until the database is closed