習慣トラッカーの『ストリークが切れたら終わり』を作らないDB・通知設計
C言語や組み込みの仕事をしながら、副業で自分用の習慣トラッカーを作ったことがある人向けの記事。自分は制御系エンジニアとして23年やってきて、副業で自作ツールを作るようになったのはここ数年の話。
最初に作った習慣トラッカーは3週間で自分自身が使わなくなった。理由は単純で、1日サボったらストリークが0に戻る設計にしていたからだ。データベースの持ち方と通知の出し方を変えたら、この現象は防げることが分かった。今回はそのDB設計と通知ロジックの話を書く。
結論から言うと
ストリークを「連続日数のカウンタ」として持つのをやめて、「実施ログの集合」として持つように変えた。カウンタ方式だと1日抜けた瞬間に数字がリセットされ、心理的なダメージが数字としてそのまま可視化される。ログ方式なら「28日中22日やった」という見え方に変えられる。
通知も「今日やってないですよ」ではなく「今週あと2日で目標達成」という進捗ベースの文言に変えた。これだけでアプリを開く頻度が体感で倍くらいになった。
なぜストリークは折れると再起不能になるのか
自分が最初に作ったトラッカーのテーブル設計はこうだった。
CREATE TABLE habit_streak (
habit_id INTEGER PRIMARY KEY,
current_streak INTEGER,
last_done_date TEXT
);
last_done_dateが昨日でなければcurrent_streakを0に戻す。これだけの単純なロジックだったが、副業でコード書く習慣も、ダイエットの歩数記録も、これで3回くらい心が折れた。
100日続けていたものが1日抜けて0に戻ると、その瞬間に「もう100日分の意味がなくなった」という感覚になる。実際にはデータとして100日分の実施記録は消えていないのに、UIがカウンタ1個しか見せていないせいで、そう錯覚してしまう。
これはHabiticaのような他人と競うタイプの習慣化アプリで顕著だと感じている。リーグ降格や連続記録の可視化が、続けている人にはご褒美になるが、1回でも折れた人には強い離脱理由になる。自分は後者のタイプで、40日目で1日サボった時に完全にアプリを開かなくなった。
C言語のバグ修正と似ていて、「動いてたのに1箇所直したら全部崩れた」という状態に近い。1つの欠損が全体の評価を破壊する設計は、人間のモチベーション管理には向いていないという実感がある。
解決手順:ログ集合+期間集計への切り替え
ステップ1:ストリークカウンタを廃止し、実施ログテーブルだけを持つ
まずテーブル設計を変えた。カウンタという「状態」を持たず、いつ何をやったかという「事実」だけを記録する。
CREATE TABLE habit_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
habit_id INTEGER NOT NULL,
done_date TEXT NOT NULL,
UNIQUE(habit_id, done_date)
);
これだけだと当たり前に見えるが、ポイントは「連続日数」という概念をテーブルに一切持たせないこと。連続日数は表示のたびに計算する値であって、保存する値ではないと決めた。
ステップ2:表示側で「連続日数」ではなく「達成率」を計算する
直近28日のログを取ってきて、達成率を出す。SQLiteで直近28日分を取るクエリはこうなる。
SELECT COUNT(*) FROM habit_log
WHERE habit_id = ?
AND done_date >= date('now', '-27 days');
このカウントを28で割って達成率にする。22/28なら78%というように、1日抜けても数字が緩やかにしか動かない。100日連続が0になるショックと、78%が75%に下がるショックでは、心理的な破壊力が全然違う。
ステップ3:通知文言を「催促」から「進捗」に変える
通知バッチのC言語側の擬似コードはこんな感じにした。
int done_this_week = count_done_in_range(habit_id, week_start, today);
int target = get_weekly_target(habit_id); // 例: 週5日
if (done_this_week < target) {
int remain_days = target - done_this_week;
int remain_dates = days_left_in_week(today);
// 「今週あと2日で達成」のような文言を生成
generate_notification(habit_id, remain_days, remain_dates);
}
「サボってますよ」ではなく「あと2日で今週の目標達成」という文言にしただけで、通知をタップする率が自分の手元の記録では週3回から週6回くらいに増えた。厳密なABテストはしていないので体感値だが、開く理由が「責められる」から「達成に近い」に変わったのは大きい。
ステップ4:週次・月次のリセットラインを作る
達成率も無限に遡ると「半年前サボったせいで一生70%台」みたいな話になりかねない。なので集計期間を「直近28日」のように固定窓にして、古いログは集計対象から自然に外れるようにした。過去の失敗が未来永劫足を引っ張らない設計にしている。
この設計に変えてから、自分の副業のコード執筆習慣は、月20日中14〜16日くらいのペースで続いている。100%は目指していないし、100%を目指す設計にすると大体また心が折れる。
落とし穴と対処
実際にこの設計に切り替える過程で詰まった点が3つある。
1つ目はUNIQUE(habit_id, done_date)の制約忘れ。最初これを入れておらず、1日に2回ログを送ってしまうバグがあり、達成率が実態より高く出るという不具合を起こした。組み込みで培った「境界値を疑う」感覚が、こういうアプリ開発でもそのまま使えると実感した。
2つ目はタイムゾーンの扱い。date('now')はUTC基準になるため、日本時間の深夜0時〜9時に記録したログが前日扱いになるケースがあった。ローカルタイムでの日付変換を通知バッチ側に入れて解決したが、この手のバグは本番で気づきにくく、自分は2週間気づかなかった。
3つ目は達成率だけを見せると「今日やらなくていいや」という油断が生まれること。これに対しては、達成率の横に「今日やったか」というシンプルなON/OFF表示も残した。進捗の安心感と当日のアクションを両方見せる必要があると分かった。
この手の「1人称視点の行動ログをどう貯めて、どう見せて、どう通知するか」という設計は、副業のコミット管理でもダイエットの歩数管理でも根っこは同じだと感じている。自分はこの仕組みを、副業のコミットとダイエットの歩数を同じキャラクターで育てる形にしたツールとして作り直した。それがALTERという名前で、自分自身が90日間のドッグフーディング中(3kg減と月の副業収益+5万円を目標に、Xで記録を公開している)。
関連ツール
副業のcommitとダイエットの歩数を1つのキャラで育てる、焦らせない人生OSとしてALTERを作っている。行動ログから職業を判定する仕組みや、食事・運動・集中時間・副業の指標を1つのキャラクターに集約する設計は、この記事で書いたログ集合の考え方をベースにしている。
→ ALTER
まとめ
ストリークのカウンタ方式は、1日抜けた瞬間に積み上げてきた実感ごと0に戻すという構造的な欠陥を持っている。実施ログを集合として持ち、期間内の達成率で見せる方式に変えるだけで、心理的な離脱ポイントをかなり減らせるという実感がある。通知文言も「サボり指摘」から「進捗共有」に変えるだけで、開く動機が変わる。習慣化ツールを自作する人は、テーブル1個分の設計判断でここまで挙動が変わるということを一度試してみてほしい。
著者:ぽん(@pon_freelance)
C言語実務23年、組み込み/制御系。
副業で技術記事販売と自作ツール販売をやっている。
書いているもの:
- ALTER - 副業の commit とダイエットの歩数を1つのキャラで育てる、焦らせない人生 OS(SaaS)
(その他:(なし))