Wydajność SwiftData to problem warstwy magazynu danych
Laboratorium grupowe SwiftData na WWDC 2026 obsadzili ludzie, którzy odpowiadają za warstwy leżące pod tym frameworkiem, w tym inżynier utrzymujący SQLite na wszystkich platformach Apple oraz menedżer Core Data i SwiftData. Wspólnym wątkiem ich odpowiedzi była użyteczna korekta sposobu, w jaki większość programistów sięga po wydajność: gdy SwiftData jest już w aplikacji, kosztowne jest I/O, a nie kod w Swift, a zyski biorą się z czytania mniejszej ilości danych i ze zrozumienia silnika magazynu danych, a nie z dodawania współbieżności. Większość tego, co następuje, jest ugruntowana w dokumentacji Apple oraz SQLite; tam, gdzie dane twierdzenie jest raczej inżynierskim rozumowaniem laboratorium niż udokumentowanym faktem, jest to oznaczone.
Laboratorium grupowe SwiftData na WWDC 2026.
TL;DR
- Magazyn SQLite w SwiftData domyślnie używa write-ahead logging (WAL), co oznacza, że wielu czytelników działa współbieżnie z jednym piszącym. To nie jest blokada typu czytelnik/piszący — rozróżnienie, które według laboratorium programiści nagminnie mylą.23
- Czytanie bez materializowania obiektów:
fetchCount(_:)zwraca liczbę dopasowań, afetchIdentifiers(_:)zwraca[PersistentIdentifier], oba bez nawadniania modeli. Warto łączyć je z obserwacją historii, aby zdecydować, czy odświeżenie jest w ogóle potrzebne.45 - Obiekty
@Modelnie sąSendablei nie należy ich do tego zmuszać. Aby przekroczyć granicę aktora, należy przekazaćPersistentIdentifier(który jestSendable) oraz dowolne wyłuskane wartości, a następnie ponownie pobrać model w kontekście docelowym.6 - SwiftData nie ma odpowiednika zapytań agregujących Core Data wypychanych do SQL (sum, average, min, max). Furtką ratunkową jest współistnienie: uruchomienie stosu Core Data na tym samym pliku magazynu i pozwolenie mu na policzenie agregatu.8
- Przesłanie laboratorium dotyczące wydajności: warto profilować, aby ustalić, dlaczego SwiftUI ponownie pobiera dane, zanim założymy, że baza danych jest wolna, ponieważ nadmierne unieważnianie widoku wygląda jak problem I/O, choć nim nie jest.110
WAL: współbieżni czytelnicy, jeden piszący, nie blokada
Najbardziej użyteczna pojedyncza korekta z laboratorium dotyczy współbieżności w warstwie magazynu danych. SwiftData jest zbudowany na magazynie SQLite z Core Data, a ten magazyn domyślnie używa write-ahead logging od czasów iOS 7.3 Przy WAL — jak ujmuje to dokumentacja SQLite — “WAL provides more concurrency as readers do not block writers and a writer does not block readers. Reading and writing can proceed concurrently.”2 Wciąż jest dokładnie jeden piszący naraz, ale błędny jest model mentalny muteksu serializującego cały dostęp do bazy danych: odczyty nie muszą czekać za zapisem.
Ujęcie laboratorium, sparafrazowane z nagrania, było takie, że ludzie traktują regułę jednego piszącego jak blokadę czytelnik/piszący i projektują architekturę wokół ograniczenia, które nie istnieje.1 Trafny model to ten z WAL: należy projektować pod kątem wielu współbieżnych odczytów i jednego serializowanego zapisu, a nie pod kątem globalnego wykluczenia.
Czytaj mniej: licz i identyfikuj bez nawadniania
Skoro koszt to I/O, ruchem o największej dźwigni jest zaprzestanie ładowania obiektów, których nie potrzebujemy. SwiftData daje na to dwa prymitywy, oba zweryfikowane w API:
fetchCount(_:) na ModelContext przyjmuje FetchDescriptor i zwraca liczbę pasujących modeli jako Int bez instancjonowania żadnego z nich.4 Gdy potrzebna jest liczba do plakietki lub nagłówka sekcji, jest to ściśle tańsze niż pobranie i wywołanie .count.
fetchIdentifiers(_:) zwraca [PersistentIdentifier] dla deskryptora, ponownie bez materializowania modeli, a przeciążenie fetchIdentifiers(_:batchSize:) przetwarza pracę partiami.5 Sugerowane przez laboratorium zastosowanie, w parafrazie, łączy to z obserwacją historii: gdy nadchodzi zmiana, należy pobrać dotknięte identyfikatory i porównać je z tym, co widok faktycznie wyświetla, zanim zdecydujemy o ponownym załadowaniu czegokolwiek.1 Same API historii i obserwacji są omówione w Obserwacja i historia w SwiftData na iOS 27; fetchIdentifiers to lekki odczyt, który czyni je wydajnymi. Typem obserwacji, po który należy sięgać poza SwiftUI, jest ResultsObserver — oparty na Swift Observation obserwator wprowadzony dla wydań z 2027 roku, który obsługuje te same prymitywy co @Query, w tym sekcjonowanie po ścieżce klucza przez sectionBy:.9
Granica Sendable jest realna, a graf modelu jej nie przekracza
Modele SwiftData to typy referencyjne wplecione w graf wewnątrz swojego kontekstu i nie są Sendable. Laboratorium wypowiedziało się dosadnie, że nie da się ich sensownie do tego zmusić, ponieważ graf nie jest bezpieczny wątkowo, a częściowe nawodnienie go na innym aktorze prowadzi do kłopotów.1 Wspierany wzorzec używa PersistentIdentifier, który jest Sendable, Hashable i Codable, jako tożsamości przenoszonej przez granice.6 Należy wyłuskać potrzebne wartości do struktury, dołączyć PersistentIdentifier, przekazać to drugiemu aktorowi i ponownie pobrać model w kontekście docelowym, jeśli potrzebny jest żywy obiekt.
Jedna precyzja warta zapamiętania: Apple zaznacza, że zdekodowany PersistentIdentifier i ten utworzony przez domyślny magazyn nie zawsze są uznawane za równoważne, więc identyfikator należy traktować jako stabilny uchwyt między kontekstami, a nie zakładać, że zdekodowana kopia równa się żywemu obiektowi.6
Ta sama dyscyplina tożsamość-nie-graf pojawia się między procesami. Gdy przenosimy magazyn do grupy aplikacji, aby współdzielić go z widgetem lub rozszerzeniem, domyślna konfiguracja kopiuje istniejący magazyn do kontenera grupy aplikacji za nas; przy własnym URL magazynu lokalizacją zarządzamy sami.7 Tak czy inaczej, procesy koordynują się przez magazyn i jego identyfikatory, a nie przez przekazywanie między sobą żywych obiektów.
Luka w agregacjach i furtka ratunkowa Core Data
Realne ograniczenie, które wskazało laboratorium: SwiftData nie ma odpowiednika zapytań agregujących Core Data opartych na NSExpression — tych, które wypychają sum, average, min i max w dół do SQLite, by baza danych je liczyła bez ładowania wierszy.8 W SwiftData trzeba by pobrać wiersze i zredukować w pamięci, co niweczy cel na dużej tabeli. Dla min lub max można pobrać z deskryptorem sortowania i limitem pobrania równym jeden; dla prawdziwych agregatów laboratorium wskazało na współistnienie.
Współistnienie, jak ujęło to Apple na WWDC 2023, to “two completely separate persistent stacks, one Core Data stack and one SwiftData stack, talking to the same persistent store.”8 Oba stosy wskazują na ten sam URL magazynu, a ponieważ SwiftData automatycznie włącza śledzenie historii trwałej, strona Core Data także musi włączyć NSPersistentHistoryTrackingKey, w przeciwnym razie magazyn otwiera się tylko do odczytu.8 Mając to na miejscu, można przeprowadzić wypychaną do SQL agregację przez Core Data na tym samym pliku, którego właścicielem jest SwiftData. To więcej maszynerii, niż potrzebuje większość aplikacji, ale jest to udokumentowana droga, gdy naprawdę potrzebujemy agregacji po stronie bazy danych.
Profiluj unieważnianie, nie tylko bazę danych
Najbardziej praktyczna wskazówka wydajnościowa z laboratorium, w parafrazie, mówiła, że pozorny koszt I/O w aplikacji SwiftData to często ukryty problem unieważniania SwiftUI: widok, który unieważnia się zbyt często, ponownie pobiera dane, a profiler pokazuje to ponowne pobranie jako czas bazy danych, podczas gdy prawdziwą winą jest to, że widok w ogóle nie powinien się był odświeżyć.1 Naprawą jest ta sama dyscyplina izolacji widoku, która pomaga przy każdym problemie wydajnościowym SwiftUI, omówiona w Wydajność i współdziałanie SwiftUI: należy rozbić duże widoki na mniejsze o węższych zależnościach i przekazywać już pobrane modele w dół, aby zapytanie nie uruchamiało się ponownie.
Narzędzia wspierają taki odczyt. Instruments dostarcza szablon SwiftUI, który łączy instrument SwiftUI obok instrumentów Hangs i Hitches, szablon File Activity, którego instrument Reads i Writes pokazuje rzeczywisty ruch dyskowy (tylko na urządzeniu, nie w symulatorze), oraz szablon Core Data z instrumentem Data Persistence raportującym usterki, pobrania i zapisy.10 Uruchomienie widoków SwiftUI i trwałości razem mówi, czy ponowne pobranie było prawdziwym odczytem, czy nadmiarowym, wywołanym przez nadmierne unieważnianie.
Przestroga, którą laboratorium podniosło na temat benchmarkingu, w parafrazie: pamięci podręczne są na każdym poziomie w dół — pamięć podręczna stron SQLite, pamięć podręczna plików systemu operacyjnego i kontroler magazynu — więc “szybki” przebieg może być trafieniem w pamięć podręczną, a nie realną poprawą. Należy mierzyć na realistycznie dużym zbiorze danych i używać instrumentu File Activity, aby potwierdzić, że faktyczne I/O miało miejsce.1
O dodawaniu współbieżności
Najmocniejsza opinia laboratorium, i część, którą należy traktować jako inżynierskie rozumowanie, a nie udokumentowany fakt, była przestrogą przed sięganiem po współbieżność jako lekarstwo na wydajność. Inżynierowie opisali pulę połączeń SwiftData jako celowo ograniczoną i argumentowali, że powyżej niewielkiej liczby współbieżnych operacji trafiamy na sufit sprzętu magazynu, więc więcej kontekstów daje malejące zyski kosztem większej pamięci i większego I/O.1 Apple nie dokumentuje konkretnego limitu współbieżności, więc nie należy przyjmować twardej liczby od nikogo, łącznie z tym wpisem. Obronnym wnioskiem jest ten kierunkowy: na urządzeniu z magazynem flash dokładanie współbieżnych piszących nie jest niezawodnym sposobem na przyspieszenie, a model WAL już daje współbieżne odczyty za darmo.
Co z tego wynieść
Laboratorium przeformułowuje wydajność SwiftData wokół silnika magazynu danych. Zweryfikowane dźwignie są konkretne: oprzeć się na współbieżnych odczytach WAL zamiast obawiać się blokady, użyć fetchCount i fetchIdentifiers, aby uniknąć nawadniania obiektów, przenosić PersistentIdentifier przez aktorów zamiast grafu modelu, oraz sięgać po współistnienie z Core Data, gdy potrzebny jest prawdziwy agregat. Dyscyplina profilowania polega na potwierdzeniu, że koszt I/O jest realny, zanim zoptymalizujemy bazę danych, bo winowajcą jest często widok, który odświeżył się, choć nie powinien.
FAQ
Czy SwiftData blokuje bazę danych podczas zapisów?
Nie w sensie blokady czytelnik/piszący. Magazyn używa write-ahead logging z SQLite, co pozwala wielu czytelnikom działać współbieżnie z jednym piszącym; odczyty nie blokują piszącego, a piszący nie blokuje odczytów.23 Jest jeden piszący naraz, ale odczyty przebiegają obok niego.
Jak policzyć lub sprawdzić rekordy bez ich ładowania?
Należy użyć ModelContext.fetchCount(_:) dla liczby dopasowań oraz ModelContext.fetchIdentifiers(_:) dla wartości [PersistentIdentifier], z których żadna nie materializuje obiektów modelu.45 Warto połączyć fetchIdentifiers z obserwacją historii, aby zdecydować, czy zmiana faktycznie wpływa na to, co pokazuje widok, zanim go przeładujemy.
Jak przekazać obiekt SwiftData innemu aktorowi?
Nie przekazuje się obiektu. Typy @Model nie są Sendable. Należy przekazać PersistentIdentifier (który jest Sendable) oraz dowolne wyłuskane wartości, a następnie ponownie pobrać w kontekście docelowym.6 Należy unikać przekazywania żywego grafu modelu przez granicę.
Czy SwiftData potrafi liczyć sum/average/min/max w bazie danych?
Nie. SwiftData nie ma odpowiednika agregatów Core Data opartych na NSExpression i wypychanych do SQL.8 Dla min/max należy pobrać z sortowaniem i limitem pobrania równym jeden; dla prawdziwych agregatów uruchomić stos Core Data na tym samym pliku magazynu (współistnienie), co wymaga dopasowania URL magazynu i włączenia śledzenia historii trwałej po stronie Core Data.8
Ścieżka SwiftData na tym blogu obejmuje dyscyplinę schematu i migracji w dyscyplina schematu oraz przewodnik po migracjach, a API obserwacji i historii z iOS 27 w obserwacja i historia. Ten wpis dodaje warstwę wydajności i magazynu danych. Centrum całej serii to Seria Apple Ecosystem.
References
-
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. ↩↩
