← すべての記事

SwiftDataのパフォーマンスはストレージの問題である

WWDC 2026のSwiftDataグループラボには、フレームワークの下層を担う本人たちが顔をそろえていました。Appleの各プラットフォームでSQLiteをメンテナンスしているエンジニアや、Core DataとSwiftDataのマネージャーもその一人です。彼らの回答に一貫していたのは、多くの開発者がパフォーマンスへ向かうときの手の出し方に対する有用な訂正でした。すなわち、SwiftDataがアプリに組み込まれた時点で、コストのかかるものはI/OであってSwiftのコードではなく、勝ち筋は並行処理を増やすことではなく、読み込みを減らしストレージエンジンを理解することにある、ということです。以下の内容のほとんどはAppleのドキュメントとSQLiteのドキュメントに基づいています。ある主張が文書化された事実ではなくラボのエンジニアリング上の推論である場合は、その旨を明記しています。

Watch: SwiftData Group Lab (WWDC26)

WWDC 2026のSwiftDataグループラボ。

要点

  • SwiftDataのSQLiteストアはデフォルトでwrite-ahead logging(WAL)を使用します。これは、複数のリーダーが単一のライターと並行して動作することを意味します。リーダー/ライターロックではなく、この区別を開発者は決まって取り違えるとラボは述べています。23
  • オブジェクトを実体化せずに読む——fetchCount(_:)は一致した件数を返し、fetchIdentifiers(_:)[PersistentIdentifier]を返します。どちらもモデルをハイドレートしません。これらを履歴の監視と組み合わせれば、そもそもリフレッシュが必要かどうかを判断できます。45
  • @ModelオブジェクトはSendableではなく、無理にSendableにすべきでもありません。actor境界を越えるには、PersistentIdentifier(こちらはSendable)と抽出した値を渡し、宛先のコンテキストで再フェッチします。6
  • SwiftDataには、Core DataのSQLへプッシュダウンする集計クエリ(sum、average、min、max)に相当するものがありません。その逃げ道は共存です——同じストアファイルに対してCore Dataスタックを動かし、集計を計算させるのです。8
  • ラボのパフォーマンスに関するメッセージ——データベースが遅いと決めつける前に、なぜSwiftUIが再フェッチしたのかをプロファイルで突き止めましょう。ビューの過剰な無効化は、実際にはそうでなくてもI/Oの問題のように見えるからです。110

WAL:並行するリーダー、単一のライター、ロックではない

ラボから得られた最も有用な訂正は、ストレージ層での並行処理に関するものです。SwiftDataはCore DataのSQLiteストアの上に構築されており、そのストアはiOS 7以来write-ahead loggingをデフォルトにしてきました。3 WALのもとでは、SQLiteのドキュメントが述べるように、「WALはより高い並行性を提供します。リーダーはライターをブロックせず、ライターはリーダーをブロックしません。読み込みと書き込みは並行して進められます」。2 同時に書き込めるのは依然として正確に1つのライターだけですが、データベースへのすべてのアクセスを直列化するミューテックスというメンタルモデルは誤りです——読み込みが書き込みの後ろで待つ必要はありません。

ラボの説明は、録音から言い換えると、人々は単一ライターのルールをリーダー/ライターロックとして扱い、存在しない制約を中心に設計を組み立ててしまう、というものでした。1 正確なモデルはWALのものです——グローバルな排他ではなく、多数の並行する読み込みと直列化された書き込みのために設計するのです。

読み込みを減らす:ハイドレートせずに数え、特定する

I/Oがコストであるなら、最もレバレッジの高い一手は、必要のないオブジェクトの読み込みをやめることです。SwiftDataはこのための2つのプリミティブを提供しており、どちらもAPIで確認できます。

ModelContextfetchCount(_:)FetchDescriptorを受け取り、一致するモデルの数をIntとして返します。そのいずれもインスタンス化しません。4 バッジやセクションヘッダーのために件数が必要なときは、フェッチして.countを呼ぶよりも確実に安上がりです。

fetchIdentifiers(_:)はディスクリプタに対して[PersistentIdentifier]を返し、ここでもモデルを実体化しません。さらにfetchIdentifiers(_:batchSize:)のオーバーロードが作業をバッチ処理します。5 ラボが提案した使い方は、言い換えると、これを履歴の監視と組み合わせるものでした——変更が入ってきたら、影響を受ける識別子をフェッチし、ビューが実際に表示しているものと比較してから、何かを再読み込みするかどうかを判断するのです。1 履歴と監視のAPIそのものはSwiftDataのiOS 27における監視と履歴で扱っています。fetchIdentifiersは、それらを効率的にする軽量な読み込みです。SwiftUIの外で手を伸ばすべき監視型はResultsObserverで、これは2027年のリリースに向けて導入されたSwift Observationベースのオブザーバーです。sectionBy:によるキーパスのセクション分割を含め、@Queryと同じプリミティブをサポートします。9

Sendable境界は実在し、モデルグラフはそれを越えない

SwiftDataのモデルは参照型であり、コンテキスト内部のグラフに結線されており、Sendableではありません。ラボは、それらを無理にSendableにすることは正気ではできない、とはっきり述べました。グラフはスレッドセーフではなく、別のactor上で部分的にハイドレートすると問題を招くからです。1 サポートされているパターンは、SendableかつHashableかつCodableであるPersistentIdentifierを、境界を越えて運ぶ識別子として使います。6 必要な値を構造体に抽出し、PersistentIdentifierを添え、それを別のactorに渡し、ライブなオブジェクトが必要なら宛先のコンテキストでモデルを再フェッチします。

押さえておく価値のある精密な点が一つあります。Appleは、デコードされたPersistentIdentifierとデフォルトストアが作成したものが常に等価とみなされるわけではない、と注記しています。したがって、デコードされたコピーがライブなものと等しいと仮定するのではなく、その識別子を安定したコンテキスト横断ハンドルとして扱いましょう。6

同じ「グラフではなくアイデンティティ」という規律は、プロセスをまたいでも現れます。ストアをapp groupに移してwidgetやextensionと共有する場合、デフォルト構成では既存のストアをapp groupコンテナへコピーしてくれます。カスタムのストアURLを使う場合は、自分でロケーションを管理します。7 いずれにせよ、プロセスはストアとその識別子を通じて協調するのであって、ライブなオブジェクトを互いに渡し合うのではありません。

集計のギャップと、Core Dataという逃げ道

ラボが名指しした実在の制約があります。SwiftDataには、Core DataのNSExpressionベースの集計クエリ——sumaverageminmaxをSQLiteへプッシュダウンし、行を読み込まずにデータベースが計算するもの——に相当するものがありません。8 SwiftDataでは行をフェッチしてメモリ内でリデュースすることになり、大きなテーブルでは本末転倒です。minmaxであれば、ソートディスクリプタとフェッチリミット1でフェッチできます。本物の集計が必要なら、ラボは共存を指し示しました。

共存とは、AppleがWWDC 2023で説明したように、「2つの完全に分離した永続化スタック、すなわち1つのCore Dataスタックと1つのSwiftDataスタックが、同じ永続化ストアと対話する」ことです。8 どちらのスタックも同じストアURLを指します。そして、SwiftDataは永続化履歴トラッキングを自動的に有効にするため、Core Data側もNSPersistentHistoryTrackingKeyを有効にしなければ、ストアは読み取り専用で開かれてしまいます。8 これを整えれば、SwiftDataが所有するまさにそのファイルに対して、Core Data経由でSQLへプッシュダウンした集計を実行できます。ほとんどのアプリに必要な以上の仕掛けですが、データベース側の集計が本当に必要なときの、文書化された道筋です。

データベースだけでなく、無効化をプロファイルする

ラボの最も実践的なパフォーマンスの助言は、言い換えると、SwiftDataアプリの見かけ上のI/Oコストは、しばしば形を変えたSwiftUIの無効化の問題である、というものでした——頻繁に無効化しすぎるビューは再フェッチし、プロファイラはその再フェッチをデータベースの時間として示しますが、本当の原因はそのビューがそもそもリフレッシュすべきでなかったことにあるのです。1 修正策は、あらゆるSwiftUIのパフォーマンス問題に効くのと同じビュー分離の規律です。これはSwiftUIのパフォーマンスと相互運用で扱っています——大きなビューを依存範囲の狭い小さなビューに分割し、すでにフェッチ済みのモデルを下に渡してクエリが再実行されないようにするのです。

ツール群もこの読み取りを支えます。Instrumentsには、SwiftUI instrumentをHangs and Hitches instrumentと束ねたSwiftUIテンプレート、Reads and Writes instrumentが実際のディスクトラフィックを示すFile Activityテンプレート(シミュレーターではなく実機のみ)、そしてフォールト・フェッチ・セーブを報告するData Persistence instrumentを備えたCore Dataテンプレートが付属します。10 SwiftUIと永続化のビューを一緒に走らせれば、再フェッチが本物の読み込みだったのか、それとも過剰な無効化が引き起こした冗長なものだったのかが分かります。

ベンチマークについてラボが挙げた注意点を、言い換えると次のとおりです——下層に至るまでキャッシュが何重にもあります。SQLiteのページキャッシュ、OSのファイルキャッシュ、そしてストレージコントローラ。ですから「速い」実行は、実際の改善ではなくキャッシュヒットかもしれません。現実的に大きなデータセットに対して測定し、File Activity instrumentで実際にI/Oが起きたことを確認しましょう。1

並行処理を追加することについて

ラボの最も強い意見であり、文書化された事実ではなくエンジニアリング上の推論として扱うべき部分は、パフォーマンスの修正策として並行処理に手を伸ばすことへの警告でした。エンジニアたちは、SwiftDataのコネクションプーリングは意図的に上限が設けられていると説明し、少数の並行操作を超えるとストレージハードウェアの上限に当たるため、コンテキストを増やしてもメモリとI/Oが増える代償に対してリターンは逓減する、と論じました。1 Appleは特定の並行処理の上限を文書化していないので、本稿を含め、誰からも確定的な数字を受け取らないでください。守れる教訓は方向性のものです——フラッシュストレージのデバイスでは、並行ライターを積み上げることは確実に速くする手段ではなく、WALモデルはすでに並行する読み込みを無料で与えてくれている、ということです。

ここから持ち帰るもの

ラボはSwiftDataのパフォーマンスをストレージエンジンを中心に再構成します。確認されたレバーは具体的です——ロックを恐れるのではなくWALの並行する読み込みに頼り、fetchCountfetchIdentifiersを使ってオブジェクトのハイドレートを避け、モデルグラフではなくPersistentIdentifierをactor間で移動させ、本物の集計が必要なときはCore Dataの共存に手を伸ばす。そしてプロファイリングの規律とは、データベースを最適化する前にI/Oコストが本物であることを確認することです。なぜなら、その犯人はしばしば、リフレッシュすべきでなかったのにリフレッシュしたビューだからです。

FAQ

SwiftDataは書き込み中にデータベースをロックしますか?

リーダー/ライターロックという意味ではしません。ストアはSQLiteのwrite-ahead loggingを使用し、これは複数のリーダーが単一のライターと並行して動作することを許します。読み込みはライターをブロックせず、ライターは読み込みをブロックしません。23 同時に書き込めるのは1つのライターですが、読み込みはその傍らで進みます。

レコードを読み込まずに数えたり確認したりするには?

一致件数にはModelContext.fetchCount(_:)を、[PersistentIdentifier]の値にはModelContext.fetchIdentifiers(_:)を使います。いずれもモデルオブジェクトを実体化しません。45 fetchIdentifiersを履歴の監視と組み合わせれば、再読み込みの前に、変更が実際にビューの表示内容へ影響するかどうかを判断できます。

SwiftDataのオブジェクトを別のactorに渡すには?

オブジェクトを渡してはいけません。@Model型はSendableではありません。PersistentIdentifier(こちらはSendable)と抽出した値を渡し、宛先のコンテキストで再フェッチします。6 ライブなモデルグラフを境界の向こうへ手渡すことは避けましょう。

SwiftDataはデータベース内でsum/average/min/maxを実行できますか?

いいえ。SwiftDataには、Core DataのNSExpressionによるSQLプッシュダウン集計に相当するものがありません。8 min/maxであれば、ソートとフェッチリミット1でフェッチします。本物の集計には、同じストアファイルに対してCore Dataスタックを動かします(共存)。これには、ストアURLを一致させ、Core Data側で永続化履歴トラッキングを有効にすることが必要です。8


このブログのSwiftDataのレーンでは、スキーマとマイグレーションの規律をスキーマの規律マイグレーションガイドで、iOS 27の監視と履歴のAPIを監視と履歴で扱っています。本稿はパフォーマンスとストレージの層を加えるものです。シリーズ全体のハブはApple Ecosystem Seriesです。

References


  1. 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 @Model non-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). 

  2. 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. 

  3. 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. 

  4. Apple, ModelContext.fetchCount(_:). Signature func fetchCount<T>(_ descriptor: FetchDescriptor<T>) throws -> Int where T : PersistentModel; returns the number of models matching the descriptor without instantiating them. 

  5. Apple, ModelContext.fetchIdentifiers(_:) and fetchIdentifiers(_:batchSize:). Returns [PersistentIdentifier] for a fetch descriptor without materializing the models, with a batched overload. 

  6. Apple, PersistentIdentifier. The aggregate identity of a SwiftData model; it is Sendable, Hashable, and Codable, making it the type to move across actor boundaries. Apple notes a decoded PersistentIdentifier and one created by the default store are not always considered equivalent, so treat it as a stable cross-context handle. 

  7. 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. 

  8. 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 enable NSPersistentHistoryTrackingKey or the store opens read-only, and for Core Data’s NSExpression-based SQL aggregates that SwiftData does not provide an equivalent to. 

  9. 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 @Query including key-path sectioning via sectionBy:, shipping in the 2027 platform releases. 

  10. 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. 

関連記事

UIKitのシーン義務化:iOS 27で起動しなくなるもの

iOS 27 SDKでビルドしたアプリは、UIKitのシーンベースのライフサイクルを採用しなければ起動しません。タイムライン、移行手順、そしてエージェントスキルを解説します。

2 分で読める

ImageCreator が非推奨に:iOS 27 で何が壊れるのか

Apple は iOS 27 で Image Playground の ImageCreator クラスを廃止します。ベータ版では TestFlight 実行時エラーが発生し、正式リリースではコンパイルが通らなくなります。

3 分で読める

76から100へ:Lighthouseパーフェクトスコア達成への道

個人ポートフォリオサイトのモバイルLighthouseパフォーマンススコアを76(CLS 0.493)から全カテゴリ100/100/100/100のパーフェクトスコアに改善した方法を解説します。

3 分で読める