← All posts
September 10, 2026·schemapostgreshealthkit

Apple Health Data Schema: What's in Your Database

Apple Health Data Schema: What’s in Your Database

Understanding the health4ai database schema lets you write custom SQL queries, build dashboards directly on the database, and debug sync issues. Here’s a complete reference.

Two Tables

health4ai uses a two-tier storage architecture:

Beyond 30 days, raw samples are no longer individually stored — the daily summary captures the aggregate. This keeps the database size manageable while preserving the analytical signal.

healthkit_metrics

CREATE TABLE healthkit_metrics (
    id            BIGSERIAL PRIMARY KEY,
    user_id       TEXT NOT NULL,
    metric_type   TEXT NOT NULL,
    value         DOUBLE PRECISION,
    unit          TEXT,
    started_at    TIMESTAMPTZ NOT NULL,
    ended_at      TIMESTAMPTZ,
    source_name   TEXT,
    source_device TEXT,
    metadata      JSONB,
    created_at    TIMESTAMPTZ DEFAULT NOW()
);

-- Primary query index
CREATE INDEX idx_healthkit_metrics_user_type_time 
  ON healthkit_metrics (user_id, metric_type, started_at DESC);

-- Source filtering (used for sleep deduplication)
CREATE INDEX idx_healthkit_metrics_source 
  ON healthkit_metrics (user_id, metric_type, source_device);

Columns:

Column Type Description
user_id TEXT User identifier (matches HEALTHKIT_USER_ID env var)
metric_type TEXT HealthKit quantity/category type identifier
value DOUBLE PRECISION Numeric value (null for some workout records)
unit TEXT HealthKit unit string (count/min, ms, %, etc.)
started_at TIMESTAMPTZ Sample start time (UTC)
ended_at TIMESTAMPTZ Sample end time (null for instantaneous samples)
source_name TEXT App that wrote the sample (e.g., “Health”, “Oura”)
source_device TEXT Device model string (e.g., “Apple Watch Series 9”)
metadata JSONB Additional data — workout type, duration, distance, etc.

Workout records store their detail in the metadata JSONB column:

{
  "workout_type": "Running",
  "duration_seconds": 2340.0,
  "total_distance_meters": 5820.0,
  "total_energy_burned_cal": 387.0
}

Sleep records use integer values for stage codes:

healthkit_daily_summaries

CREATE TABLE healthkit_daily_summaries (
    id            BIGSERIAL PRIMARY KEY,
    user_id       TEXT NOT NULL,
    metric_type   TEXT NOT NULL,
    date          DATE NOT NULL,
    avg_value     DOUBLE PRECISION,
    min_value     DOUBLE PRECISION,
    max_value     DOUBLE PRECISION,
    sum_value     DOUBLE PRECISION,
    sample_count  INTEGER,
    UNIQUE (user_id, metric_type, date)
);

CREATE INDEX idx_healthkit_daily_summaries_user_type_date
  ON healthkit_daily_summaries (user_id, metric_type, date DESC);

Columns:

Column Description
date Calendar date (America/New_York timezone)
avg_value Mean of all samples that day
min_value Minimum sample value that day
max_value Maximum sample value that day
sum_value Sum of all samples (used for cumulative metrics: steps, energy)
sample_count Number of raw samples that contributed

Common HealthKit Type Identifiers

Activity

Metric Identifier
Steps HKQuantityTypeIdentifierStepCount
Distance (walk/run) HKQuantityTypeIdentifierDistanceWalkingRunning
Distance (cycling) HKQuantityTypeIdentifierDistanceCycling
Active energy HKQuantityTypeIdentifierActiveEnergyBurned
Basal energy HKQuantityTypeIdentifierBasalEnergyBurned
Exercise time HKQuantityTypeIdentifierAppleExerciseTime
Stand time HKQuantityTypeIdentifierAppleStandTime
Flights climbed HKQuantityTypeIdentifierFlightsClimbed

Vitals

Metric Identifier
Heart rate HKQuantityTypeIdentifierHeartRate
Resting HR HKQuantityTypeIdentifierRestingHeartRate
Walking HR avg HKQuantityTypeIdentifierWalkingHeartRateAverage
HRV (SDNN) HKQuantityTypeIdentifierHeartRateVariabilitySDNN
Blood oxygen HKQuantityTypeIdentifierOxygenSaturation
Respiratory rate HKQuantityTypeIdentifierRespiratoryRate

Body

Metric Identifier
Weight HKQuantityTypeIdentifierBodyMass
BMI HKQuantityTypeIdentifierBodyMassIndex
Body fat % HKQuantityTypeIdentifierBodyFatPercentage
Lean mass HKQuantityTypeIdentifierLeanBodyMass
VO2 Max HKQuantityTypeIdentifierVO2Max

Sleep and Workouts

Metric Identifier
Sleep analysis HKCategoryTypeIdentifierSleepAnalysis
Workouts HKWorkoutTypeIdentifier

Example Queries

All metric types in your database:

SELECT metric_type, COUNT(*) as rows,
       MIN(started_at)::date as earliest,
       MAX(started_at)::date as latest
FROM healthkit_metrics
WHERE user_id = 'your_user_id'
GROUP BY metric_type
ORDER BY rows DESC;

Monthly HRV averages (from daily summaries):

SELECT DATE_TRUNC('month', date) as month,
       ROUND(AVG(avg_value)::numeric, 1) as avg_hrv_ms,
       COUNT(*) as days_with_data
FROM healthkit_daily_summaries
WHERE user_id = 'your_user_id'
  AND metric_type = 'HKQuantityTypeIdentifierHeartRateVariabilitySDNN'
GROUP BY month
ORDER BY month DESC;

Recent sleep stage records:

SELECT started_at, ended_at, value,
       ROUND((EXTRACT(EPOCH FROM (ended_at - started_at)) / 60)::numeric, 1) as duration_min,
       CASE value::int
         WHEN 3 THEN 'Core'
         WHEN 4 THEN 'Deep'
         WHEN 5 THEN 'REM'
         WHEN 2 THEN 'Awake'
       END as stage
FROM healthkit_metrics
WHERE user_id = 'your_user_id'
  AND metric_type = 'HKCategoryTypeIdentifierSleepAnalysis'
  AND value IN (2, 3, 4, 5)
  AND started_at >= NOW() - INTERVAL '7 days'
ORDER BY started_at DESC;

Workout list with metadata:

SELECT started_at::date as date,
       metadata->>'workout_type' as type,
       ROUND((metadata->>'duration_seconds')::numeric / 60, 0) as duration_min,
       ROUND((metadata->>'total_distance_meters')::numeric / 1000, 2) as distance_km,
       ROUND((metadata->>'total_energy_burned_cal')::numeric) as calories
FROM healthkit_metrics
WHERE user_id = 'your_user_id'
  AND metric_type = 'HKWorkoutTypeIdentifier'
  AND started_at >= NOW() - INTERVAL '30 days'
ORDER BY started_at DESC;

health4ai: Free while in early access.
Download on the App Store →