Database Schema

The database is stored at ~/.healthsync/healthsync.db by default. You can query it directly with sqlite3 for advanced analysis.

Tables

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 unit column — it stores category values, not quantities.

Sleep stage values:

ValueMeaning
HKCategoryValueSleepAnalysisInBedIn bed
HKCategoryValueSleepAnalysisAsleepCoreCore sleep
HKCategoryValueSleepAnalysisAsleepDeepDeep sleep
HKCategoryValueSleepAnalysisAsleepREMREM sleep
HKCategoryValueSleepAnalysisAwakeAwake
HKCategoryValueSleepAnalysisAsleepUnspecifiedUnspecified

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.