Database Schema
The database is stored at ~/.healthsync/healthsync.db by default. You can query it directly with sqlite3 for advanced analysis.
Tables
Cardiac / Vitals
heart_rate
CREATE TABLE heart_rate (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source_name TEXT NOT NULL,
start_date TEXT NOT NULL,
end_date TEXT NOT NULL,
value REAL NOT NULL, -- BPM
unit TEXT NOT NULL, -- "count/min"
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
UNIQUE(source_name, start_date, end_date, value)
);
steps
CREATE TABLE steps (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source_name TEXT NOT NULL,
start_date TEXT NOT NULL,
end_date TEXT NOT NULL,
value REAL NOT NULL, -- count
unit TEXT NOT NULL DEFAULT 'count',
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
UNIQUE(source_name, start_date, end_date, value)
);
spo2
CREATE TABLE spo2 (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source_name TEXT NOT NULL,
start_date TEXT NOT NULL,
end_date TEXT NOT NULL,
value REAL NOT NULL, -- fraction (0.98 = 98%)
unit TEXT NOT NULL DEFAULT '%',
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
UNIQUE(source_name, start_date, end_date, value)
);
Note: SpO2 values are stored as fractions (0.0–1.0), not percentages. 0.98 means 98%.
vo2_max
CREATE TABLE vo2_max (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source_name TEXT NOT NULL,
start_date TEXT NOT NULL,
end_date TEXT NOT NULL,
value REAL NOT NULL, -- mL/min·kg
unit TEXT NOT NULL,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
UNIQUE(source_name, start_date, end_date, value)
);
sleep
CREATE TABLE sleep (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source_name TEXT NOT NULL,
start_date TEXT NOT NULL,
end_date TEXT NOT NULL,
value TEXT NOT NULL, -- sleep stage
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
UNIQUE(source_name, start_date, end_date, value)
);
Note: The sleep table has no
unitcolumn — it stores category values, not quantities.
Sleep stage values:
| Value | Meaning |
|---|---|
HKCategoryValueSleepAnalysisInBed | In bed |
HKCategoryValueSleepAnalysisAsleepCore | Core sleep |
HKCategoryValueSleepAnalysisAsleepDeep | Deep sleep |
HKCategoryValueSleepAnalysisAsleepREM | REM sleep |
HKCategoryValueSleepAnalysisAwake | Awake |
HKCategoryValueSleepAnalysisAsleepUnspecified | Unspecified |
resting_heart_rate
CREATE TABLE resting_heart_rate (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source_name TEXT NOT NULL,
start_date TEXT NOT NULL,
end_date TEXT NOT NULL,
value REAL NOT NULL, -- count/min (BPM)
unit TEXT NOT NULL,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
UNIQUE(source_name, start_date, end_date, value)
);
hrv
CREATE TABLE hrv (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source_name TEXT NOT NULL,
start_date TEXT NOT NULL,
end_date TEXT NOT NULL,
value REAL NOT NULL, -- ms (SDNN)
unit TEXT NOT NULL,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
UNIQUE(source_name, start_date, end_date, value)
);
heart_rate_recovery
CREATE TABLE heart_rate_recovery (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source_name TEXT NOT NULL,
start_date TEXT NOT NULL,
end_date TEXT NOT NULL,
value REAL NOT NULL, -- count/min
unit TEXT NOT NULL,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
UNIQUE(source_name, start_date, end_date, value)
);
respiratory_rate
CREATE TABLE respiratory_rate (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source_name TEXT NOT NULL,
start_date TEXT NOT NULL,
end_date TEXT NOT NULL,
value REAL NOT NULL, -- breaths/min
unit TEXT NOT NULL,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
UNIQUE(source_name, start_date, end_date, value)
);
blood_pressure
Blood pressure is stored as a paired reading — systolic and diastolic are matched by source_name + start_date during parsing and written as a single row.
CREATE TABLE blood_pressure (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source_name TEXT NOT NULL,
start_date TEXT NOT NULL,
end_date TEXT NOT NULL,
systolic REAL NOT NULL, -- mmHg
diastolic REAL NOT NULL, -- mmHg
unit TEXT NOT NULL, -- "mmHg"
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
UNIQUE(source_name, start_date, end_date, systolic, diastolic)
);
Activity / Energy
The following tables share the standard schema (source_name, start_date, end_date, value REAL, unit TEXT):
active_energy— active calories burned (kcal). Supports--totalfor deduplicated daily totals.basal_energy— resting/basal calories burned (kcal). Supports--total.exercise_time— exercise minutesstand_time— stand minutesflights_climbed— flights of stairsdistance_walking_running— walk/run distancedistance_cycling— cycling distance
CREATE TABLE active_energy (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source_name TEXT NOT NULL,
start_date TEXT NOT NULL,
end_date TEXT NOT NULL,
value REAL NOT NULL, -- kcal
unit TEXT NOT NULL,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
UNIQUE(source_name, start_date, end_date, value)
);
Body
The following tables share the standard schema:
body_mass— body weight (kg/lb)body_mass_index— BMIheight— height (m/ft)
Mobility / Walking
The following tables share the standard schema:
walking_speed— m/swalking_step_length— mwalking_asymmetry— %walking_double_support— %walking_steadiness— scorestair_ascent_speed— ft/sstair_descent_speed— ft/ssix_minute_walk— m
Running metrics
The following tables share the standard schema:
running_speed— m/srunning_power— Wrunning_stride_length— mrunning_ground_contact_time— msrunning_vertical_oscillation— cm
Other quantity types
The following tables share the standard schema:
wrist_temperature— °C deviation from baselinetime_in_daylight— minutesdietary_water— mL/Lphysical_effort— MET scorewalking_heart_rate— BPM average while walking
mindful_sessions
CREATE TABLE mindful_sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source_name TEXT NOT NULL,
start_date TEXT NOT NULL,
end_date TEXT NOT NULL,
value TEXT NOT NULL, -- category value
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
UNIQUE(source_name, start_date, end_date, value)
);
Note: No
unitcolumn — this is a category type. Duration can be derived fromend_date - start_date.
stand_hours
CREATE TABLE stand_hours (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source_name TEXT NOT NULL,
start_date TEXT NOT NULL,
end_date TEXT NOT NULL,
value TEXT NOT NULL, -- HKCategoryValueAppleStandHourStood or Idle
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
UNIQUE(source_name, start_date, end_date, value)
);
Note: No
unitcolumn — this is a category type.
workouts
CREATE TABLE workouts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
activity_type TEXT NOT NULL,
source_name TEXT NOT NULL,
start_date TEXT NOT NULL,
end_date TEXT NOT NULL,
duration REAL, -- minutes (nullable)
duration_unit TEXT,
total_distance REAL, -- nullable
total_distance_unit TEXT,
total_energy_burned REAL, -- nullable
total_energy_burned_unit TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
UNIQUE(activity_type, start_date, end_date, source_name)
);
Note: Distance and energy fields are nullable — not all workout types track them (e.g. Yoga has no distance).
Common activity_type values: HKWorkoutActivityTypeRunning, HKWorkoutActivityTypeWalking, HKWorkoutActivityTypeCycling, HKWorkoutActivityTypeYoga, HKWorkoutActivityTypeSwimming, HKWorkoutActivityTypeHighIntensityIntervalTraining, HKWorkoutActivityTypeTraditionalStrengthTraining.
Date format
All dates are stored as text in Apple Health format:
2024-01-15 08:30:00 +0530
When filtering with --from / --to, use date prefixes — SQLite does string comparison, so 2024-01-01 matches all timestamps on that day.
Useful queries
Daily step totals
SELECT date(start_date) as day, ROUND(SUM(value)) as total_steps
FROM steps
GROUP BY day
ORDER BY day DESC
LIMIT 7;
Average heart rate per day
SELECT date(start_date) as day,
ROUND(AVG(value), 1) as avg_hr,
MIN(value) as min_hr,
MAX(value) as max_hr
FROM heart_rate
GROUP BY day
ORDER BY day DESC
LIMIT 30;
Sleep duration per night
SELECT date(start_date) as night,
ROUND(SUM((julianday(end_date) - julianday(start_date)) * 24), 1) as hours
FROM sleep
WHERE value LIKE '%Asleep%'
GROUP BY night
ORDER BY night DESC
LIMIT 14;
Workout summary by type
SELECT activity_type,
COUNT(*) as count,
ROUND(AVG(duration), 1) as avg_min,
ROUND(SUM(total_energy_burned)) as total_kcal
FROM workouts
GROUP BY activity_type
ORDER BY count DESC;
Weekly VO2 Max trend
SELECT strftime('%Y-W%W', start_date) as week,
ROUND(AVG(value), 2) as avg_vo2
FROM vo2_max
GROUP BY week
ORDER BY week DESC
LIMIT 12;
Deduplication
All tables use UNIQUE constraints and INSERT OR IGNORE for idempotent imports. Re-running healthsync parse on the same export inserts 0 new rows.
Indexes
All tables have an index on start_date for efficient date-range queries.