はじめに
Qiitaに記事を書いている人なら、一度はこう思ったことがないでしょうか。「自分の記事、どうしたら伸びるんだろう」と。
私はずっと、これを"なんとなくの肌感"で語るのがイヤでした。「画像は多めがいい」「タイトルは長すぎない方が」——どれも、誰かの体験談の又聞きです。だったら、Qiitaの記事を片っ端から集めて、データで殴ればいいじゃないか。そう思って手を動かしたのがこの記事です。
ついでに、ちょうど気になっていた ClickHouse の Cloud に載った"AIエージェント機能" も試したかった。集めたデータを、人間(私)とAIエージェントの両方に分析させて、勝負させてみます。
この記事でやることは3つです。
- Qiita API で記事データを大量に集める(結果:76,764件 / 直近半年分)
- ClickHouse Cloud に放り込んで、素のSQLで爆速集計する
- その同じデータを、人間に何も教えていない"白紙のAIエージェント"(リモートMCP × Claude)に分析させ、私の分析と突き合わせる
先に結論
- 効くのは「画像の多さ」と「タイトルの付け方」。コードの量は、ほぼ関係なかった。
- そして一番おもしろかったのは、私がSQLでハマった落とし穴を、AIが最初の数分で回避していたこと。
対象読者:Qiitaに記事を書く全エンジニア/データ分析・AI活用に興味がある人
なぜ ClickHouse なのか(手短に)
ClickHouse は列指向で集計がとにかく速いDBです。最近は AnthropicやOpenAIが自社のオブザーバビリティ基盤に採用していることもあって、「AIスタックの土台」としても名前を聞くようになりました。今回これを選んだ理由は2つだけ。数十万件の集計を一瞬で回したかったことと、2026年のCloudに載った Ask AI / リモートMCPサーバー(β)を触ってみたかったことです。
ClickHouse Cloud のAIエージェント機能は2026年6月時点でβ。内容は変わる可能性があります。最新は公式をご確認ください。
Qiita API からデータを集める
最初の関門はデータ収集でした。Qiita API には地味にキツい制約が2つあります。
- 認証ありでも 1,000リクエスト/時 のレート制限
- 1つのクエリ条件で取れるのは 最大1万件(
per_page100 ×page100)
半年分の記事は余裕で1万件を超えるので、created(投稿日)の範囲で期間を区切って取りにいきます。さらに、区切った窓がそれでも1万件を超えたら、その窓を二分割して再帰的に細かくする、という作戦にしました。
期間分割+二分割再帰の中核(collector/collect.py)
def collect_window(session, win_start, win_end, args, ckpt, stats, out_fh):
"""窓 [win_start, win_end) を収集。1 万件超なら二分割して再帰する。"""
key = f"{win_start.isoformat()}_{win_end.isoformat()}"
if key in ckpt["done"]:
log(f"窓 {key}: スキップ (チェックポイント済み)")
return
span_days = (win_end - win_start).days
query = build_query(win_start, win_end, args.query_extra)
_, total = api_get(session, {"query": query, "page": 1, "per_page": 1}, args, stats)
if total > WINDOW_CAP and span_days > 1:
# 1 万件超 → 二分割して再帰 (過去データは決定的なので再開時も同じ分割になる)
mid = win_start + timedelta(days=max(1, span_days // 2))
collect_window(session, win_start, mid, args, ckpt, stats, out_fh)
collect_window(session, mid, win_end, args, ckpt, stats, out_fh)
else:
fetched = fetch_pages(session, query, total, args, stats, out_fh)
stats["items"] += fetched
mark_done(ckpt, key, args.checkpoint) # 窓ごとに完了を記録 → 中断しても再開できる
レート制限は素直に 1リクエストあたり3.6秒スリープ(3600秒 ÷ 1000)で守りました。Total-Count ヘッダで各窓の件数を先に見て、半開区間 created:>=A created:<B で隣の窓と被らないようにしています。
ここでハマったポイントを正直に。
収集は数時間かかるのですが、途中で何度かPCがスリープして収集が止まりました。半分諦めかけたんですが、窓ごとに完了をチェックポイント保存していたおかげで、再起動して流し直すだけで全件回収できました。「過去データは決定的だから、再開しても同じ分割になる」——この設計に救われました。
ちなみに page_views_count(PV)は他人の記事だと取れないので、今回は使っていません。
集めた body(本文)はそのまま保存せず、体裁の特徴量に変換して持ちました。後で「画像の数」「コードブロックの数」で伸びを分析するためです。
def extract_features(title, body):
return {
"title_length": len(title),
"body_length": len(body),
"code_block_count": body.count("```") // 2, # ``` のペア数
"heading_count": len(_HEADING_RE.findall(body)), # 行頭の # 見出し
"image_count": len(_IMAGE_RE.findall(body)), # 
"link_count": len(_LINK_RE.findall(body)), # [text](url)
}
最終的に集まったのは 76,764件(2025-12-01〜2026-06-07)でした。
ClickHouse Cloud に投入する
ここからが本番。ClickHouse Cloud の無料トライアルに登録して、テーブルを作ります。
スキーマはこんな感じ。さっき特徴量化したカラムが並んでいます。
CREATE TABLE qiita_items (
id String, title String,
created_at DateTime('Asia/Tokyo'), updated_at DateTime('Asia/Tokyo'),
likes_count UInt32, stocks_count UInt32, comments_count UInt32,
tags Array(String),
user_id String, user_followers UInt32, user_items_count UInt32, url String,
title_length UInt16, body_length UInt32, code_block_count UInt16,
heading_count UInt16, image_count UInt16, link_count UInt16
) ENGINE = MergeTree
ORDER BY (created_at, id);
ちなみに余談ですが、テーブルを作らなくても各種有名Serviceにコネクトできるので、
既にデータがある場合はコネクタ経由で持ってきたほうが楽だなと思いました。

話を戻してクリックハウスにデータをバッチで投入するスクリプトを書きましてテーブル作成します。
clickhouse-connect(Python)でバッチ投入したところ、76,764件の投入が19.3秒で終わりました。
まず素のSQLで分析してみる(ここで一回ハマる)
データが入ったので、まずは自分の手でSQLを書いて分析します。
最初、私はタグ別に いいねの中央値 を出しました。「タグごとにいいねの真ん中の値を見れば、付きやすいタグがわかるだろう」と。ところが——
ほとんどのタグで中央値が 0 か 1。差がまったく出ません。
原因はすぐ分かりました。いいね数は典型的な べき分布 だったんです。
| p50(中央値) | p90 | p95 | p99 | 最大 |
|---|---|---|---|---|
| 1 | 8 | 14 | 49 | 1,535 |
半分以上の記事はいいねがほぼ付かず、ごく一部が爆発的に伸びる。だから中央値で見ると、全部「真ん中=ほぼ0」に潰れてしまう。
そこで指標を変えました。中央値ではなく、「ヒット率 = いいねが10以上ついた記事の割合」で見ることにしたんです(全体で 8.2%)。これに切り替えた瞬間、像が一気に結びました。
ちなみに、いいねは投稿してから時間をかけて貯まります。直近の記事はまだ伸びきっていないので、分析の主軸は 投稿から3か月以上たった"成熟記事"(39,222件) に絞っています。
ヒット率で見ると、こうなりました。
画像の数 × ヒット率
| 画像数 | 0 | 1-2 | 3-5 | 6+ |
|---|---|---|---|---|
| ヒット率 | 6.5% | 8.9% | 11.0% | 12.6% |
フォロワー数 × ヒット率
| フォロワー | 0 | 1-9 | 10-49 | 50-199 | 200-999 | 1000+ |
|---|---|---|---|---|---|---|
| ヒット率 | 3.6% | 7.4% | 8.4% | 13.1% | 17.4% | 38.6% |
タイトル文字数 × ヒット率
| 文字数 | <15 | 15-24 | 25-34 | 35-44 | 45+ |
|---|---|---|---|---|---|
| ヒット率 | 4.7% | 8.3% | 8.7% | 8.5% | 8.2% |
画像は多いほど効く。フォロワーは桁違いに効く。タイトルは短すぎると損。だいぶ見えてきました。
そして肝心の速さですが、ここまでの 深掘り8クエリのサーバー処理時間は合計229.4ms。7.7万件に対する ARRAY JOIN tags のタグ集計ですら 143ms です。手元で何度も角度を変えて試せるので、分析のテンポが完全に変わりました。
ヒット率で見直したクエリの例(sql/analyze_deep.py より)
-- フォロワー帯ごとの「いいね10+」ヒット率
SELECT
multiIf(user_followers=0,'0', user_followers<10,'1-9', user_followers<50,'10-49',
user_followers<200,'50-199', user_followers<1000,'200-999','1000+') AS band,
count() AS n,
round(countIf(likes_count>=10)/count()*100, 1) AS hit_pct
FROM qiita_items
WHERE created_at < '2026-03-01'
GROUP BY band ORDER BY hit_pct;
ちなみに詳しい結果は下記に別記事としてまとめておきました。
本題:AIエージェントに分析を丸投げする
ここからが、この記事を書きたかった理由です。
ClickHouse Cloud のリモートMCPサーバーを Claude に接続すると、自然言語で質問するだけで、AIが自分でSQLを書いて実行し、答えを返してくれます。そこで実験をしました。
人間(私)の分析結果を一切見せていない"白紙のAIエージェント" を用意し、接続情報と6つの質問だけを渡して、丸投げしたんです。
投げた質問はこの6つ。
| # | 質問 |
|---|---|
| 1 | 🏷️ いいねが最も付きやすいタグは?(母数の偏りを考慮して) |
| 2 | 📝 伸びる記事のタイトルに共通する特徴は? |
| 3 | 📅 投稿する曜日・時間帯はいいね数に影響する? |
| 4 | 👥 フォロワーが少なくても伸びている記事の共通点は? |
| 5 | 🖼️ コードブロックや画像の量は記事の評価と関係ある? |
| 6 | 🔍 この分析の限界・バイアスを説明して |
結果①:まず分布を確認していた
分析する前に全体のデータの分布を確認する基本的な動きをしていました。
SNSのデータは伸びている記事とそうではない記事で二分する極端な分布をする傾向にあるので、それを考慮してくれるのかと感心してました。
いいね数は平均3.36・中央値0・55.6%がゼロいいね。中央値は代表値として使い物にならず、平均は少数のバズに乗っ取られる。よって「ヒット率(10いいね以上)」を主軸の指標に採用する。
AIが書いたタグ分析SQL(自然言語で頼んだだけ)
-- 「付きやすさ」を中央値・ヒット率・著者集中度まで一度に見ようとしている
SELECT tag, count() AS n, uniqExact(user_id) AS authors,
round(count()/uniqExact(user_id),1) AS articles_per_author,
quantile(0.5)(likes_count) AS med_likes,
round(countIf(likes_count>=10)/count(),3) AS hit10_rate
FROM (SELECT arrayJoin(tags) AS tag, * FROM qiita_items)
GROUP BY tag HAVING n >= 100
ORDER BY hit10_rate DESC LIMIT 20;
結果②:人間 vs AI の答え合わせ
6問の結論を突き合わせると、こうなりました。
タイトルの「してみた」が1.58倍効く、コロン区切り(〇〇:△△)はむしろ逆効果、みたいな粒度は、正直私は出していませんでした。ここはAIに完敗です。
結果③:同じデータなのに、結論が割れた「フォロワー問題」
一番おもしろかったのが、フォロワーの話です。
- 私:フォロワー帯ごとのヒット率を見て、「F0=3.6% に対して F1000+=38.6%。約10倍。フォロワーが最強要因だ」
- AI:いいねとフォロワーの相関係数は0.009(ほぼ0)。だからフォロワーの効きは弱い
真逆です。でも、どちらも嘘ではありませんでした。
種明かしをすると、フォロワー数には 最大92.7万 という外れ値(公式アカウント等)があり、いいねもべき分布。この2つが相関係数を0近くまで潰してしまう。一方で、帯に区切って見ると、きれいな単調増加が立つ。同じ列を、相関で見るか、帯で見るかで、結論がひっくり返ったわけです。
AIに分析を任せる時代って、たぶんこういうことなんだと思います。AIはSQLを速く正確に書く。でも「どの指標で見るか」「その数字をどう解釈するか」を間違えると、平気で逆の結論を出す。問いの立て方は、まだ人間の仕事でした。
正直な評価
- 速さ:AIは合計15本のクエリを投げて、交絡チェック(「画像が多い記事は、もともとフォロワーが多い人が書いてるだけでは?」を低フォロワー層だけで再検証)と自己批判まで付けて、約7分で完走。全クエリのサーバー処理は合計1秒未満でした。私が数時間かけた分析と、ほぼ同じ場所に着地しています。
- 甘かった点:フォロワーの相関≈0を額面通り強調しすぎ。帯別の山が見えていませんでした。
- 鋭かった点:誰も教えていないのに「いいねが記事の古さで正規化されていない(古い記事ほど有利)」というバイアスを自分で見つけて、結論に留保を付けてきたこと。
おまけ:MCP接続でハマったところ
リモートMCPの認証で、ブラウザに「localhost で接続が拒否されました」と出て一瞬焦りました。でもこれは失敗ではなく、認可が成功してリダイレクトされたサイン。アドレスバーのURLを使えば認証は通ります。
ほかにも、「Connect with MCP」トグルは既定オフ/サービスがアイドルだと初回クエリでウェイク(実測5秒くらい)/クエリツールにはサービスID(UUID)が必須、あたりが地味なハマりどころでした。
結論:「伸びる記事の分析」
人間とAI、両方の分析がだいたい一致した「伸びる条件」をまとめます。
- 最大要因はフォロワー数。F1000+で38.6%、F0で3.6%。残酷だけど、母数は強い。
-
でもフォロワー0でも3.6%は当たる。
ポエム/新人エンジニア/キャリア/マネジメントのような実体験・共感系はフォロワー不問で平均いいねが高い。無名でも伸びる記事は、画像・リンク・見出しが多い「作り込み型」でした。 - 体裁:画像は多いほど効く(6.5%→12.6%)。タイトルは25字以上+「してみた」「?」が効く。コードの量は、ほぼ関係ない。
- タイミング:平日の朝〜昼(月曜6時台で11.4%など)。
ところでこの記事、薄々お気づきかもしれませんが——共感寄りのテーマで、画像多め、タイトルは25字以上、コードは盛らない。つまり、上の条件にできるだけ寄せて書いています。効くかどうかは、この記事のいいね数で答え合わせということで。
AI時代のデータベースに思うこと
最後に少しだけ考えたことを。
今回いちばん効いたのは、ClickHouseが速いことでした。速いから、私もAIも何度も角度を変えて試せた。AIは15本のクエリを1秒未満で回しています。もしDBが遅かったら、AIの試行回数も減って、分析はもっと浅くなっていたはずです。速度は、エージェントの試行回数に直結する。
もう一つは、スキーマの分かりやすさ。カラム名が likes_count や image_count と素直だったから、AIは迷わずSQLを書けました。私の持論ですが人がSQLを書く時代はもう後5年後にはない気がしています。(少なくとも先進的な企業では)
データアナリストの仕事は、「SQLを書く人」から「問いと指標を設計して、AIの出した答えを検証する人」へ寄っていくんだろうな、と。今回のフォロワー問題が、まさにその縮図でした。
おわりに
長くなりましたが、ここまで読んでくれてありがとうございます。
あなたの記事は、今回の条件のどれに当てはまっていましたか? もし手元のデータで試したら、結果を教えてください。私はもう少し、いいねの正規化(記事の古さ補正)に踏み込んでみようと思います。


