axiom-grdb by charleswiltgen/axiom
npx skills add https://github.com/charleswiltgen/axiom --skill axiom-grdb使用 GRDB.swift 直接访问 SQLite —— 一个用于 SQLite 数据库的工具包,提供类型安全的查询、迁移和响应式观察。
核心原则 围绕原生 SQL 的类型安全 Swift 包装器,在需要时提供完整的 SQLite 功能。
要求 iOS 13+, Swift 5.7+ 许可证 MIT (免费且开源)
注意: SQLiteData 现在通过查询构建器支持 GROUP BY (.group(by:)) 和 HAVING (.having()) —— 请参阅 axiom-sqlitedata-ref 技能。
@Table 模型足够使用广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
关于迁移 请参阅 axiom-database-migration 技能以了解安全的模式演进模式。
这些是开发者提出的真实问题,本技能旨在回答:
→ 本技能展示涉及多表和聚合的复杂 JOIN 查询
→ 本技能涵盖用于响应式查询更新的 ValueObservation 模式
→ 本技能解释迁移注册、数据转换和安全回滚模式
→ 本技能涵盖 EXPLAIN QUERY PLAN、用于性能分析的 database.trace 以及索引创建
→ 本技能演示何时 GRDB 的原生 SQL 比类型安全包装器更清晰
import GRDB
// 基于文件的数据库
let dbPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
let dbQueue = try DatabaseQueue(path: "\(dbPath)/db.sqlite")
// 内存数据库 (测试用)
let dbQueue = try DatabaseQueue()
// 适用于具有大量并发访问的应用
let dbPool = try DatabasePool(path: dbPath)
使用 Queue 适用于 大多数应用 (更简单,足够) 使用 Pool 适用于 来自多个线程的大量并发写入
struct Track: Codable {
var id: String
var title: String
var artist: String
var duration: TimeInterval
}
// 获取
let tracks = try dbQueue.read { db in
try Track.fetchAll(db, sql: "SELECT * FROM tracks")
}
// 插入
try dbQueue.write { db in
try track.insert(db) // Codable 一致性提供了 insert 方法
}
struct TrackInfo: FetchableRecord {
var title: String
var artist: String
var albumTitle: String
init(row: Row) {
title = row["title"]
artist = row["artist"]
albumTitle = row["album_title"]
}
}
let results = try dbQueue.read { db in
try TrackInfo.fetchAll(db, sql: """
SELECT tracks.title, tracks.artist, albums.title as album_title
FROM tracks
JOIN albums ON tracks.albumId = albums.id
""")
}
struct Track: Codable, PersistableRecord {
var id: String
var title: String
// 自定义表名
static let databaseTableName = "tracks"
}
try dbQueue.write { db in
var track = Track(id: "1", title: "Song")
try track.insert(db)
track.title = "Updated"
try track.update(db)
try track.delete(db)
}
// 获取所有行
let rows = try dbQueue.read { db in
try Row.fetchAll(db, sql: "SELECT * FROM tracks WHERE genre = ?", arguments: ["Rock"])
}
// 获取单个值
let count = try dbQueue.read { db in
try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM tracks")
}
// 获取到 Codable 对象
let tracks = try dbQueue.read { db in
try Track.fetchAll(db, sql: "SELECT * FROM tracks ORDER BY title")
}
try dbQueue.write { db in
try db.execute(sql: """
INSERT INTO tracks (id, title, artist, duration)
VALUES (?, ?, ?, ?)
""", arguments: ["1", "Song", "Artist", 240])
}
try dbQueue.write { db in
// 自动事务 - 要么全部成功,要么全部失败
for track in tracks {
try track.insert(db)
}
// 成功时自动提交,出错时回滚
}
let request = Track
.filter(Column("genre") == "Rock")
.filter(Column("duration") > 180)
let tracks = try dbQueue.read { db in
try request.fetchAll(db)
}
let request = Track
.order(Column("title").asc)
.limit(10)
struct TrackWithAlbum: FetchableRecord {
var trackTitle: String
var albumTitle: String
}
let request = Track
.joining(required: Track.belongsTo(Album.self))
.select(Column("title").forKey("trackTitle"), Column("album_title").forKey("albumTitle"))
let results = try dbQueue.read { db in
try TrackWithAlbum.fetchAll(db, request)
}
let sql = """
SELECT
tracks.title as track_title,
albums.title as album_title,
artists.name as artist_name,
COUNT(plays.id) as play_count
FROM tracks
JOIN albums ON tracks.albumId = albums.id
JOIN artists ON albums.artistId = artists.id
LEFT JOIN plays ON plays.trackId = tracks.id
WHERE artists.genre = ?
GROUP BY tracks.id
HAVING play_count > 10
ORDER BY play_count DESC
LIMIT 50
"""
struct TrackStats: FetchableRecord {
var trackTitle: String
var albumTitle: String
var artistName: String
var playCount: Int
init(row: Row) {
trackTitle = row["track_title"]
albumTitle = row["album_title"]
artistName = row["artist_name"]
playCount = row["play_count"]
}
}
let stats = try dbQueue.read { db in
try TrackStats.fetchAll(db, sql: sql, arguments: ["Rock"])
}
import GRDB
import Combine
let observation = ValueObservation.tracking { db in
try Track.fetchAll(db)
}
// 使用 Combine 开始观察
let cancellable = observation.publisher(in: dbQueue)
.sink(
receiveCompletion: { _ in },
receiveValue: { tracks in
print("Tracks updated: \(tracks.count)")
}
)
import GRDB
import GRDBQuery // https://github.com/groue/GRDBQuery
@Query(Tracks())
var tracks: [Track]
struct Tracks: Queryable {
static var defaultValue: [Track] { [] }
func publisher(in dbQueue: DatabaseQueue) -> AnyPublisher<[Track], Error> {
ValueObservation
.tracking { db in try Track.fetchAll(db) }
.publisher(in: dbQueue)
.eraseToAnyPublisher()
}
}
请参阅 GRDBQuery 文档 以了解 SwiftUI 响应式绑定。
func observeGenre(_ genre: String) -> ValueObservation<[Track]> {
ValueObservation.tracking { db in
try Track
.filter(Column("genre") == genre)
.fetchAll(db)
}
}
let cancellable = observeGenre("Rock")
.publisher(in: dbQueue)
.sink { tracks in
print("Rock tracks: \(tracks.count)")
}
var migrator = DatabaseMigrator()
// 迁移 1: 创建表
migrator.registerMigration("v1") { db in
try db.create(table: "tracks") { t in
t.column("id", .text).primaryKey()
t.column("title", .text).notNull()
t.column("artist", .text).notNull()
t.column("duration", .real).notNull()
}
}
// 迁移 2: 添加列
migrator.registerMigration("v2_add_genre") { db in
try db.alter(table: "tracks") { t in
t.add(column: "genre", .text)
}
}
// 迁移 3: 添加索引
migrator.registerMigration("v3_add_indexes") { db in
try db.create(index: "idx_genre", on: "tracks", columns: ["genre"])
}
// 运行迁移
try migrator.migrate(dbQueue)
关于迁移安全模式 请参阅 axiom-database-migration 技能。
migrator.registerMigration("v4_normalize_artists") { db in
// 1. 创建新表
try db.create(table: "artists") { t in
t.column("id", .text).primaryKey()
t.column("name", .text).notNull()
}
// 2. 提取唯一艺术家
try db.execute(sql: """
INSERT INTO artists (id, name)
SELECT DISTINCT
lower(replace(artist, ' ', '_')) as id,
artist as name
FROM tracks
""")
// 3. 向 tracks 添加外键
try db.alter(table: "tracks") { t in
t.add(column: "artistId", .text)
.references("artists", onDelete: .cascade)
}
// 4. 填充外键
try db.execute(sql: """
UPDATE tracks
SET artistId = (
SELECT id FROM artists
WHERE artists.name = tracks.artist
)
""")
}
try dbQueue.write { db in
for batch in tracks.chunked(into: 500) {
for track in batch {
try track.insert(db)
}
}
}
try dbQueue.write { db in
let statement = try db.makeStatement(sql: """
INSERT INTO tracks (id, title, artist, duration)
VALUES (?, ?, ?, ?)
""")
for track in tracks {
try statement.execute(arguments: [track.id, track.title, track.artist, track.duration])
}
}
try db.create(index: "idx_tracks_artist", on: "tracks", columns: ["artist"])
try db.create(index: "idx_tracks_genre_duration", on: "tracks", columns: ["genre", "duration"])
// 唯一索引
try db.create(index: "idx_tracks_unique_title", on: "tracks", columns: ["title"], unique: true)
// 分析查询性能
let explanation = try dbQueue.read { db in
try String.fetchOne(db, sql: "EXPLAIN QUERY PLAN SELECT * FROM tracks WHERE artist = ?", arguments: ["Artist"])
}
print(explanation)
当使用 SQLiteData 但需要 GRDB 进行特定操作时:
import SQLiteData
import GRDB
@Dependency(\.database) var database // SQLiteData Database
// 访问底层的 GRDB DatabaseQueue
try await database.database.write { db in
// 此处拥有完整的 GRDB 功能
try db.execute(sql: "CREATE INDEX idx_genre ON tracks(genre)")
}
// 读取单个值
let count = try db.fetchOne(Int.self, sql: "SELECT COUNT(*) FROM tracks")
// 读取所有行
let rows = try Row.fetchAll(db, sql: "SELECT * FROM tracks WHERE genre = ?", arguments: ["Rock"])
// 写入
try db.execute(sql: "INSERT INTO tracks VALUES (?, ?, ?)", arguments: [id, title, artist])
// 事务
try dbQueue.write { db in
// 要么全部成功,要么全部失败
}
// 观察变更
ValueObservation.tracking { db in
try Track.fetchAll(db)
}.publisher(in: dbQueue)
GitHub : groue/GRDB.swift, groue/GRDBQuery
文档 : sqlite.org/docs.html
技能 : axiom-database-migration, axiom-sqlitedata, axiom-swiftdata
如果你发现以下任何症状:
database.trace 进行分析EXPLAIN QUERY PLAN 来理解执行过程var database = try DatabaseQueue(path: dbPath)
// 启用追踪以查看 SQL 执行
database.trace { print($0) }
// 运行慢查询
try database.read { db in
let results = try Track.fetchAll(db) // 观察输出中的执行时间
}
// 使用 EXPLAIN QUERY PLAN 来理解执行过程:
try database.read { db in
let plan = try String(fetching: db, sql: "EXPLAIN QUERY PLAN SELECT ...")
print(plan)
// 查找 SCAN (慢,全表扫描) 与 SEARCH (快,索引查找)
}
// 在频繁查询的列上添加索引
try database.write { db in
try db.execute(sql: "CREATE INDEX idx_plays_track_id ON plays(track_id)")
}
// 对数据库的任何写入都会重新评估查询
ValueObservation.tracking { db in
try Track.fetchAll(db)
}.start(in: database, onError: { }, onChange: { tracks in
// 每次变更都会调用 —— CPU 飙升!
})
// 合并快速更新 (推荐)
ValueObservation.tracking { db in
try Track.fetchAll(db)
}.removeDuplicates() // 跳过重复结果
.debounce(for: 0.5, scheduler: DispatchQueue.main) // 批量更新
.start(in: database, ...)
.tracking.removeDuplicates() + .debounce()var migrator = DatabaseMigrator()
migrator.registerMigration("v1_initial") { db in
try db.execute(sql: "CREATE TABLE tracks (...)")
}
migrator.registerMigration("v2_add_plays") { db in
try db.execute(sql: "CREATE TABLE plays (...)")
}
// GRDB 保证:
// - 每个迁移只运行一次
// - 按顺序运行 (v1, 然后 v2)
// - 多次调用 migrate() 是安全的
try migrator.migrate(dbQueue)
migrate() 两次只会执行新的迁移for track in 50000Tracks {
try dbQueue.write { db in try track.insert(db) } // 5 万次事务!
}
修复 使用带批处理的单次事务
let tracks = try dbQueue.read { db in try Track.fetchAll(db) } // 阻塞 UI
修复 使用 async/await 或调度到后台队列
// 没有索引的慢查询
try Track.filter(Column("genre") == "Rock").fetchAll(db)
修复 在频繁查询的列上创建索引
for track in tracks {
let album = try Album.fetchOne(db, key: track.albumId) // N 次查询!
}
修复 使用 JOIN 或批量获取
tvOS 上的本地 GRDB 数据库不是持久化的。 系统在存储压力下会删除缓存 (包括 Application Support)。纯本地的 GRDB 数据库将在应用启动之间丢失所有数据。
如果目标平台是 tvOS ,请将 GRDB 与 CloudKit 同步配对 (通过 CKSyncEngine 或 SQLiteData 的 SyncEngine),这样 iCloud 就是持久化存储,而本地数据库是可重建的缓存。请参阅 axiom-tvos 以了解完整的 tvOS 存储限制。
目标平台: iOS 13+, Swift 5.7+ 框架: GRDB.swift 6.0+ 历史: 请参阅 git log 了解变更
每周安装量
100
代码库
GitHub 星标数
606
首次出现
2026 年 1 月 21 日
安全审计
安装于
opencode86
gemini-cli80
codex80
claude-code80
github-copilot75
cursor73
Direct SQLite access using GRDB.swift — a toolkit for SQLite databases with type-safe queries, migrations, and reactive observation.
Core principle Type-safe Swift wrapper around raw SQL with full SQLite power when you need it.
Requires iOS 13+, Swift 5.7+ License MIT (free and open source)
Note: SQLiteData now supports GROUP BY (.group(by:)) and HAVING (.having()) via the query builder — see the axiom-sqlitedata-ref skill.
@Table models are sufficientFor migrations See the axiom-database-migration skill for safe schema evolution patterns.
These are real questions developers ask that this skill is designed to answer:
→ The skill shows complex JOIN queries with multiple tables and aggregations
→ The skill covers ValueObservation patterns for reactive query updates
→ The skill explains migration registration, data transforms, and safe rollback patterns
→ The skill covers EXPLAIN QUERY PLAN, database.trace for profiling, and index creation
→ The skill demonstrates when GRDB's raw SQL is clearer than type-safe wrappers
import GRDB
// File-based database
let dbPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
let dbQueue = try DatabaseQueue(path: "\(dbPath)/db.sqlite")
// In-memory database (tests)
let dbQueue = try DatabaseQueue()
// For apps with heavy concurrent access
let dbPool = try DatabasePool(path: dbPath)
Use Queue for Most apps (simpler, sufficient) Use Pool for Heavy concurrent writes from multiple threads
struct Track: Codable {
var id: String
var title: String
var artist: String
var duration: TimeInterval
}
// Fetch
let tracks = try dbQueue.read { db in
try Track.fetchAll(db, sql: "SELECT * FROM tracks")
}
// Insert
try dbQueue.write { db in
try track.insert(db) // Codable conformance provides insert
}
struct TrackInfo: FetchableRecord {
var title: String
var artist: String
var albumTitle: String
init(row: Row) {
title = row["title"]
artist = row["artist"]
albumTitle = row["album_title"]
}
}
let results = try dbQueue.read { db in
try TrackInfo.fetchAll(db, sql: """
SELECT tracks.title, tracks.artist, albums.title as album_title
FROM tracks
JOIN albums ON tracks.albumId = albums.id
""")
}
struct Track: Codable, PersistableRecord {
var id: String
var title: String
// Customize table name
static let databaseTableName = "tracks"
}
try dbQueue.write { db in
var track = Track(id: "1", title: "Song")
try track.insert(db)
track.title = "Updated"
try track.update(db)
try track.delete(db)
}
// Fetch all rows
let rows = try dbQueue.read { db in
try Row.fetchAll(db, sql: "SELECT * FROM tracks WHERE genre = ?", arguments: ["Rock"])
}
// Fetch single value
let count = try dbQueue.read { db in
try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM tracks")
}
// Fetch into Codable
let tracks = try dbQueue.read { db in
try Track.fetchAll(db, sql: "SELECT * FROM tracks ORDER BY title")
}
try dbQueue.write { db in
try db.execute(sql: """
INSERT INTO tracks (id, title, artist, duration)
VALUES (?, ?, ?, ?)
""", arguments: ["1", "Song", "Artist", 240])
}
try dbQueue.write { db in
// Automatic transaction - all or nothing
for track in tracks {
try track.insert(db)
}
// Commits automatically on success, rolls back on error
}
let request = Track
.filter(Column("genre") == "Rock")
.filter(Column("duration") > 180)
let tracks = try dbQueue.read { db in
try request.fetchAll(db)
}
let request = Track
.order(Column("title").asc)
.limit(10)
struct TrackWithAlbum: FetchableRecord {
var trackTitle: String
var albumTitle: String
}
let request = Track
.joining(required: Track.belongsTo(Album.self))
.select(Column("title").forKey("trackTitle"), Column("album_title").forKey("albumTitle"))
let results = try dbQueue.read { db in
try TrackWithAlbum.fetchAll(db, request)
}
let sql = """
SELECT
tracks.title as track_title,
albums.title as album_title,
artists.name as artist_name,
COUNT(plays.id) as play_count
FROM tracks
JOIN albums ON tracks.albumId = albums.id
JOIN artists ON albums.artistId = artists.id
LEFT JOIN plays ON plays.trackId = tracks.id
WHERE artists.genre = ?
GROUP BY tracks.id
HAVING play_count > 10
ORDER BY play_count DESC
LIMIT 50
"""
struct TrackStats: FetchableRecord {
var trackTitle: String
var albumTitle: String
var artistName: String
var playCount: Int
init(row: Row) {
trackTitle = row["track_title"]
albumTitle = row["album_title"]
artistName = row["artist_name"]
playCount = row["play_count"]
}
}
let stats = try dbQueue.read { db in
try TrackStats.fetchAll(db, sql: sql, arguments: ["Rock"])
}
import GRDB
import Combine
let observation = ValueObservation.tracking { db in
try Track.fetchAll(db)
}
// Start observing with Combine
let cancellable = observation.publisher(in: dbQueue)
.sink(
receiveCompletion: { _ in },
receiveValue: { tracks in
print("Tracks updated: \(tracks.count)")
}
)
import GRDB
import GRDBQuery // https://github.com/groue/GRDBQuery
@Query(Tracks())
var tracks: [Track]
struct Tracks: Queryable {
static var defaultValue: [Track] { [] }
func publisher(in dbQueue: DatabaseQueue) -> AnyPublisher<[Track], Error> {
ValueObservation
.tracking { db in try Track.fetchAll(db) }
.publisher(in: dbQueue)
.eraseToAnyPublisher()
}
}
See GRDBQuery documentation for SwiftUI reactive bindings.
func observeGenre(_ genre: String) -> ValueObservation<[Track]> {
ValueObservation.tracking { db in
try Track
.filter(Column("genre") == genre)
.fetchAll(db)
}
}
let cancellable = observeGenre("Rock")
.publisher(in: dbQueue)
.sink { tracks in
print("Rock tracks: \(tracks.count)")
}
var migrator = DatabaseMigrator()
// Migration 1: Create tables
migrator.registerMigration("v1") { db in
try db.create(table: "tracks") { t in
t.column("id", .text).primaryKey()
t.column("title", .text).notNull()
t.column("artist", .text).notNull()
t.column("duration", .real).notNull()
}
}
// Migration 2: Add column
migrator.registerMigration("v2_add_genre") { db in
try db.alter(table: "tracks") { t in
t.add(column: "genre", .text)
}
}
// Migration 3: Add index
migrator.registerMigration("v3_add_indexes") { db in
try db.create(index: "idx_genre", on: "tracks", columns: ["genre"])
}
// Run migrations
try migrator.migrate(dbQueue)
For migration safety patterns See the axiom-database-migration skill.
migrator.registerMigration("v4_normalize_artists") { db in
// 1. Create new table
try db.create(table: "artists") { t in
t.column("id", .text).primaryKey()
t.column("name", .text).notNull()
}
// 2. Extract unique artists
try db.execute(sql: """
INSERT INTO artists (id, name)
SELECT DISTINCT
lower(replace(artist, ' ', '_')) as id,
artist as name
FROM tracks
""")
// 3. Add foreign key to tracks
try db.alter(table: "tracks") { t in
t.add(column: "artistId", .text)
.references("artists", onDelete: .cascade)
}
// 4. Populate foreign keys
try db.execute(sql: """
UPDATE tracks
SET artistId = (
SELECT id FROM artists
WHERE artists.name = tracks.artist
)
""")
}
try dbQueue.write { db in
for batch in tracks.chunked(into: 500) {
for track in batch {
try track.insert(db)
}
}
}
try dbQueue.write { db in
let statement = try db.makeStatement(sql: """
INSERT INTO tracks (id, title, artist, duration)
VALUES (?, ?, ?, ?)
""")
for track in tracks {
try statement.execute(arguments: [track.id, track.title, track.artist, track.duration])
}
}
try db.create(index: "idx_tracks_artist", on: "tracks", columns: ["artist"])
try db.create(index: "idx_tracks_genre_duration", on: "tracks", columns: ["genre", "duration"])
// Unique index
try db.create(index: "idx_tracks_unique_title", on: "tracks", columns: ["title"], unique: true)
// Analyze query performance
let explanation = try dbQueue.read { db in
try String.fetchOne(db, sql: "EXPLAIN QUERY PLAN SELECT * FROM tracks WHERE artist = ?", arguments: ["Artist"])
}
print(explanation)
When using SQLiteData but need GRDB for specific operations:
import SQLiteData
import GRDB
@Dependency(\.database) var database // SQLiteData Database
// Access underlying GRDB DatabaseQueue
try await database.database.write { db in
// Full GRDB power here
try db.execute(sql: "CREATE INDEX idx_genre ON tracks(genre)")
}
// Read single value
let count = try db.fetchOne(Int.self, sql: "SELECT COUNT(*) FROM tracks")
// Read all rows
let rows = try Row.fetchAll(db, sql: "SELECT * FROM tracks WHERE genre = ?", arguments: ["Rock"])
// Write
try db.execute(sql: "INSERT INTO tracks VALUES (?, ?, ?)", arguments: [id, title, artist])
// Transaction
try dbQueue.write { db in
// All or nothing
}
// Observe changes
ValueObservation.tracking { db in
try Track.fetchAll(db)
}.publisher(in: dbQueue)
GitHub : groue/GRDB.swift, groue/GRDBQuery
Docs : sqlite.org/docs.html
Skills : axiom-database-migration, axiom-sqlitedata, axiom-swiftdata
If you see ANY of these symptoms:
database.traceEXPLAIN QUERY PLAN to understand executionvar database = try DatabaseQueue(path: dbPath)
// Enable tracing to see SQL execution
database.trace { print($0) }
// Run the slow query
try database.read { db in
let results = try Track.fetchAll(db) // Watch output for execution time
}
// Use EXPLAIN QUERY PLAN to understand execution:
try database.read { db in
let plan = try String(fetching: db, sql: "EXPLAIN QUERY PLAN SELECT ...")
print(plan)
// Look for SCAN (slow, full table) vs SEARCH (fast, indexed)
}
// Add index on frequently queried column
try database.write { db in
try db.execute(sql: "CREATE INDEX idx_plays_track_id ON plays(track_id)")
}
// Re-evaluates query on ANY write to database
ValueObservation.tracking { db in
try Track.fetchAll(db)
}.start(in: database, onError: { }, onChange: { tracks in
// Called for every change — CPU spike!
})
// Coalesce rapid updates (recommended)
ValueObservation.tracking { db in
try Track.fetchAll(db)
}.removeDuplicates() // Skip duplicate results
.debounce(for: 0.5, scheduler: DispatchQueue.main) // Batch updates
.start(in: database, ...)
.tracking.removeDuplicates() + .debounce()var migrator = DatabaseMigrator()
migrator.registerMigration("v1_initial") { db in
try db.execute(sql: "CREATE TABLE tracks (...)")
}
migrator.registerMigration("v2_add_plays") { db in
try db.execute(sql: "CREATE TABLE plays (...)")
}
// GRDB guarantees:
// - Each migration runs exactly ONCE
// - In order (v1, then v2)
// - Safe to call migrate() multiple times
try migrator.migrate(dbQueue)
migrate() twice only executes new onesfor track in 50000Tracks {
try dbQueue.write { db in try track.insert(db) } // 50k transactions!
}
Fix Single transaction with batches
let tracks = try dbQueue.read { db in try Track.fetchAll(db) } // Blocks UI
Fix Use async/await or dispatch to background queue
// Slow query without index
try Track.filter(Column("genre") == "Rock").fetchAll(db)
Fix Create indexes on frequently queried columns
for track in tracks {
let album = try Album.fetchOne(db, key: track.albumId) // N queries!
}
Fix Use JOIN or batch fetch
Local GRDB databases are not persistent on tvOS. The system deletes Caches (including Application Support) under storage pressure. A local-only GRDB database will lose all data between app launches.
If targeting tvOS , pair GRDB with CloudKit sync (via CKSyncEngine or SQLiteData's SyncEngine) so iCloud is the persistent store and the local database is a rebuildable cache. See axiom-tvos for full tvOS storage constraints.
Targets: iOS 13+, Swift 5.7+ Framework: GRDB.swift 6.0+ History: See git log for changes
Weekly Installs
100
Repository
GitHub Stars
606
First Seen
Jan 21, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
opencode86
gemini-cli80
codex80
claude-code80
github-copilot75
cursor73
SQL查询优化指南:PostgreSQL、Snowflake、BigQuery高性能SQL编写技巧与方言参考
951 周安装