Perplexity Deep Researchを無料で使ってみた
以前からOpenAIのChatGPTやGoogle GeminiのDeep Researchに興味はあったものの、利用料金が高い(ChatGPT Deep Researchは月額3万円以上)ため躊躇していました。しかし、昨日PerplexityでもDeep Researchが利用可能となり、1日5回まで無料で使えるとのことで、早速試してみました。
出力結果
以下は、現在調査中のリユース業界やRFIDに関する調査結果です。
リユース業界向け販売管理システム展開の可能性と戦略的妥当性に関する調査
https://www.perplexity.ai/page/riyusuye-jie-xiang-kefan-mai-g-EpGi3Vw0QMaVtfEuBXhvcwRFIDシステム導入によるリユース業界向け販売管理システムの最適化戦略
https://www.perplexity.ai/page/rfidsisutemudao-ru-niyoruriyus-Twwn3smhQQ.4T9kdc6ULZARFID技術を活用した棚卸し工数削減の実践事例と効果分析
https://www.perplexity.ai/page/rfidji-shu-wohuo-yong-sitapeng-YTVfVbQOR7e4SSBZ4cYN5g
所感
圧倒的な生産性を実感しました。私であれば、このレベルのアウトプットを作成するのに数日かかるでしょうし、これほど多角的な視点を持つレポートを自力で作成するのは難しいと感じます。また、関連質問も提案してくれるため、見落としがちな確認事項もカバーできる点が魅力的です。プロンプトエンジニアリングのスキルが低くても十分に活用できるサービスだと思います。
今後、このサービスとどう向き合っていくべきかを模索中ですが、まずはとにかく多用してみる予定です。ChatGPT Plusでも利用可能になることを期待しており、OpenAIからもその計画があるとの話を聞いているので、近いうちに実現するだろうと感じています。非常に使いやすいため、すぐに利用制限に達しそうですが、必要に応じてGoogle Geminiの有料版にも加入するかもしれません。他社も参入することで、最終的には費用が下がり、ひとつのサービス(おそらくChatGPT Plus)で十分になると考えています。とにかく非常に楽しみです!
◾️ 参考サイト
- Introducing Perplexity Deep Research
- 話題の検索AIエージェント!OpenAI Deep Researchについて解説してみた
- ChatGPT「deep research」 vs. Gemini「Deep Research」──どのくらい違う? 比較検証してみた
- OpenAIのアルトマンCEO、「deep research」を無料版ChatGPTでも提供すると予告
Difyで始めるPDF要約×ポッドキャスト作成
はじめに
最近、生成AIまわりをキャッチアップしたくてYouTubeを見ていたところ、「Dify」を使ってPDFを要約し、その内容をホストとゲストが雑談する形式のポッドキャスト音声にしてくれる機能があると知りました。参考にしたのは以下の動画です。
やってみたこと
動画の手順をなるべく再現しようとしたのですが、一部うまく動かなかったため、少しやり方を変更しました。具体的には、要約用のモデル変更や音声生成用のAPIキーの設定をしました。
最終的な手順はこんな感じです。
- Difyに新規登録
- テンプレートから「NotebookLM by Dify.AI」を選択
- 要約用のモデルをgpt-4o-miniに変更
- 音声生成用にOpenAIのAPIキーを取得
- 「Podcast audio generator」でOpenAIを選択
- 実行ボタンをクリックして音声ファイルを生成

PDFをアップロードしてみる
最初は「日本の1日」という厚生労働省の資料(参考PDF)をアップロードしました。
結果として、7分程度のカジュアルなトーンで会話しているポッドキャストが完成。APIのコストは一回あたり0.03ドル(約5円)程度で、気軽に試せる範囲でした。

2回目は東証のレポート(グロース市場における今後の対応)を使ってみました。Difyのワークフロー自体は同じですが、参考動画のとおりに設定すると途中でエラーが出たため、改めてテンプレートを選び直してから進めたところ問題なく動作しました。
3回目の実行では、入力ファイルを複数選択できるように変更し、上記の東証レポートと株式会社SHIFTの決算資料(参考PDF)をアップロードしました。私の期待としては、2つの資料をもとに「SHIFT社の戦略がうまく機能している」という内容が出てくることでしたが、結果はSHIFT社の資料だけに触れられていました。Difyの設定に問題があったのかもしれません。ただ、この失敗は音声生成前にテキストをチェックすれば気づけるため、フローを変更するときはコストの高い処理を最後に実行するなどして、無駄を削減しようと思いました。

使ってみた感想
- 会話形式なので楽しい
ホストとゲストの2人の会話形式なので楽しんで聞けます。単純な要約の文章だと退屈するので - 複数のAI連携が簡単
PDF要約から音声生成までワンストップで進められるので非常に便利です。 - RAGを使えばさらに面白い
東証のレポートと決算資料を組み合わせれば「この企業と東証の方針は合っているのか?」といった話ができるかもしれません。 - 要約チェックが大事
自分が言及してほしいポイントが抜けていることがあるので、要約を確認してからポッドキャスト音声にする流れが良さそうです。
『僕が若い人たちに伝えたい 2035年最強の働き方』の原稿をGoogleのNotebookLM Plusに入れて、音声要約する機能。
— ひろゆき (@hirox246) 2025年2月10日
二人のパーソナリティーが掛け合いをするラジオ風になってるけど、かなり自然。
これが、ボタン一発で出来る時代。。。https://t.co/SsdcrWDYkC
そうそう、これからもっと精度高くなるだろうから楽しみすぎです!
参考リンク
以上が、YouTubeの解説を参考にしつつも、一部の手順を変更してDifyでPDF要約×ポッドキャスト生成を試してみたレポートでした。
ISUCON12予選の復習をしました 10
この記事は何か?
ISUCON12予選の復習記録です。
第10回はISUCON12 予選の解説 (Node.jsでSQLiteのまま10万点行く方法)の「10 AddTenant APIでSQLite DBを作るのをやめる」を試します。
目次
1.「10 AddTenant APIでSQLite DBを作るのをやめる」を試す
3台構成準備のその2です。
1台構成であればAdminのAddTenant APIでSQLiteのDBを追加する、でよいのですが、上の方にも書いた通り3台構成にするとAdminのAddTenantをするサーバとテナントの処理サーバが一致していないとこの処理がうまくいきません。テナントのサブドメインはベンチマーカーから指定されるランダム文字列なので先頭文字を使って既存テナントと新規テナントを振り分けるみたいなこともできないためnginxが /api/admin/tenants/add を受け取ったときに適切なサーバに振り分けるのはなかなか難しい…ということで新規DB作成処理のタイミングを変えます。
まず、AddTenant APIでcreateTenantDB関数を呼び出すのをやめます。そして、テナントAPIに出てくるconnectToTenantDB関数の中でテナントのDBファイルが存在するかチェックして、なければcreateTenantDBを呼ぶという処理に変えると、nginxでテナントの振り分けさえ出来ていれば全てのSQLiteの処理は振り分けたサーバで行われるようになるので、3台構成が可能になります。
上記のとおりです。
私のISUCON歴では、3台全てを利用できることがなかったです。全台を利用するのは必須だと思うのでやっていきます。
- ソースコード変更
isucon@ip-192-168-0-12:~/webapp/go$ git diff
--- a/go/isuports.go
+++ b/go/isuports.go
@@ -85,6 +85,17 @@ func tenantDBPath(id int64) string {
// テナントDBに接続する
func connectToTenantDB(id int64) (*sqlx.DB, error) {
p := tenantDBPath(id)
+
+ // テナントDBが存在しない場合は新規に作成する
+ if _, err := os.Stat(p); err == nil {
+ } else if errors.Is(err, os.ErrNotExist) {
+ if err := createTenantDB(id); err != nil {
+ return nil, fmt.Errorf("error createTenantDB: id=%d %w", id, err)
+ }
+ } else {
+ return nil, fmt.Errorf("error file exist check")
+ }
+
db, err := sqlx.Open(sqliteDriverName, fmt.Sprintf("file:%s?mode=rw", p))
if err != nil {
return nil, fmt.Errorf("failed to open tenant DB: %w", err)
@@ -530,9 +541,9 @@ func tenantsAddHandler(c echo.Context) error {
// NOTE: 先にadminDBに書き込まれることでこのAPIの処理中に
// /api/admin/tenants/billingにアクセスされるとエラーになりそう
// ロックなどで対処したほうが良さそう
- if err := createTenantDB(id); err != nil {
- return fmt.Errorf("error createTenantDB: id=%d name=%s %w", id, name, err)
- }
+ //if err := createTenantDB(id); err != nil {
+ // return fmt.Errorf("error createTenantDB: id=%d name=%s %w", id, name, err)
+ //}
res := TenantsAddHandlerResult{
Tenant: TenantWithBilling{
- 変更前のスコア
22:42:51.168658 SCORE: 15122 (+16087 -965(6%))
- 変更後のスコア
10:29:17.392487 SCORE: 9872 (+11893 -2021(17%))
感想
エラーが多くなりました。何かが足りないと思いますが、とりあえず気にせず前に進みます。
参考
ISUCON12予選の復習をしました 9
この記事は何か?
ISUCON12予選の復習記録です。
第9回はISUCON12 予選の解説 (Node.jsでSQLiteのまま10万点行く方法)の「9 Ranking APIでランキング集計するのをやめる」を試します。
先に結論を書くと、この対応は「失敗」しました。途中までの経緯を書きます。
目次
1. リクエスト傾向確認
alpでみていくと、ranking APIの呼び出される回数とscoreが入稿される回数は10~20倍くらい差があります。
ということで、確認しました。その結果、解説に書かれているとおり、/rankingは/scoreの30倍くらい差があります。
- ALP抜粋
+-------+--------+--------+----------+--------+--------+--------+--------+--------+-----------+------------+--------------+-----------+--------+---------------------------------------+ | COUNT | MIN | MAX | SUM | AVG | P1 | P50 | P99 | STDDEV | MIN(BODY) | MAX(BODY) | SUM(BODY) | AVG(BODY) | METHOD | URI | +-------+--------+--------+----------+--------+--------+--------+--------+--------+-----------+------------+--------------+-----------+--------+---------------------------------------+ | 106 | 0.012 | 3.656 | 31.532 | 0.297 | 0.000 | 0.144 | 2.560 | 0.536 | 39.000 | 62.000 | 5656.000 | 53.358 | POST | /api/organizer/competition/.*/score | | 3320 | 0.012 | 9.100 | 2489.008 | 0.750 | 0.012 | 0.472 | 4.272 | 0.887 | 0.000 | 17281.000 | 47525692.000 | 14314.967 | GET | /api/player/competition/.*/ranking | +-------+--------+--------+----------+--------+--------+--------+--------+--------+-----------+------------+--------------+-----------+--------+---------------------------------------+
- ALP全体
isucon@ip-192-168-0-12:~/webapp$ alp -f /var/log/nginx/access.log --cnt --aggregates='/api/player/competition/.*/ranking','/api/organizer/competition/.*/score','/api/player/player/.*','/api/organizer/competition/.*/finish','/api/organizer/player/.*/disqualified' +-------+--------+--------+----------+--------+--------+--------+--------+--------+-----------+------------+--------------+-----------+--------+---------------------------------------+ | COUNT | MIN | MAX | SUM | AVG | P1 | P50 | P99 | STDDEV | MIN(BODY) | MAX(BODY) | SUM(BODY) | AVG(BODY) | METHOD | URI | +-------+--------+--------+----------+--------+--------+--------+--------+--------+-----------+------------+--------------+-----------+--------+---------------------------------------+ | 1 | 15.724 | 15.724 | 15.724 | 15.724 | 15.724 | 15.724 | 15.724 | 0.000 | 55.000 | 55.000 | 55.000 | 55.000 | POST | /initialize | | 1 | 0.000 | 0.000 | 0.000 | 0.000 | 0.000 | 0.000 | 0.000 | 0.000 | 205.000 | 205.000 | 205.000 | 205.000 | GET | /api/organizer/competitions | | 1 | 0.000 | 0.000 | 0.000 | 0.000 | 0.000 | 0.000 | 0.000 | 0.000 | 479.000 | 479.000 | 479.000 | 479.000 | GET | /index.html | | 1 | 0.000 | 0.000 | 0.000 | 0.000 | 0.000 | 0.000 | 0.000 | 0.000 | 33294.000 | 33294.000 | 33294.000 | 33294.000 | GET | /js/app.3a4ec98c.js | | 1 | 0.000 | 0.000 | 0.000 | 0.000 | 0.000 | 0.000 | 0.000 | 0.000 | 4868.000 | 4868.000 | 4868.000 | 4868.000 | GET | /css/app.83b4c321.css | | 1 | 0.000 | 0.000 | 0.000 | 0.000 | 0.000 | 0.000 | 0.000 | 0.000 | 162.000 | 162.000 | 162.000 | 162.000 | GET | /favicon.ico | | 11 | 0.004 | 0.520 | 1.088 | 0.099 | 0.004 | 0.016 | 0.228 | 0.153 | 39.000 | 185.000 | 1664.000 | 151.273 | POST | /api/organizer/player/.*/disqualified | | 15 | 0.928 | 11.904 | 53.232 | 3.549 | 0.928 | 2.184 | 10.264 | 3.159 | 14950.000 | 33660.000 | 393099.000 | 26206.600 | POST | /api/organizer/players/add | | 16 | 0.004 | 0.168 | 0.896 | 0.056 | 0.000 | 0.052 | 0.136 | 0.047 | 39.000 | 200.000 | 2263.000 | 141.438 | POST | /api/admin/tenants/add | | 70 | 0.004 | 30.000 | 106.576 | 1.523 | 0.004 | 0.408 | 9.060 | 3.805 | 0.000 | 5203.000 | 119388.000 | 1705.543 | GET | /api/organizer/billing | | 80 | 0.004 | 23.352 | 88.356 | 1.104 | 0.000 | 0.112 | 15.248 | 3.529 | 0.000 | 1614.000 | 110481.000 | 1381.013 | GET | /api/admin/tenants/billing | | 84 | 0.004 | 1.496 | 12.664 | 0.151 | 0.000 | 0.072 | 1.248 | 0.245 | 0.000 | 602067.000 | 3698133.000 | 44025.393 | GET | /api/organizer/players | | 85 | 0.004 | 3.244 | 39.848 | 0.469 | 0.004 | 0.228 | 2.576 | 0.608 | 0.000 | 39.000 | 1761.000 | 20.718 | POST | /api/organizer/competition/.*/finish | | 92 | 0.008 | 1.100 | 13.672 | 0.149 | 0.008 | 0.072 | 1.092 | 0.199 | 39.000 | 223.000 | 17613.000 | 191.446 | POST | /api/organizer/competitions/add | | 106 | 0.012 | 3.656 | 31.532 | 0.297 | 0.000 | 0.144 | 2.560 | 0.536 | 39.000 | 62.000 | 5656.000 | 53.358 | POST | /api/organizer/competition/.*/score | | 502 | 0.004 | 8.236 | 96.832 | 0.193 | 0.000 | 0.108 | 1.248 | 0.461 | 0.000 | 2831.000 | 519832.000 | 1035.522 | GET | /api/player/competitions | | 3320 | 0.012 | 9.100 | 2489.008 | 0.750 | 0.012 | 0.472 | 4.272 | 0.887 | 0.000 | 17281.000 | 47525692.000 | 14314.967 | GET | /api/player/competition/.*/ranking | | 6936 | 0.004 | 30.004 | 2716.212 | 0.392 | 0.004 | 0.196 | 2.644 | 1.042 | 0.000 | 1457.000 | 6777835.000 | 977.197 | GET | /api/player/player/.* | +-------+--------+--------+----------+--------+--------+--------+--------+--------+-----------+------------+--------------+-----------+--------+---------------------------------------+
2. アップロードされるCSVファイル確認
ベンチマークは同じ大会にscoreを何度か入稿してきますが、scoreの件数は減らない(追記式のスコアデータがCSVとして何度も入稿される)というベンチマーカーの仕様があってそれを知った上であれば ON DUPLICATE KEY UPDATE を使って1クエリでいけるのでMySQLを使いました。
ということで、確認しました。その結果、解説に書かれているとおり、socreの件数は変更がありませんでした。
- CSVを出力するコードを一時的に追加
isucon@ip-192-168-0-12:~/webapp/go$ git diff
diff --git a/go/isuports.go b/go/isuports.go
index c943073..3e16807 100644
--- a/go/isuports.go
+++ b/go/isuports.go
@@ -1103,6 +1103,22 @@ func competitionScoreHandler(c echo.Context) error {
}
defer f.Close()
+ // アップロードされるファイルの傾向を調べるために一時的にファイルに保存
+ currentTime := time.Now()
+ outFilePath := fmt.Sprintf("/tmp/%v-%v-%v.csv", v.tenantID, competitionID, currentTime.UnixNano())
+ outFile, err := os.Create(outFilePath)
+ if err != nil {
+ return err
+ }
+ io.Copy(outFile, f)
+ defer outFile.Close()
+
+ f, err = fh.Open()
+ if err != nil {
+ return fmt.Errorf("error fh.Open FormFile(scores): %w", err)
+ }
+ defer f.Close()
+
r := csv.NewReader(f)
headers, err := r.Read()
if err != nil {
- 出力されたCSV一覧
ubuntu@ip-192-168-0-12:/tmp$ ls | sort 1-4068d268-26b4-11ed-a74f-0ef728f3fa93-1661679113511136695.csv 1-5dc158aa-26b4-11ed-a74f-0ef728f3fa93-1661679164143958364.csv 16-3fbc64fd-26b4-11ed-a74f-0ef728f3fa93-1661679112403862815.csv 16-4375511b-26b4-11ed-a74f-0ef728f3fa93-1661679118652853644.csv ... 99-45611dc0-26b4-11ed-a74f-0ef728f3fa93-1661679121847557828.csv 99-45611dc0-26b4-11ed-a74f-0ef728f3fa93-1661679122419324934.csv
- 出力されたCSV内の行数
ubuntu@ip-192-168-0-12:/tmp$ cat 99-45611dc0-26b4-11ed-a74f-0ef728f3fa93-1661679121847557828.csv | wc
76 77 1155
ubuntu@ip-192-168-0-12:/tmp$ cat 99-45611dc0-26b4-11ed-a74f-0ef728f3fa93-1661679122419324934.csv | wc
76 77 1080
- 出力されたCSV内の内容

3. rankingテーブルを利用するように変更
以下手順で変更しました。しかし、ベンチマークが失敗します。ランキングが正しくないとエラーが発生します。
- rankingテーブル追加
- /score時にranking追加
- /ranking時にrankingテーブルを参照するように変更
感想
この対応方法については別の方のブログを参照するなどしてリベンジしたいと思います。
参考
ISUCON12予選の復習をしました 8
この記事は何か?
ISUCON12予選の復習記録です。
第8回はISUCON12 予選の解説 (Node.jsでSQLiteのまま10万点行く方法)の「8 tenantDB player_scoreにINDEXをはる」を試します。
目次
1.「8 tenantDB player_scoreにINDEXをはる」を試す
ISUCON12予選は2つのデータソースがあります。AdminDBとTenantDBです。TenantDBのテーブル構造は以下です。
/home/isucon/webapp/sql/tenant/vi 10_schema.sql DROP TABLE IF EXISTS competition; DROP TABLE IF EXISTS player; DROP TABLE IF EXISTS player_score; CREATE TABLE competition ( id VARCHAR(255) NOT NULL PRIMARY KEY, tenant_id BIGINT NOT NULL, title TEXT NOT NULL, finished_at BIGINT NULL, created_at BIGINT NOT NULL, updated_at BIGINT NOT NULL ); CREATE TABLE player ( id VARCHAR(255) NOT NULL PRIMARY KEY, tenant_id BIGINT NOT NULL, display_name TEXT NOT NULL, is_disqualified BOOLEAN NOT NULL, created_at BIGINT NOT NULL, updated_at BIGINT NOT NULL ); CREATE TABLE player_score ( id VARCHAR(255) NOT NULL PRIMARY KEY, tenant_id BIGINT NOT NULL, player_id VARCHAR(255) NOT NULL, competition_id VARCHAR(255) NOT NULL, score BIGINT NOT NULL, row_num BIGINT NOT NULL, created_at BIGINT NOT NULL, updated_at BIGINT NOT NULL );
上記のとおりインデックスがありません。最も大きなテナントの件数を確認します。
/home/isucon/webapp/tenant_db/1.db sqlite> select count(*) from competition; 22 sqlite> select count(*) from player; 5001 sqlite> select count(*) from player_score; 1675597
player_scoreにインデックスをはると効果がありそうです。TenantDBに対するSELECT文を全部見てみます。
// player_score, tenant_id, competition_id, player_id if err := tenantDB.SelectContext( ctx, &scoredPlayerIDs, "SELECT DISTINCT(player_id) FROM player_score WHERE tenant_id = ? AND competition_id = ?", // competition, PK if err := tenantDB.SelectContext( ctx, &cs, "SELECT * FROM competition WHERE tenant_id=?", // player, tenant_id, created_at if err := tenantDB.SelectContext( ctx, &pls, "SELECT * FROM player WHERE tenant_id=? ORDER BY created_at DESC", // player, tenant_id if err := tenantDB.SelectContext( ctx, &playerIDs, "SELECT DISTINCT(id) FROM player WHERE tenant_id = ?", // competition, tenant_id, created_at if err := tenantDB.SelectContext( ctx, &cs, "SELECT * FROM competition WHERE tenant_id=? ORDER BY created_at DESC", // competition, tenant_id, created_at if err := tenantDB.SelectContext( ctx, &cs, "SELECT * FROM competition WHERE tenant_id = ? ORDER BY created_at ASC", // ..? (JOINの時の適切なインデックスとは?) if err := tenantDB.SelectContext( ctx, &pss, "SELECT player_score.*, player.display_name FROM player_score JOIN player ON player.id = player.score_id WHERE player_score.tenant_id = ? AND competition_id = ? ORDER BY row_num DESC", // competition, tenant_id, created_at if err := tenantDB.SelectContext( ctx, &cs, "SELECT * FROM competition WHERE tenant_id=? ORDER BY created_at DESC", // competition, created_at if err := tenantDB.SelectContext( ctx, &cs, "SELECT * FROM competition ORDER BY created_at DESC",
上記のとおり、解説にあるとおり以下のインデックスが有効そうです。ただし「player_id」を含める発想はありませんでした。
CREATE INDEX idx_score ON player_score (tenant_id, competition_id, player_id);
また、初期データに対してもインデックスを作成します。
cd /home/isucon/webapp/tenant_db for db in *.db; do echo "CREATE INDEX idx_score ON player_score (tenant_id, competition_id, player_id);" | sqlite3 $db; done
- ソースコードの変更点
$ git diff @@ -30,3 +30,5 @@ CREATE TABLE player_score ( created_at BIGINT NOT NULL, updated_at BIGINT NOT NULL ); + +CREATE INDEX idx_score ON player_score (tenant_id, competition_id, player_id);
- ソースコード変更前のスコア
09:21:44.540379 SCORE: 9868 (+11747 -1879(16%))
- ソースコード変更後のスコア
22:42:51.168658 SCORE: 15122 (+16087 -965(6%))
解説にあるとおりスコアが1.5倍になりました。
感想
必要なインデックスの洗い出しや、for db in *.db.. のコマンドを時間かけずにできるようにする必要がありそうです。
予選突破スコアは22,000点あたりなので、近づいてきました。
参考
ISUCON12予選の復習をしました 7
この記事は何か?
ISUCON12予選の復習記録です。
第7回はISUCON12 予選の解説 (Node.jsでSQLiteのまま10万点行く方法)の「7 Finish APIでBillingReportを生成する」を試します。
目次
1.「7 Finish APIでBillingReportを生成する」を試す
1.「7 Finish APIでBillingReportを生成する」を試す
今回の対応は難しいです。これができた人は尊敬します。
反映までの猶予時間について 一部APIは他のリクエストへの反映までに許容される猶予時間があります POST /api/organizer/competition/:competition_id/finish 以下のエンドポイントは上記APIの情報の反映まで、レスポンスを返してから3秒の猶予が許容されます GET /api/organizer/billing GET /api/admin/tenants/billing
上記は「ISUCON12 予選当日マニュアル」に記載されています。ヒントですがこれが何を意味しているか分かりませんでした。過去問でも同じような「許容される猶予時間」についての記載があるので、慣れる必要があります。(ISUCON文学に慣れる必要がある)
この意味は「/finishエンドポイントの完了後、3秒以内には、結果反映を検証するベンチ処理は発生しない」という意味だと今は理解しています。正解は今後ベンチマークをデバッグする時に答え合わせします。
エンドポイント「テナントごとの課金レポート /api/admin/tenants/billing」は、テナント毎の課金レポートを出力しますが、全テナントの全大会のデータを集計する必要があり、大変重い処理です。「この集計処理を/billingではなく、/finishエンドポイントで事前に実行しておく」というのが今回の対応になります。
この発想に気づくことも僕には難しいですが、実装するのも注意が必要です。以下プロセスで行います。
1. テーブル作成 2. /initializeエンドポイントで初期データ分の集計処理を追加 3. /finishエンドポイントにデータ登録処理を追加 4. /billingエンドポイントにデータ参照処理を追加 5. /initializeエンドポイントで実行されるデータ初期化処理に追加 (TRUNCATE追加)
- テーブル作成
CREATE TABLE `billing_report` ( `tenant_id` BIGINT UNSIGNED NOT NULL, `competition_id` VARCHAR(255) NOT NULL, `competition_title` VARCHAR(255) NOT NULL, `player_count` BIGINT NOT NULL, `visitor_count` BIGINT NOT NULL, `billing_player_yen` BIGINT NOT NULL, `billing_visitor_yen` BIGINT NOT NULL, `billing_yen` BIGINT NOT NULL, PRIMARY KEY(`tenant_id`, `competition_id`) ) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8mb4;
- ソースコード変更部分
diff --git a/go/isuports.go b/go/isuports.go
index dc182fb..c943073 100644
--- a/go/isuports.go
+++ b/go/isuports.go
@@ -449,6 +449,17 @@ type PlayerScoreJoinPlayerRow struct {
DisplayName string `db:"display_name"`
}
+type BillingReportRow struct {
+ TenantID int64 `db:"tenant_id"`
+ CompetitionID string `db:"competition_id"`
+ CompetitionTitle string `db:"competition_title"`
+ PlayerCount int64 `db:"player_count"`
+ VisitorCount int64 `db:"visitor_count"`
+ BillingPlayerYen int64 `db:"billing_player_yen"`
+ BillingVisitorYen int64 `db:"billing_visitor_yen"`
+ BillingYen int64 `db:"billing_yen"`
+}
+
// 排他ロックのためのファイル名を生成する
func lockFilePath(id int64) string {
tenantDBDir := getEnv("ISUCON_TENANT_DB_DIR", "../tenant_db")
@@ -717,11 +728,32 @@ func tenantsBillingHandler(c echo.Context) error {
return fmt.Errorf("failed to Select competition: %w", err)
}
for _, comp := range cs {
+ /*
report, err := billingReportByCompetition(ctx, tenantDB, t.ID, comp.ID)
if err != nil {
return fmt.Errorf("failed to billingReportByCompetition: %w", err)
}
tb.BillingYen += report.BillingYen
+ */
+
+ // 請求情報テーブルから取得
+ br := BillingReportRow{}
+ if err := adminDB.GetContext(
+ ctx,
+ &br,
+ // 最後にCSVに登場したスコアを採用する = row_numが一番大きいもの
+ "SELECT * FROM billing_report WHERE tenant_id = ? AND competition_id = ?",
+ t.ID,
+ comp.ID,
+ ); err != nil {
+ // 行がない = スコアが記録されてない
+ if errors.Is(err, sql.ErrNoRows) {
+ continue
+ }
+ return fmt.Errorf("failed to billingReportByCompetition: %w", err)
+ }
+
+ tb.BillingYen += br.BillingYen
}
tenantBillings = append(tenantBillings, tb)
return nil
@@ -1008,6 +1040,13 @@ func competitionFinishHandler(c echo.Context) error {
now, now, id, err,
)
}
+
+ // 請求情報保存 TODO 非同期処理
+ err = insertBillingReport(ctx, tenantDB, v.tenantID, id)
+ if err != nil {
+ return fmt.Errorf("error insertBillingReport: %w", err)
+ }
+
return c.JSON(http.StatusOK, SuccessResult{Status: true})
}
@@ -1690,5 +1729,59 @@ func initializeHandler(c echo.Context) error {
res := InitializeHandlerResult{
Lang: "go",
}
+
+ // 請求情報保存
+ ctx := context.Background()
+
+ ts := []TenantRow{}
+ if err := adminDB.SelectContext(ctx, &ts, "SELECT * FROM tenant ORDER BY id DESC"); err != nil {
+ return fmt.Errorf("error Select tenant: %w", err)
+ }
+
+ for _, t := range ts {
+ tenantDB, err := connectToTenantDB(t.ID)
+ if err != nil {
+ return fmt.Errorf("failed to connectToTenantDB: %w", err)
+ }
+ defer tenantDB.Close()
+ cs := []CompetitionRow{}
+ if err := tenantDB.SelectContext(
+ ctx,
+ &cs,
+ "SELECT * FROM competition ORDER BY created_at DESC",
+ ); err != nil {
+ return fmt.Errorf("error Select competition: %w", err)
+ }
+
+ for _, comp := range cs {
+ err := insertBillingReport(ctx, tenantDB, comp.TenantID, comp.ID)
+ if err != nil {
+ return fmt.Errorf("error insertBillingReport: %w", err)
+ }
+ }
+ }
+
return c.JSON(http.StatusOK, SuccessResult{Status: true, Data: res})
}
+
+// 請求情報保存
+func insertBillingReport(ctx context.Context, tenantDB dbOrTx, tenantID int64, competitonID string) error {
+ report, err := billingReportByCompetition(ctx, tenantDB, tenantID, competitonID)
+ if err != nil {
+ return fmt.Errorf("failed to billingReportByCompetition: %w", err)
+ }
+
+ if _, err := adminDB.ExecContext(
+ ctx,
+ "INSERT INTO billing_report (tenant_id, competition_id, competition_title, player_count, visitor_count, billing_player_yen, billing_visitor_yen, billing_yen) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
+ tenantID, report.CompetitionID, report.CompetitionTitle, report.PlayerCount, report.VisitorCount, report.BillingPlayerYen, report.BillingVisitorYen, report.BillingYen,
+ ); err != nil {
+ return fmt.Errorf(
+ "error Insert billing_report: %w",
+ err,
+ )
+ }
+
+ return nil
+}
+
diff --git a/sql/init.sql b/sql/init.sql
index 990be9a..b4ff9e2 100644
--- a/sql/init.sql
+++ b/sql/init.sql
@@ -2,3 +2,4 @@ DELETE FROM tenant WHERE id > 100;
DELETE FROM visit_history WHERE created_at >= '1654041600';
UPDATE id_generator SET id=2678400000 WHERE stub='a';
ALTER TABLE id_generator AUTO_INCREMENT=2678400000;
+TRUNCATE table billing_report;
- ソースコード変更前のスコア
01:57:29.017960 SCORE: 5617 (+5617 0(0%))
- ソースコード変更後のスコア
09:21:44.540379 SCORE: 9868 (+11747 -1879(16%))
感想
スコアが大幅に増えましたが、失敗によるマイナスが発生しています。上記のコード修正では足りない何かがあるからだと思います。詳細は今後別の人の解説を見て解決します。
また、「請求情報保存 TODO 非同期処理」の部分は解説にあるとおり今後goroutineを利用してみます。
予選当日マニュアルに書かれている「許容される猶予時間」の対応方法については慣れておきたいです。また、今回テーブルを増やしましたが、テーブル増やすことも毎回あるのでそういう発想を忘れないようにしたいです。
今回SQLiteへの検索の一部をMySQLに移しましたが、これは3台構成に変更するための布石となっています。このあたりは次回以降記載します。
参考
ISUCON12予選の復習をしました 6
この記事は何か?
ISUCON12予選の復習記録です。
第6回はISUCON12 予選の解説 (Node.jsでSQLiteのまま10万点行く方法)の「6 adminDB visit_historyの初期データをコンパクトにする」を試します。
目次
- 「6 adminDB visit_historyの初期データをコンパクトにする」を試す
MySQLに入っているvisit_historyデータは100テナントで322万行の巨大データですが、「アクセスをしたかどうか」だけがわかれば大丈夫というアプリケーションの作りになっているため、全件を保持しておく必要はありません。
ISUCON12予選のデータソースは2つあります。AdminDBとTenantDBです。AdminDBは以下のとおり3つのテーブルで構成されており、visit_historyテーブルに300万件のデータがあるためこのテーブルを対策する必要があることが分かります。
- AdminDB初期データ件数
mysql> select count(*) from id_generator; +----------+ | count(*) | +----------+ | 1 | +----------+ 1 row in set (0.00 sec) mysql> select count(*) from tenant; +----------+ | count(*) | +----------+ | 106 | +----------+ 1 row in set (0.00 sec) mysql> select count(*) from visit_history; +----------+ | count(*) | +----------+ | 3225470 | +----------+ 1 row in set (1.33 sec)
以下のとおり、visit_historyへの参照は1つの関数から呼び出されています。SQLを確認すると「MIN(created_at)」とあり、プレイヤーのテナントID&大会IDに対して最も古い参照履歴レコードだけを利用していることがわかります。
- visit_history
■ 参照
□ 参照エンドポイント
// テナント管理者向けAPI
// GET /api/organizer/billing
// テナント内の課金レポートを取得する
func billingHandler(c echo.Context) error {
// SaaS管理者用API
// テナントごとの課金レポートを最大10件、テナントのid降順で取得する
// GET /api/admin/tenants/billing
// URL引数beforeを指定した場合、指定した値よりもidが小さいテナントの課金レポートを取得する
func tenantsBillingHandler(c echo.Context) error {
□ 参照エンドポイントから呼ばれるSQL
// 大会ごとの課金レポートを計算する
func billingReportByCompetition(ctx context.Context, tenantDB dbOrTx, tenantID int64, competitonID string) (*BillingReport, error) {
"SELECT player_id, MIN(created_at) AS min_created_at FROM visit_history WHERE tenant_id = ? AND competition_id = ? GROUP BY player_id",
■ 更新
□ 更新エンドポイント
// 参加者向けAPI
// GET /api/player/competition/:competition_id/ranking
// 大会ごとのランキングを取得する
func competitionRankingHandler(c echo.Context) error {
□ 更新エンドポイントから呼ばれるSQL
"INSERT INTO visit_history (player_id, tenant_id, competition_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?)",
- 不要なデータを削除する
解説のとおり不要データを削除します。結果、300万件から20万件に削減できました。
mysql> CREATE TABLE `visit_history_tmp` (
-> `player_id` VARCHAR(255) NOT NULL,
-> `tenant_id` BIGINT UNSIGNED NOT NULL,
-> `competition_id` VARCHAR(255) NOT NULL,
-> `created_at` BIGINT NOT NULL,
-> `updated_at` BIGINT NOT NULL,
-> INDEX `tenant_id_idx` (`tenant_id`)
-> ) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8mb4;
Query OK, 0 rows affected (0.04 sec)
mysql> insert into visit_history_tmp select * from visit_history;
Query OK, 3226340 rows affected (24.40 sec)
Records: 3226340 Duplicates: 0 Warnings: 0
mysql> insert into visit_history select player_id, tenant_id, competition_id, min(created_at) as created_at, min(updated_at) as updated_at from visit_history_tmp group by player_id, tenant_id, competition_id;
Query OK, 201131 rows affected (27.66 sec)
Records: 201131 Duplicates: 0 Warnings: 0
mysql> select count(*) from visit_history;
+----------+
| count(*) |
+----------+
| 201131 |
+----------+
1 row in set (0.02 sec)
- 変更前のスコア
22:36:19.017277 SCORE: 6390 (+6390 0(0%))
- 変更後のスコア
01:57:29.017960 SCORE: 5617 (+5617 0(0%))
- 変更後のtopコマンド結果

DBのCPU負荷が少なくなっていることが分かります。
感想
不要な初期データの削減はISUCONでよくみる問題だと思います。このあたりもコードをささっと理解できる必要がありそうです。