Query a feature table for statistics, grouping and sorting by different fields.

Use case
You can use statistical queries, grouping, and sorting to process large amounts of data saved in feature tables. This is helpful for identifying trends and relationships within the data, which can be used to support further interpretations and decisions. For example, a health agency can use information on medical conditions occurring throughout a country to identify at-risk areas or demographics, and decide on further action and preventive measures.
How to use the sample
Select a combination of fields and statistic types to include in the query. Choose one or more fields by which to group the results. For example, selecting “State” will calculate the results by state. Choose one or more fields to order results by. Only those fields selected for grouping are valid choices for ordering results. Tap “Edit” to rearrange and delete the fields and statistic types. Tap the “Query Statistics” button to execute the query. Results will be displayed in a hierarchical view that is grouped and sorted according to the chosen fields. Tap “Reset” to clear the fields and statistic types.
How it works
- Create a
ServiceFeatureTableusing the URL of a feature service and load the table. - Create
StatisticDefinitionobjects and use them to createStatisticsQueryParameters. - To have the results grouped by fields, add the field names to the query parameters’
groupByFieldNamesarray. - To have the results ordered by fields, create
OrderByobjects, specifying the field name andSortOrder. Add theseOrderBys to the parameters’orderByFieldscollection. - To execute the query, call
FeatureTable.queryStatistics(using:). - From the
StatisticQueryResult, you can useStatisticsQueryResult.statisticRecords()to loop through and display theStatisticRecordobjects.
Relevant API
- Field
- OrderBy
- QueryParameters
- ServiceFeatureTable
- StatisticDefinition
- StatisticRecord
- StatisticsQueryParameters
- StatisticsQueryResult
- StatisticType
About the data
This sample uses a Diabetes, Obesity, and Inactivity by US County feature layer hosted on ArcGIS Online.
Tags
correlation, data, fields, filter, group, sort, statistics, table
Sample code
// Copyright 2025 Esri//// Licensed under the Apache License, Version 2.0 (the "License");// you may not use this file except in compliance with the License.// You may obtain a copy of the License at//// https://www.apache.org/licenses/LICENSE-2.0//// Unless required by applicable law or agreed to in writing, software// distributed under the License is distributed on an "AS IS" BASIS,// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.// See the License for the specific language governing permissions and// limitations under the License.
import ArcGISimport SwiftUI
struct QueryTableStatisticsGroupAndSortView: View { /// The view model containing the feature table to query. @State private var model = Model()
/// The definitions for the statistics to be queried for in the feature table. @State private var statisticDefinitions: [StatisticDefinition] = []
/// The field names by which statistical query results will be grouped. @State private var groupByFields: [String] = []
/// The fields by which statistical query results will be ordered. @State private var orderByFields: [OrderBy] = []
/// The records resulting from a statistical query. @State private var statisticRecords: [StatisticRecord] = []
/// A Boolean value indicating whether there is an ongoing query operation. @State private var isQuerying = false
/// A view that can be displayed in the sheet for this sample. private enum SheetView: Identifiable { case addStatisticDefinition, addGroupByFields, addOrderByField, statisticRecordsList
var id: Self { self } }
/// The view currently presented in the sheet. @State private var presentedSheetView: SheetView?
/// The error shown in the error alert. @State private var error: (any Error)?
/// A Boolean value indicating whether edits have been made to the from. private var formHasEdits: Bool { !statisticDefinitions.isEmpty || !groupByFields.isEmpty || !orderByFields.isEmpty }
var body: some View { VStack { Text(model.tableName) .font(.headline.bold()) .padding(.top)
Form { Section("Statistic Definitions") { ForEach(statisticDefinitions, id: \.objectID) { definition in LabeledContent(definition.fieldName, value: definition.statisticType.label) } .onDelete { statisticDefinitions.remove(atOffsets: $0) }
Button("Add Statistic Definition") { presentedSheetView = .addStatisticDefinition } }
Section("Group By Fields") { ForEach(groupByFields, id: \.self) { field in Text(field) } .onDelete { offsets in // Removes the corresponding order by fields. let groupByFieldsToRemove = Set(offsets.map { groupByFields[$0] }) orderByFields.removeAll( where: { groupByFieldsToRemove.contains($0.fieldName) } )
groupByFields.remove(atOffsets: offsets) } .onMove { groupByFields.move(fromOffsets: $0, toOffset: $1) }
Button("Add Group By Fields") { presentedSheetView = .addGroupByFields } }
Section { ForEach(orderByFields, id: \.fieldName) { field in let sortByLabel = field.sortOrder == .ascending ? "Ascending" : "Descending" LabeledContent(field.fieldName, value: sortByLabel) } .onDelete { orderByFields.remove(atOffsets: $0) } .onMove { orderByFields.move(fromOffsets: $0, toOffset: $1) }
Button("Add Order By Field") { presentedSheetView = .addOrderByField } .disabled(groupByFields.isEmpty) } header: { Text("Order By Fields") } footer: { Text("(Optional)") } } } .background(Color(.systemGroupedBackground)) .overlay(alignment: .center) { if isQuerying { ProgressView("Querying Statistics") .padding() .background(.ultraThickMaterial) .clipShape(.rect(cornerRadius: 10)) .shadow(radius: 50) } } .toolbar { ToolbarItemGroup(placement: .bottomBar) { Button("Reset") { statisticDefinitions.removeAll() groupByFields.removeAll() orderByFields.removeAll() } .disabled(!formHasEdits)
Spacer()
Button("Query Statistics") { isQuerying = true } .disabled(isQuerying || statisticDefinitions.isEmpty || groupByFields.isEmpty) .task(id: isQuerying) { // Queries the statistics when the button is pressed. guard isQuerying else { return } defer { isQuerying = false }
do { statisticRecords = try await queryStatistics() presentedSheetView = .statisticRecordsList } catch { self.error = error } }
Spacer()
EditButton() .disabled(!formHasEdits) } } .sheet(item: $presentedSheetView) { sheetView in NavigationStack { switch sheetView { case .addStatisticDefinition: AddStatisticDefinitionView( definitions: $statisticDefinitions, fieldOptions: model.numericFields ) case .addGroupByFields: let unselectedFields = model.fields.filter { !groupByFields.contains($0) } AddGroupByFieldsView( groupByFields: $groupByFields, fieldOptions: unselectedFields ) case .addOrderByField: let unselectedGroupByFields = groupByFields.filter { field in !orderByFields.contains(where: { $0.fieldName == field }) } AddOrderByFieldView( orderByFields: $orderByFields, fieldOptions: unselectedGroupByFields ) case .statisticRecordsList: StatisticRecordsList(records: statisticRecords, groupByFields: groupByFields) } } } .task { // Sets up the model when the sample opens. do { try await model.setUp() } catch { self.error = error } } .errorAlert(presentingError: $error) }
/// Queries the feature table's statistics using the current view state. /// - Returns: The statistic records resulting from the query. private func queryStatistics() async throws -> [StatisticRecord] { // Creates query parameters using the added definitions and fields. let queryParameters = StatisticsQueryParameters(statisticDefinitions: statisticDefinitions) queryParameters.addGroupByFieldNames(groupByFields) queryParameters.addOrderByFields(orderByFields)
// Queries the feature table's statistics using the parameters. let queryResult = try await model.featureTable.queryStatistics(using: queryParameters)
// Gets the statistic records from the query result. return Array(queryResult.statisticRecords()) }}
/// The view model containing the feature table to query.@Observableprivate final class Model { /// A feature table containing 2013 diabetes, inactivity, and obesity data /// for counties in the US. let featureTable = ServiceFeatureTable(url: .usCounties2013Layer)
/// The name of the feature table in its source dataset. private(set) var tableName = ""
/// The names of the feature table's fields. @ObservationIgnored private(set) var fields: [String] = []
/// The names of the feature table fields that have a numeric field type. @ObservationIgnored private(set) var numericFields: [String] = []
/// Loads the feature table and sets up the model's properties. @MainActor func setUp() async throws { try await featureTable.load()
tableName = featureTable.tableName
fields = featureTable.fields.compactMap { field in field.type != .oid && field.type != .globalID ? field.name : nil }
let numericFieldTypes: Set<FieldType> = [.int16, .int32, .int64, .float32, .float64] numericFields = featureTable.fields.compactMap { field in if let type = field.type, numericFieldTypes.contains(type) { field.name } else { nil } } }}
private extension StatisticDefinition { /// The identifier for the statistic definition object. var objectID: ObjectIdentifier { .init(self) }}
extension StatisticDefinition.StatisticType { /// A human-readable label for the statistic type. var label: String { switch self { case .average: "Average" case .count: "Count" case .maximum: "Maximum" case .minimum: "Minimum" case .standardDeviation: "Standard Deviation" case .sum: "Sum" case .variance: "Variance" @unknown default: "Unknown" } }}
private extension URL { /// A URL to the "US_Counties_2013" layer on the /// "Counties_Obesity_Inactivity_Diabetes_2013" feature service. static var usCounties2013Layer: URL { URL(string: "https://services.arcgis.com/jIL9msH9OI208GCb/arcgis/rest/services/Counties_Obesity_Inactivity_Diabetes_2013/FeatureServer/0")! }}
#Preview { QueryTableStatisticsGroupAndSortView()}// Copyright 2025 Esri//// Licensed under the Apache License, Version 2.0 (the "License");// you may not use this file except in compliance with the License.// You may obtain a copy of the License at//// https://www.apache.org/licenses/LICENSE-2.0//// Unless required by applicable law or agreed to in writing, software// distributed under the License is distributed on an "AS IS" BASIS,// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.// See the License for the specific language governing permissions and// limitations under the License.
import ArcGISimport SwiftUI
/// The helper views for `QueryTableStatisticsGroupAndSortView`.extension QueryTableStatisticsGroupAndSortView { /// A view for creating and adding a statistic definition to a given list. struct AddStatisticDefinitionView: View { /// A binding to the list of statistic definitions to add to. @Binding var definitions: [StatisticDefinition]
/// The fields to select a definition field name from. let fieldOptions: [String]
/// The action to dismiss the view. @Environment(\.dismiss) private var dismiss
/// The field name selected by the "Field" picker. @State private var selectedField: String?
/// The statistic type selected by the "Statistic Type" picker. @State private var selectedStatisticType = StatisticDefinition.StatisticType.average
/// A Boolean value indicating whether the new definition is a duplicate /// of one already found in the definitions list. private var definitionIsDuplicate: Bool { guard let selectedField else { return false } return definitions.contains(where: { $0.fieldName == selectedField && $0.statisticType == selectedStatisticType }) }
var body: some View { Form { Section { NavigationLink { FieldPicker(fields: fieldOptions, selection: $selectedField) } label: { LabeledContent("Field", value: selectedField ?? "Select") }
Picker("Statistic Type", selection: $selectedStatisticType) { ForEach(StatisticDefinition.StatisticType.allCases, id: \.self) { type in Text(type.label) } } } footer: { if definitionIsDuplicate { Text("Statistic definition must be unique.") .foregroundStyle(.red) } } } .navigationTitle("Add Statistic Definition") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel", role: .cancel) { dismiss() } } ToolbarItem(placement: .confirmationAction) { Button("Add") { let newDefinition = StatisticDefinition( fieldName: selectedField!, statisticType: selectedStatisticType ) definitions.append(newDefinition)
dismiss() } .disabled(selectedField == nil || definitionIsDuplicate) } } .presentationDetents([.fraction(0.5)]) } }
/// A view for adding group by fields to a given list. struct AddGroupByFieldsView: View { /// A binding to the list of group by fields to add to. @Binding var groupByFields: [String]
/// The fields to select from in the list. let fieldOptions: [String]
/// The action to dismiss the view. @Environment(\.dismiss) private var dismiss
/// The fields selected in the list. @State private var selectedFields = Set<String>()
var body: some View { List(fieldOptions, id: \.self, selection: $selectedFields) { field in Text(field) } .environment(\.editMode, .constant(.active)) .navigationTitle("Add Group by Fields") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel", role: .cancel) { dismiss() } } ToolbarItem(placement: .confirmationAction) { Button("Add") { groupByFields.append(contentsOf: selectedFields.sorted()) dismiss() } } } } }
/// A view for creating and adding an order by field to a given list. struct AddOrderByFieldView: View { /// A binding to the list of order by fields to add to. @Binding var orderByFields: [OrderBy]
/// The fields to select an order by field name from. let fieldOptions: [String]
/// The action to dismiss the view. @Environment(\.dismiss) private var dismiss
/// The field name selected by the "Field" picker. @State private var selectedField: String?
/// A Boolean value indicating whether the new order by field will sort /// the query results in an ascending order. @State private var sortsAscending = true
var body: some View { Form { NavigationLink { FieldPicker(fields: fieldOptions, selection: $selectedField) } label: { LabeledContent("Field", value: selectedField ?? "Select") }
Toggle("Sort Ascending", isOn: $sortsAscending) } .navigationTitle("Add Order by Field") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel", role: .cancel) { dismiss() } } ToolbarItem(placement: .confirmationAction) { Button("Add") { let sortOrder = sortsAscending ? OrderBy.SortOrder.ascending : .descending let orderBy = OrderBy(fieldName: selectedField!, sortOrder: sortOrder) orderByFields.append(orderBy)
dismiss() } .disabled(selectedField == nil) } } .presentationDetents([.fraction(0.5)]) } }
/// A list that displays given statistic records and their statistics. struct StatisticRecordsList: View { /// The statistic records to show in the list. let records: [StatisticRecord]
/// The field names by which to sort the records' group values. let groupByFields: [String]
/// The action to dismiss the view. @Environment(\.dismiss) private var dismiss
var body: some View { List { Section(groupByFields.formatted(.list(type: .and, width: .narrow))) { ForEach(records, id: \.objectID) { record in if !record.statistics.isEmpty || !record.group.isEmpty { let groupDescription = record.groupValuesDescription( sortedBy: groupByFields ) NavigationLink(groupDescription) { StatisticsList(statistics: record.statistics) .navigationTitle(groupDescription) } } } } } .navigationTitle("Statistics") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .confirmationAction) { Button("Done") { dismiss() } } } } }}
/// A view for selecting a field name from a given list.private struct FieldPicker: View { /// The field name options to select from. let fields: [String]
/// A binding to the currently-selected field name. @Binding var selection: String?
/// The action to dismiss the view. @Environment(\.dismiss) private var dismiss
var body: some View { List(fields, id: \.self, selection: $selection) { field in Text(field) } .navigationTitle("Fields") .onChange(of: selection) { dismiss() } }}
/// A list that displays given statistics.private struct StatisticsList: View { /// The statistic names and values to show in the list. let statistics: [String: any Sendable]
var body: some View { List { if !statistics.isEmpty { ForEach(statistics.sorted(by: { $0.key < $1.key }), id: \.key) { name, value in if let integer = value as? Int { LabeledContent(name, value: integer, format: .number) } else if let double = value as? Double { LabeledContent(name, value: double, format: .number) } } } else { ContentUnavailableView( "No Statistics", systemImage: "list.bullet", description: Text("There are no statistics for this record.") ) } } }}
// MARK: - Extensions
private extension StatisticRecord { /// Returns the description of the records group value. /// - Parameter groupByFields: The field names by which to sort the group /// values. func groupValuesDescription(sortedBy groupByFields: [String]) -> String { let groupValues = groupByFields.compactMap { field in if let value = group[field] { "\(value)" } else { nil } }
return groupValues.formatted(.list(type: .and, width: .narrow)) }}
private extension StatisticDefinition.StatisticType { /// All of the statistic type cases. static var allCases: [Self] { return [average, count, maximum, minimum, standardDeviation, sum, variance] }}
private extension StatisticRecord { /// The identifier for the statistic record object. var objectID: ObjectIdentifier { .init(self) }}