SwiftData 的效能其實是儲存問題
WWDC 2026 的 SwiftData 小組實驗室,由真正掌管這套框架底層的人坐鎮,其中包括負責維護 Apple 各平台 SQLite 的工程師,以及 Core Data 與 SwiftData 的經理。他們回答的主軸,是對多數開發者追求效能的方式做了一次有用的修正:一旦 SwiftData 進入你的 App,昂貴的是 I/O,而不是你的 Swift 程式碼;效能的提升來自讀得更少、理解儲存引擎,而非靠堆疊並行。以下內容多半立基於 Apple 與 SQLite 的官方文件;凡屬實驗室的工程推論而非有文件佐證的事實,都會特別標明。
WWDC 2026 SwiftData 小組實驗室。
重點摘要
- SwiftData 的 SQLite 儲存區預設採用 write-ahead logging(WAL),這意味著多個讀取者可與單一寫入者並行執行。它並不是讀寫鎖(reader/writer lock);實驗室指出,這個區別開發者經常搞錯。23
- 在不具現化物件的情況下讀取:
fetchCount(_:)回傳符合條件的數量,fetchIdentifiers(_:)回傳[PersistentIdentifier],兩者都不會載入完整模型。把它們與歷史觀測搭配使用,就能判斷是否真有必要重新整理。45 @Model物件並非Sendable,也不該硬要讓它變成Sendable。若要跨越 actor 邊界,請傳遞PersistentIdentifier(它是Sendable)以及任何已抽取出的值,再於目的端的 context 重新擷取。6- SwiftData 沒有等同於 Core Data 那種下推到 SQL 的聚合查詢(sum、average、min、max)。其逃生出口是共存(coexistence):對同一個儲存檔案執行一套 Core Data 堆疊,由它來計算聚合值。8
- 實驗室的效能訊息是:在假定資料庫太慢之前,先剖析出 SwiftUI 為何重新擷取,因為視圖過度失效讀起來很像 I/O 問題,但其實不是。110
WAL:多個讀取者、一個寫入者,而不是鎖
實驗室帶來最有用的一項修正,與儲存層的並行有關。SwiftData 建構於 Core Data 的 SQLite 儲存區之上,而自 iOS 7 起,該儲存區的預設值便是 write-ahead logging。3 在 WAL 之下,正如 SQLite 文件所述:「WAL 提供更高的並行度,因為讀取者不會阻擋寫入者,寫入者也不會阻擋讀取者。讀取與寫入可以並行進行。」2 同一時間仍然只有一個寫入者,但把資料庫的所有存取都序列化的那種互斥鎖(mutex)心智模型是錯的:你的讀取不必排在寫入後面等待。
實驗室的說法(轉述自錄音)是:人們把單一寫入者規則當成讀寫鎖,於是圍繞一個根本不存在的限制來設計架構。1 正確的模型是 WAL 模型:為大量並行讀取與序列化寫入而設計,而非為全域互斥而設計。
讀得更少:在不具現化的情況下計數與識別
如果成本在於 I/O,那麼最有槓桿效益的一招,就是別再載入你不需要的物件。SwiftData 為此提供了兩個原語,兩者都已在 API 中驗證:
ModelContext 上的 fetchCount(_:) 接受一個 FetchDescriptor,回傳符合條件的模型數量(以 Int 表示),且不會具現化其中任何一個。4 當你只是要為徽章或區段標題取得一個數字時,這嚴格來說比擷取後再呼叫 .count 更省。
fetchIdentifiers(_:) 為某個 descriptor 回傳 [PersistentIdentifier],同樣不會具現化模型;另有 fetchIdentifiers(_:batchSize:) 多載可分批處理。5 實驗室建議的用法(轉述)是把它與歷史觀測搭配:當變更進來時,先擷取受影響的識別碼,與你的視圖實際顯示的內容比對,再決定是否需要重新載入任何東西。1 歷史與觀測 API 本身已在〈SwiftData 在 iOS 27 的觀測與歷史〉中介紹;fetchIdentifiers 則是讓它們得以高效運作的輕量讀取。在 SwiftUI 之外可採用的觀測型別是 ResultsObserver,這是為 2027 年發行版本所推出、以 Swift Observation 為基礎的觀測器,支援與 @Query 相同的原語,包括透過 sectionBy: 進行的鍵路徑分段。9
Sendable 邊界是真實存在的,而模型圖不會跨越它
SwiftData 模型是參考型別,在其 context 內被串接進一張圖中,而它們並非 Sendable。實驗室直言,你無法理智地強迫它們變成 Sendable,因為那張圖並非執行緒安全,而在另一個 actor 上局部具現化它只會帶來麻煩。1 受支援的模式是以 PersistentIdentifier 作為你跨邊界移動的識別身分,它是 Sendable、Hashable 與 Codable。6 把你需要的值抽取到一個 struct 中,附上 PersistentIdentifier,把它交給另一個 actor,若你需要存活的物件,再於目的端的 context 重新擷取該模型。
有一個值得記住的細節:Apple 指出,經解碼的 PersistentIdentifier 與由預設儲存區建立的 PersistentIdentifier 並不總是被視為相等,因此請把該識別碼當成一個穩定的跨 context 控制代碼,而不要假定解碼出的副本就等於存活的那一個。6
同樣「移動身分而非移動圖」的紀律也出現在跨行程的情境中。當你把一個儲存區移入 app group 以便與 widget 或 extension 共享時,預設組態會為你把既有儲存區複製到 app group 容器中;若使用自訂的儲存區 URL,則由你自己管理位置。7 無論哪一種,行程之間都是透過儲存區及其識別碼來協調,而非在彼此之間傳遞存活的物件。
聚合查詢的缺口,以及 Core Data 這道逃生出口
實驗室點名了一項真實的限制:SwiftData 沒有等同於 Core Data 以 NSExpression 為基礎的聚合查詢,也就是那些把 sum、average、min、max 下推到 SQLite、讓資料庫不必載入資料列就完成計算的查詢。8 在 SwiftData 中,你只能擷取資料列再於記憶體中歸納(reduce),而這在大型資料表上會讓整件事失去意義。若是 min 或 max,你可以用排序 descriptor 搭配擷取上限為一的方式取得;若是真正的聚合,實驗室指向了共存。
共存,正如 Apple 在 WWDC 2023 所描述,是「兩套完全分離的持久化堆疊,一套 Core Data 堆疊與一套 SwiftData 堆疊,對著同一個持久化儲存區交談」。8 兩套堆疊指向同一個儲存區 URL;而因為 SwiftData 會自動啟用持久化歷史追蹤,Core Data 那一側也必須啟用 NSPersistentHistoryTrackingKey,否則儲存區會以唯讀方式開啟。8 一旦如此設定妥當,你就能透過 Core Data,對著 SwiftData 所擁有的那個檔案,執行下推到 SQL 的聚合。這比多數 App 所需的機制要繁複,但當你確實需要資料庫端的聚合時,這就是有文件佐證的途徑。
剖析失效本身,而不只是剖析資料庫
實驗室最務實的效能指引(轉述)是:一個 SwiftData App 表面上的 I/O 成本,往往是 SwiftUI 失效問題的偽裝——失效過於頻繁的視圖會重新擷取,而剖析器會把這次重新擷取顯示為資料庫時間,但真正的過錯在於這個視圖根本不該重新整理。1 修正之道,與任何 SwiftUI 效能問題的解法相同,也就是視圖隔離的紀律,已在〈SwiftUI 效能與互通〉中介紹:把大型視圖拆成依賴更窄的小型視圖,並把已擷取的模型往下傳,讓查詢不會再次執行。
工具也支援這種解讀方式。Instruments 隨附一個 SwiftUI 範本,把 SwiftUI instrument 與 Hangs and Hitches instrument 綁在一起;還有一個 File Activity 範本,其 Reads and Writes instrument 會顯示真實的磁碟流量(僅限實體裝置,不含模擬器);以及 Core Data 範本,其 Data Persistence instrument 會回報 fault、擷取與儲存。10 把 SwiftUI 與持久化兩種檢視一起執行,就能告訴你某次重新擷取究竟是貨真價實的讀取,還是由過度失效所觸發的多餘讀取。
實驗室還提出一項關於基準測試的提醒(轉述):從上到下到處都是快取——SQLite 的頁面快取、作業系統的檔案快取,以及儲存控制器——所以一次「快速」的執行,可能只是命中快取,而非真正的改善。請對著一個符合現實的大型資料集進行量測,並使用 File Activity instrument 確認確實發生了 I/O。1
關於增加並行
實驗室最強烈的一項意見,也是應當視為工程推論而非有文件佐證之事實的部分,是對「把並行當成效能修正手段」的告誡。工程師形容 SwiftData 的連線池(connection pooling)是刻意設下界限的,並主張一旦超過少量並行操作,你就會撞上儲存硬體的天花板,因此更多 context 換來的是遞減的報酬,代價卻是更多記憶體與更多 I/O。1 Apple 並未在文件中記載特定的並行上限,所以別從任何人那裡拿一個硬性數字,包括這篇文章在內。站得住腳的結論是方向性的:在快閃儲存裝置上,堆疊並行寫入者並不是讓速度變快的可靠辦法,而 WAL 模型本就免費為你提供了並行讀取。
可以從中帶走什麼
實驗室把 SwiftData 的效能,重新框定在儲存引擎之上。已驗證的槓桿很具體:善用 WAL 的並行讀取,而非懼怕鎖;用 fetchCount 與 fetchIdentifiers 避免具現化物件;在 actor 之間移動 PersistentIdentifier,而非移動模型圖;當你需要真正的聚合時,求助於 Core Data 共存。剖析的紀律則是:在優化資料庫之前,先確認 I/O 成本是真實的,因為罪魁禍首往往是一個本不該重新整理卻重新整理了的視圖。
常見問題
SwiftData 在寫入期間會鎖住資料庫嗎?
不會,至少不是讀寫鎖那種意義上的鎖。該儲存區採用 SQLite write-ahead logging,允許多個讀取者與單一寫入者並行執行;讀取不會阻擋寫入者,寫入者也不會阻擋讀取。23 同一時間只有一個寫入者,但讀取會與它並行進行。
我要如何在不載入記錄的情況下計數或檢查記錄?
使用 ModelContext.fetchCount(_:) 取得符合條件的數量,使用 ModelContext.fetchIdentifiers(_:) 取得 [PersistentIdentifier] 值,兩者都不會具現化模型物件。45 把 fetchIdentifiers 與歷史觀測結合,就能在重新載入前先判斷某項變更是否真的影響到你的視圖所顯示的內容。
我要如何把一個 SwiftData 物件傳給另一個 actor?
你不該傳物件本身。@Model 型別並非 Sendable。請傳遞 PersistentIdentifier(它是 Sendable)以及任何已抽取出的值,再於目的端的 context 重新擷取。6 別把存活的模型圖交付過邊界。
SwiftData 能在資料庫中做 sum/average/min/max 嗎?
不能。SwiftData 沒有等同於 Core Data 以 NSExpression 下推到 SQL 的聚合。8 若是 min/max,可用排序搭配擷取上限為一的方式取得;若是真正的聚合,請對同一個儲存檔案執行一套 Core Data 堆疊(共存),這需要讓兩側使用相同的儲存區 URL,並在 Core Data 那一側啟用持久化歷史追蹤。8
本部落格的 SwiftData 脈絡,在〈結構描述紀律〉與〈遷移指南〉中涵蓋結構描述與遷移紀律,並在〈觀測與歷史〉中涵蓋 iOS 27 的觀測與歷史 API。本篇則補上效能與儲存層。完整系列的中樞是〈Apple 生態系系列〉。
參考資料
-
Apple, WWDC 2026 session 8017, SwiftData Group Lab. Paraphrased from a locally transcribed recording; Apple publishes no official captions for the labs, so the wording here is a paraphrase, not a quotation, and exact phrasing is unverified. Source for the reader/writer-lock misconception framing, the
fetchIdentifiers-plus-history refresh-gating suggestion, the@Modelnon-Sendable transfer guidance, the view-invalidation-masquerading-as-I/O point, the “caches all the way down” benchmarking caution, and the connection-pool/concurrency-ceiling position (which is the lab’s engineering reasoning, not documented behavior; no specific concurrency number is asserted here because Apple does not document one). ↩↩↩↩↩↩↩ -
SQLite, Write-Ahead Logging. Source for the WAL concurrency model: “WAL provides more concurrency as readers do not block writers and a writer does not block readers,” with a single writer at a time. ↩↩↩
-
Apple, Technical Q&A QA1809: Setting the SQLite journaling mode for a Core Data store. Source for write-ahead logging being the default journaling mode for Core Data SQLite stores since iOS 7 and OS X Mavericks; SwiftData is built on the Core Data SQLite store. ↩↩↩
-
Apple,
ModelContext.fetchCount(_:). Signaturefunc fetchCount<T>(_ descriptor: FetchDescriptor<T>) throws -> Int where T : PersistentModel; returns the number of models matching the descriptor without instantiating them. ↩↩↩ -
Apple,
ModelContext.fetchIdentifiers(_:)andfetchIdentifiers(_:batchSize:). Returns[PersistentIdentifier]for a fetch descriptor without materializing the models, with a batched overload. ↩↩↩ -
Apple,
PersistentIdentifier. The aggregate identity of a SwiftData model; it isSendable,Hashable, andCodable, making it the type to move across actor boundaries. Apple notes a decodedPersistentIdentifierand one created by the default store are not always considered equivalent, so treat it as a stable cross-context handle. ↩↩↩↩ -
Apple, Adopting SwiftData for a Core Data app. Source for the app-group behavior: when an app evolves to use an app group container, SwiftData copies the existing store into the app group container under the default configuration; with a custom store URL you manage the location yourself. ↩
-
Apple, WWDC 2023 session 10189, Migrate to SwiftData, and
NSExpression. Source for coexistence (“two completely separate persistent stacks, one Core Data stack and one SwiftData stack, talking to the same persistent store”), the requirement that both use the same store URL and that the Core Data stack enableNSPersistentHistoryTrackingKeyor the store opens read-only, and for Core Data’sNSExpression-based SQL aggregates that SwiftData does not provide an equivalent to. ↩↩↩↩↩↩ -
Apple, WWDC 2026 session 274, What’s new in SwiftData. Source for
ResultsObserver, the Swift Observation-based observation type that supports the same primitives as@Queryincluding key-path sectioning viasectionBy:, shipping in the 2027 platform releases. ↩ -
Apple, WWDC 2025 session 306, Optimize SwiftUI performance with Instruments, and the Instruments File Activity and Core Data templates. Source for the SwiftUI Instruments template (bundling the SwiftUI instrument and the Hangs and Hitches instruments), the File Activity template’s Reads and Writes instrument (device only), and the Data Persistence instrument reporting faults, fetches, and saves. ↩↩
