HKSampleQuery: a one-time snapshot of matching samples
Last verified October 3, 2026 · 5 min read
Covered here:HealthKit
HKSampleQuery is usually the first query class anyone writes, and the one most often kept for jobs it was never designed for. This page covers how it behaves, according to Apple's own documentation, and the places it quietly gives you the wrong answer. Setup, entitlements and authorization are covered in the HealthKit integration guide.
What Apple says it is#
Apple's class page summarises it in one sentence: "A general query that returns a snapshot of all the matching samples currently saved in the HealthKit store." The overview adds three facts worth keeping in mind:
- It searches "any concrete subclasses of the HKSample class, including HKCategorySample, HKCorrelation, HKQuantitySample, and HKWorkout objects."
- "Sample queries are immutable: The query's properties are set when the query is first created, and they can't change."
- "As with many HealthKit classes, don't subclass the HKSampleQuery class."
Availability. Apple's class page lists iOS, iPadOS, Mac Catalyst, macOS, visionOS and watchOS. Its documentation data gives no introduction version for the class itself. The multi-type initializer init(queryDescriptors:limit:resultsHandler:) is listed from iOS 15.0, iPadOS 15.0, Mac Catalyst 15.0, macOS 13.0, visionOS 1.0 and watchOS 8.0. The type identifiers that iOS 15 added are listed on the HealthKit types introduced in iOS 15.
The initializer, parameter by parameter#
The classic form is init(sampleType:predicate:limit:sortDescriptors:resultsHandler:). Apple documents each argument:
| Parameter | What Apple says |
|---|---|
sampleType | "This object can be an instance of the HKCategoryType, HKCorrelationType, HKQuantityType, or HKWorkoutType class." |
predicate | Limits the results. "Pass nil to receive all the samples of the specified type." |
limit | "The maximum number of samples returned by the query. If you want to return all matching samples, use HKObjectQueryNoLimit." |
sortDescriptors | The order of the results. "Pass nil if you don't need the results in a specific order." |
resultsHandler | Called once with the query, an array of samples ("or nil if an error occurs") and an error. |
The predicate is where most of the real decisions are made, including whether a sample that started yesterday counts as today's. That has its own page: HealthKit query predicates.
A minimal read#
This sketch follows Apple's "Executing Sample Queries" article: one type, a date window, results sorted by start date.
import HealthKit
func readEnergyConsumed(store: HKHealthStore, start: Date, end: Date) {
guard let energyType = HKSampleType.quantityType(forIdentifier: .dietaryEnergyConsumed) else { return }
let window = HKQuery.predicateForSamples(withStart: start, end: end, options: [])
let byStartDate = NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: true)
let query = HKSampleQuery(sampleType: energyType,
predicate: window,
limit: HKObjectQueryNoLimit,
sortDescriptors: [byStartDate]) { _, results, error in
guard let samples = results as? [HKQuantitySample] else {
// results is nil only when error is set
return
}
DispatchQueue.main.async {
// update the UI with samples
}
}
store.execute(query)
}
The identifier used here is dietaryEnergyConsumed; converting its values means choosing an HKUnit, which the HealthKit units reference lists per type.
Which thread the handler runs on#
Apple is explicit: "Queries run on an anonymous background queue. As soon as the query is complete, the results handler is executed on the background queue. You typically dispatch these results to the main queue to update the user interface." The article version adds that the handler runs on the same background queue "but not necessarily the same thread". Touching UI state directly inside the handler is the classic crash; hop to the main queue first.
Reading several types in one query#
Since iOS 15.0 you can pass an array of HKQueryDescriptor objects instead of one type, using init(queryDescriptors:limit:resultsHandler:) or the variant that also takes sort descriptors. Apple: "Each descriptor can specify a different data type." Its own example reads step count and push count samples together. The results still come back as [HKSample], so you sort them out by type yourself.
When HKSampleQuery is the wrong class#
| You need | Use instead |
|---|---|
| A sum, average, minimum or maximum over a window | HKStatisticsQuery |
| One value per day or hour for a chart | HKStatisticsCollectionQuery |
| Only what changed since the last read, including deletions | HKAnchoredObjectQuery |
| A signal that something changed | HKObserverQuery |
| async/await instead of a callback | HKSampleQueryDescriptor |
| The GPS points of a workout | HKWorkoutRouteQuery |
| Activity ring values | HKActivitySummaryQuery |
Apple makes one comparison explicitly, on the anchored query's page: "Often, it's more efficient to set up and run a single anchored object query than to run separate sample and observer queries."
Pitfalls#
- Sort identifiers only work inside a query. Apple's note on
sortDescriptors: "Use the sort descriptors you create with these identifiers only in queries. You cannot use them to perform an in-memory sort of an array of samples." To re-sort an array you already have, sort on the sample's own properties. - A snapshot never shows a deletion. Running
HKSampleQueryagain and comparing the results is a poor way to sync, because a deleted sample is simply missing. Apple's page forHKDeletedObjectsays to "use HKAnchoredObjectQuery queries to generate a list of recently deleted objects." The reasoning is laid out in incremental sync. - Adding up samples is not the same as asking for a total. Apple documents that statistics queries "automatically merge the data from all of your data sources before performing the calculations". A loop over a sample array does no such merge. Read totals with HKStatisticsQuery, and see deduplicating health data for what overlapping sources do to a naive sum.
- The limit is not optional. In this API
limitis a plainInt, so you choose a cap or passHKObjectQueryNoLimit. With a date sort descriptor and a small limit you get the first N samples in that order, which is how "the newest ten" is written; without a sort descriptor, Apple does not document which N you get. - An empty array is not an error. A query that comes back with no samples and no error can mean there is no data, the predicate matched nothing, or reading was not permitted. HealthKit returns no data covers how to tell those apart, and authorization denied covers the permission side.
Frequently asked questions
- How do I make HKSampleQuery return every matching sample instead of a capped number?
- Pass HKObjectQueryNoLimit as the limit. Apple documents it as a value indicating that the query returns all the matching samples in the HealthKit store, and the limit parameter's own description says to use it when you want every match. Any positive integer instead caps the result count, which combined with a sort descriptor on the start or end date is how you ask for only the newest or oldest few samples.
- Can I sort an array of HealthKit samples with HKSampleSortIdentifierStartDate?
- No. Apple's note on the sortDescriptors parameter says sort descriptors built from HealthKit's sort identifiers, such as HKSampleSortIdentifierStartDate or HKWorkoutSortIdentifierDuration, can be used only in queries and cannot be used to sort an array of samples in memory. Pass them to the query so the store returns the samples in order, or sort an existing array on the samples' own date properties.
- Which thread does the HKSampleQuery results handler run on?
- A background one. Apple documents that queries run on an anonymous background queue and that the results handler executes on that background queue once the query completes, though not necessarily on the same thread. Apple's guidance is to dispatch the results to the main queue before updating the user interface, so any UI work belongs inside a main-queue dispatch.
- Can a single HKSampleQuery read more than one data type?
- Yes, from iOS 15.0 and watchOS 8.0. The initializer init(queryDescriptors:limit:resultsHandler:) takes an array of HKQueryDescriptor objects, and Apple says each descriptor can specify a different data type. Apple's example reads step count and push count samples in one query. The results arrive as one array of HKSample objects, so your code separates them by type afterwards.
Keep reading
Elsewhere on the site
Pages that share this one’s concepts and sources, from other sections.
- TroubleshootingHealthKit errorDatabaseInaccessible: Reads Fail While the Device Is Locked
- TestingTesting Background Sync: Three Tests, and Only One Is Real
- Health Connect APIREAD_HEALTH_DATA_IN_BACKGROUND: reading Health Connect data from a background worker
- ComplianceApp Store Guideline 2.5.4: Background Modes for Workout Apps
Next steps
Was this page useful?
Independent comparison, last reviewed October 3, 2026. Pricing, rate limits, and feature availability change often — confirm current details in each provider’s official documentation before you commit. Product and company names are trademarks of their respective owners; AIFitnessAPI is not affiliated with, endorsed by, or sponsored by any product listed here.
← All healthkit queries · by AIFitnessAPI