100日チャレンジ day26 (時系列DB)

昨日
100日チャレンジに感化されたので、アレンジして自分でもやってみます。
やりたいこと
- 世の中のさまざまなドメインの簡易実装をつくり、バックエンドの実装に慣れる(dbスキーマ設計や、関数の分割、使いやすいインターフェイスの切り方に慣れる
- 設計力(これはシステムのオーバービューを先に自分で作ってaiに依頼できるようにする
- 生成aiをつかったバイブコーティングになれる
- 実際にやったことはzennのスクラップにまとめ、成果はzennのブログにまとめる(アプリ自体の公開は必須ではないかコードはgithubにおく)
できたもの

SQLiteで時系列DBみたいなことができるらしい

承知しました。「簡易 時系列データベース on SQLite」でいきましょう!
SQL に精通されているとのことですので、単なる CRUD + α だけではなく、SQLite の機能をもう少し深掘りするような、目新しさを感じられる要素を盛り込んでいきましょう。
特に、SQLite でも比較的新しいバージョンでサポートされたウィンドウ関数 (Window Functions) を活用したり、時系列データ特有の処理(ダウンサンプリングやギャップフィル)を SQL でどう実現するか、といった点に焦点を当てると面白いかもしれません。Prisma を使いつつも、複雑なクエリは prisma.$queryRaw
などで生の SQL を書く場面も出てくるでしょう。
実装する機能案(目新しさ重視):
-
基本 API:
- データ登録 (
POST /api/data
): key, timestamp (Unix秒), value を登録。 - 時間範囲データ取得 (
GET /api/data
): key, start, end を指定して生データを取得。
- データ登録 (
-
集計 API (
GET /api/data/aggregated
):- パラメータ:
key
,start
,end
,interval
(minute, hour, day),aggregation
(avg, max, min, sum, count)。 - SQLite の
strftime
や算術演算、GROUP BY
、集計関数を活用。
- パラメータ:
-
ダウンサンプリング API (
GET /api/data/downsampled
):- パラメータ:
key
,start
,end
,method
(every_nth
またはaggregate
),factor
。 -
every_nth
: ウィンドウ関数ROW_NUMBER()
を使って N 件ごと (factor
) にデータを抽出。SQLでの実現。 -
aggregate
: 指定した時間間隔 (factor
) で集計 API と同様の処理。SQLでの実現。
- パラメータ:
-
最新 N 件取得 API (
GET /api/data/latest
):- パラメータ:
key
,limit
。 -
ORDER BY timestamp DESC LIMIT N
。
- パラメータ:
-
(発展) ギャップフィル API (
GET /api/data/gapfilled
):- パラメータ:
key
,start
,end
,interval
,fill_method
(previous
またはfixed_value
)。 -
previous
: ウィンドウ関数LAG()
を使って、データがない場合に直前の値で補完する。SQL での実現を目指す。 -
fixed_value
: データがない場合に指定した固定値で補完する。(これは比較的簡単) - 線形補間 (
linear
) は SQL だけでは複雑なので、今回は見送るか、取得後にサーバーサイドで処理する形にします。
- パラメータ:
-
UI:
- データ登録フォーム。
- データ表示・グラフ化 (Chart.js) エリア。
- キー選択、時間範囲選択。
- 集計、ダウンサンプリング、ギャップフィルのオプション選択。
- グラフ表示。

最近毎度同じデザイン作ってくるのでやめたい
毎回異なるモダンなデザイントレンド(例: ミニマリズム、グラスモーフィズム、ニューモーフィズム、ブルータリズムなど)を積極的に試すこと。過去のプロジェクトのデザインとは明確に差別化すること

変えてもらった

承知しました。各APIエンドポイントが内部でどのようなSQLクエリを実行しているか、具体的に説明します。
各APIエンドポイントが実行するSQLクエリ
以下に、各APIが実行する主なSQLクエリ(またはPrismaが生成するSQLクエリの意図)を説明します。パラメータ (?
の部分) はリクエストに応じて変わります。
-
GET /api/keys
- 目的: 登録されているユニークなキーの一覧を取得する。
-
方法: Prisma の
findMany
とdistinct
オプションを使用。 -
実行されるSQL (相当):
SELECT DISTINCT "key" FROM "TimeSeriesData" ORDER BY "key" ASC
-
POST /api/data
- 目的: 1つまたは複数の新しい時系列データを挿入する。
-
方法: Prisma の
createMany
を使用。 -
実行されるSQL (相当):(リクエストボディのデータ数に応じて
INSERT INTO "TimeSeriesData" ("key", "timestamp", "value", "createdAt") VALUES (?, ?, ?, ?), (?, ?, ?, ?), ...
VALUES (...)
が繰り返される)
-
GET /api/data
(Raw Data)- 目的: 指定されたキーの生データを、任意で指定された時間範囲で取得する。
-
方法: Prisma の
findMany
を使用。 -
実行されるSQL (相当):(
SELECT "id", "timestamp", "key", "value", "createdAt" FROM "TimeSeriesData" WHERE "key" = ? AND "timestamp" >= ? AND "timestamp" <= ? ORDER BY "timestamp" ASC
AND "timestamp" >= ?
とAND "timestamp" <= ?
の部分は、start
/end
パラメータが指定された場合のみ追加される)
-
GET /api/data/aggregated
- 目的: データを指定の時間間隔でグルーピングし、指定の集計関数を適用する。
-
方法:
prisma.$queryRaw
で生のSQLを実行。 -
実行されるSQL (例:
interval=hour
,aggregation=avg
の場合):(SELECT strftime('%Y-%m-%d %H:00:00', "timestamp", 'unixepoch') AS "interval_start", AVG("value") AS "aggregated_value" FROM "TimeSeriesData" WHERE "key" = ? AND "timestamp" >= ? AND "timestamp" <= ? GROUP BY "interval_start" ORDER BY "interval_start" ASC
strftime
のフォーマットはinterval
パラメータに応じて、AVG
の部分はaggregation
パラメータに応じて変わる。WHERE
句のtimestamp
条件は任意。)
-
GET /api/data/downsampled
- 目的: データを間引いて取得する。
-
方法:
prisma.$queryRaw
で生のSQLを実行。-
method=every_nth
の場合: ウィンドウ関数ROW_NUMBER()
を使用。(SELECT "timestamp", "value" FROM ( SELECT "timestamp", "value", ROW_NUMBER() OVER (ORDER BY "timestamp" ASC) as "rn" FROM "TimeSeriesData" WHERE "key" = ? AND "timestamp" >= ? AND "timestamp" <= ? ) WHERE "rn" % ? = 1 -- factor パラメータで割った余りが 1 の行を抽出 ORDER BY "timestamp" ASC
WHERE
句のtimestamp
条件は任意。) -
method=aggregate
の場合: Unixタイムスタンプの整数除算でグルーピング。(SELECT AVG("value") AS "aggregated_value", -- 現在は AVG 固定 ("timestamp" / ?) AS "time_group" -- factor パラメータで整数除算 FROM "TimeSeriesData" WHERE "key" = ? AND "timestamp" >= ? AND "timestamp" <= ? GROUP BY "time_group" ORDER BY "time_group" ASC
WHERE
句のtimestamp
条件は任意。結果はJS側でtimestamp: Number(time_group) * factor
に変換される。)
-
-
GET /api/data/latest
- 目的: 指定されたキーの最新 N 件のデータを取得する。
-
方法: Prisma の
findMany
とorderBy
,take
を使用。 -
実行されるSQL (相当):(取得後、JS側で
SELECT "id", "timestamp", "key", "value", "createdAt" FROM "TimeSeriesData" WHERE "key" = ? ORDER BY "timestamp" DESC -- 最新が先頭に来るように降順 LIMIT ? -- limit パラメータで件数指定
timestamp
の昇順に並び替え直している)
これらの説明で、各APIが内部でどのようなクエリを実行しているかご理解いただけたでしょうか?

なるほど、SQLって色々できるんだな〜〜〜。
うーんたまにしか使わないので忘れることを考えるとAIにお願いしたい