AIに丸投げしたコードを見直したら、SELECTだけで死ぬほど遅かった話

前提 個人開発のEDINET分析ツール(財務レポートを解析してDiscordに投げる趣味プロジェクト)で、フロントエンドのコードをAIにほぼ丸投げして書かせた部分があった。 後から見直していたら、明らかに厳しい実装が出てきたので調査した記録。今回は原因の特定と応急処置(SELECT修正)までで、DBスキーマ自体の見直しは今後の課題として持ち越す。 出てきたコード API利用状況を表示するダッシュボードコンポーネントで、以下のように4本のAPIをPromise.allで並列取得していた。 const [statsRes, logsRes, tsRes, statusRes] = await Promise.all([ apiClient.get(`/api-logs/stats?days=${days}`), apiClient.get('/api-logs/logs?limit=50'), apiClient.get(`/api-logs/timeseries?days=${days}&interval=${interval}`), apiClient.get(`/api-logs/status?days=${days}`), ]) 取得後、4つのstateをfinallyブロックで一括更新し、loadingも1つのフラグで管理していた。 最初の疑問:Promise.allの並列度は妥当か Promise.all自体は4本のリクエストを同時発火するので、直列でawaitを重ねる実装(MDN: Promise.all)と比べれば理にかなっている。 ただし設計上の課題は残る。 loadingが1つのフラグなので、一番遅いレスポンスに画面全体の表示タイミングが引っ張られる useEffectの依存配列が[days, interval]のみで、intervalしか変えていない操作でも無関係なlogsまで再取得される 対策案としては、各エンドポイントを個別のuseEffect+個別stateに分割し、届いた順にレンダリングする方式が考えられる。この場合、依存配列も自然に分離でき、interval変更時に不要な再取得も防げる。ただしこれは「体感速度」の改善であり、根本的なネットワーク・サーバー負荷の解消にはならない。 実測結果 Chrome DevToolsのNetworkタブで実測したところ、以下の結果になった。 エンドポイント サイズ レスポンスタイム(1回目) レスポンスタイム(2回目) stats?days=7 0.3kB 5.69秒 6.25秒 logs?limit=50 7.7kB 200ms 351ms timeseries?days=7&interval=hour 14.5kB 11.52秒 6.01秒 status?days=7 0.5kB 7.92秒 11.58秒 同一アプリの別画面(Stats.tsx)にあるsummary・alerts・ranking系のエンドポイントは、同程度の小さいペイロードで30〜111msと桁違いに速い。 結論:問題はフロントの並列化ではなくバックエンド側 数百バイト〜十数キロバイトのレスポンスに対して秒単位〜十数秒のレイテンシは、ネットワークやReact側の描画方式では説明がつかない差だった。並列fetch自体は問題ではなく、疑うべきは以下。 集計クエリのN+1(プロバイダーごとにループでクエリを投げている可能性) 日時カラムへのインデックス未設定(daysによるWHERE句がフルスキャンになっている可能性) interval=hourの集計処理(DB側の集計かアプリ側ループかで原因が分岐) 2回目の計測でむしろ悪化(stats5.69→6.25秒、status7.92→11.58秒)していることから、キャッシュが効いていない、あるいはテーブルの成長に伴って劣化する設計になっている可能性 N+1問題自体についてはN+1 query problem (MDN Glossary)を参照。 今回の対応範囲 今回は該当エンドポイントのSQLクエリを見直し、集計方法をSELECT文レベルで修正するところまでを実施した。 DBスキーマ自体(インデックス追加や正規化の見直しなど)には手を付けていない。テーブルが育つほど劣化する可能性が実測から示唆されているため、これは今後の課題として残す。 教訓 AIに丸投げしたコードは、動いているように見えても「並列化されているか」程度の表面的なチェックだけでは不十分 フロントエンドの実装が妥当でも、DevToolsのNetworkタブで実測しない限りボトルネックの所在は特定できない 「並列処理にしたから速いはず」という思い込みは、実測前の仮説に過ぎない

2026年9月16日 · 1 min

Androidアプリ開発の極意 2,3章あたり ANR、UIスレッド

Androidアプリ開発の極意 | 技術評論社 この本読んでる。2017年に出版してて、内容には古いものがいくつかある。 そこはAIの使いどころ。とりあえず学習したことを軽くメモして、中身について古い場合は指摘を入れたり、今は何が主流なのかといったことを補完するような指示を出した。 サポート付きの読書っぽいことをしながら読み進めて、文章をまとめた。 概要 React Native/Expoでアプリを書いていると、「なぜかカクつくが、クラッシュログには何も残っていない」という現象に何度もぶつかる。 原因を追っていくと、大体はUIスレッドを塞いでいるか、メモリを食い潰しているかのどちらかという同じ根っこに行き着くことが多い気がする。 この記事では、Androidアプリ開発の極意 | 技術評論社の3章UIスレッド/ANRの基礎から、React NativeにおけるJS-ネイティブのスレッド分離、コルーチンとasync/awaitの正体、Android/iOS/RNの非同期処理対応、そして見落とされがちなメモリ管理(OOM・LMK・Javaのメモリリーク・useEffectのクリーンアップ)までを、実務で踏んだり踏みそうな地雷も交えて整理した。 なお、私は仕事でしばしReact Native Expoを利用しているため、なるべくそちらに沿った内容に寄せて本文を作成する。 それによって本の内容とは離れることがしばしあるが、そこは予め留意してほしい。 全体像 まずスレッドの分離構造をざっくり図にする。 [Android/ネイティブ] UIスレッド(メインスレッド) ── 描画・入力処理を担当 │ │ 5秒ブロック → ANRダイアログ ▼ ワーカースレッド/コルーチン(Dispatchers.IO等) ── 重い処理はここへ逃がす [React Native/Expo] JSスレッド ── ビジネスロジック・状態更新・レンダリング計算 │ │ (ブリッジ/JSI経由) ▼ ネイティブUIスレッド ── 実際の描画 │ (任意) Worklet用の別JSコンテキスト ── react-native-worklets-core / Reanimated ポイントは、Androidのネイティブ層には「ANR」という可視化された強制終了の仕組みがあるが、React NativeのJSスレッドが詰まっても同じ仕組みは働かないという非対称性にある。以下、順番に見ていく。 UIスレッド/ANRの基礎 Androidでは、UIスレッド(メインスレッド)が入力イベントに応答できない状態が一定時間続くと、システムが強制的にANR(Application Not Responding)ダイアログを出す。Android公式ドキュメントによれば、入力イベント(キー押下や画面タップなど)に5秒以内に応答しない場合にANRがトリガーされる。これはフォアグラウンドの入力ディスパッチに関する条件で、他にもブロードキャストレシーバーやサービスの応答遅延など複数のANR条件が存在する。 一方、React Native/Expoでは事情が異なる。RNのアーキテクチャはJSスレッドとネイティブUIスレッドが分離されている。JSスレッドが重い処理で詰まっても、ネイティブ側のUIスレッド自体がブロックされているわけではないため、Android OSが検知する「入力ディスパッチのタイムアウト」という意味でのANRダイアログは出にくい。ただし、体感としては「タップしても反応がない」「アニメーションがカクつく」という、ユーザーから見ればANRと区別のつかない現象が起きる。ANRダイアログが出ないぶん、むしろ発見が遅れて厄介とも言える。 補足: 「RNならANRが原理的に起きない」という理解は不正確。JSスレッドが完全にブロックされている間、そのJSスレッドに紐づくタッチイベント処理やRedux的な状態更新も止まるため、ユーザー体験としては同種の「固まり」が発生する。OS側の検知メカニズムが働かないだけで、問題自体は消えていない。 重い処理の逃がし方:InteractionManager(旧API)とWorkletsは別物 RN側で「重い処理をUI/JSスレッドから逃がす」ための手段として、従来は InteractionManager がよく使われていたが、2026年8月リリースのReact Native 0.87で InteractionManager は削除済みであり、公式は代替として requestIdleCallback を案内している。0.82〜0.86の時点では非推奨警告が出るようになっていたため、既存プロジェクトでこの警告を見た人も多いはずだ。以降は「過去にどう動いていたか」の説明として読んでほしい。 ...

2026年8月16日 · 2 min

PostgreSQLのpg_trgmで中間一致検索を高速化する仕組みを学ぶ

参考 この記事は、以下の記事を読んで疑問に思ったことを調べた学習記録である。 Zennの検索スピードを5倍に高速化した話 記事では、Zennのサイト内検索をpg_trgm拡張を使って平均6倍、95パーセンタイルで4.25倍高速化した事例が紹介されている。 なぜ中間一致検索は遅いのか 通常、PostgreSQLでLIKE '%keyword%'のような中間一致検索を実行すると、BTreeインデックスが使えずフルスキャンが発生する。BTreeインデックスは文字列の前方一致には有効だが、中間一致では活用できない構造になっているためである。 データ量が増えると、このフルスキャンが深刻なパフォーマンスボトルネックになる。参考記事では、検索に1秒〜数秒かかる状態だったとのことだ。 n-gramインデックスの仕組み n-gramインデックスは、文字列をn文字ずつに分割してインデックス化することで、中間一致検索でもインデックスを効かせる仕組みである。 3-gramの例 「PostgreSQL」という文字列を3-gram(トライグラム)で分割すると以下のようになる。 __P, _Po, Pos, ost, stg, tgr, gre, reS, eSQL, QL_, L__ 先頭と末尾にはパディング文字(_)が付与される。 検索時の動作 「stgre」というキーワードで検索する場合: 検索キーワードを3-gramで分割: stg, tgr, gre インデックスからこれらすべてのトライグラムを含む文書を抽出 抽出された候補に対してRecheck処理を実行 重要なのは「いずれか」ではなく「すべて」のトライグラムが存在する文書が候補になる点である。もし「いずれか」だと、無関係な文書が大量に候補に含まれてしまう。 Recheck処理が必要な理由 n-gramインデックスでは、インデックスレベルでの検索後に必ずRecheck処理が必要になる。 具体例 以下のような状況を考える。 本文: 「小学校校長」 クエリ: 「小学校長」 3-gramで分割すると: 「小学校校長」→ 小学校, 学校校, 校校長 「小学校長」→ 小学校, 学校長 「小学校」が共通しているため、n-gramレベルでは「小学校校長」が候補として抽出される。しかし実際には「小学校長」という文字列は含まれていない。 このようなfalse positive(誤検出)を除外するため、インデックスで絞り込んだ候補に対して、実際に検索キーワードが含まれているかを厳密にチェックする必要がある。これがRecheck処理である。 pg_trgmとpg_bigmの選択 PostgreSQLには2つの主要なn-gram拡張がある。 pg_trgm: 3-gram方式、PostgreSQL本体にcontribとして付属 pg_bigm: 2-gram方式、サードパーティ製(NECが開発) 比較表 機能 pg_trgm pg_bigm エコシステム PostgreSQLコミュニティ サードパーティ ILIKE対応 ○ × 2文字以下の検索 × ○ Recheck無効化 × ○ インデックスサイズ 小 大(約2倍) なぜpg_trgmが選ばれたか 参考記事では、以下の理由でpg_trgmのみを採用している。 ...

2025年12月21日 · 1 min