articles 詳細クエリの実行プラン(slug 指定)

記事詳細ページ用の単一記事取得クエリ。slugusers.name の 2 つのユニークインデックスから Point_Get を 2 本走らせて HashJoin する形になっており、構造的にはこれ以上削れない。

対象クエリ

SELECT a.article_id, a.title, a.slug, a.user_id, a.content,
       a.thumbnail, a.description, a.status, a.`type`,
       a.published_at, a.created_at, a.updated_at
FROM articles a
JOIN users u ON a.user_id = u.user_id
WHERE a.status = 'published'
  AND a.slug = '01f07hctzhjcwtdq4h6ew9stk8'
  AND u.name = 'shuntaka';

実行プラン(合計 1.18ms / 1 行)

TiDB Query Execution Plan (Article Detail)

articles.slugusers.name の 2 本の Point_Get が HashJoin で合流する Y 字構造。

EXPLAIN ANALYZE 出力

id                     estRows  estCost   actRows  task  access object                                  operator info                                                          memory   disk
HashJoin_9             0.68     27876.31  1        root                                                 inner join, equal:[eq(articles.user_id, users.user_id)]                33.7 KB  0 Bytes
├─Point_Get_13(Build)  1.00     242.91    1        root  table:users, index:uq_users_name(name)
└─Selection_12(Probe)  0.68     26010.67  1        root                                                 eq(articles.status, "published")                                       19.9 KB  N/A
  └─Point_Get_11       1.00     25960.77  1        root  table:articles, index:uq_articles_slug(slug)

arguments: ("01f07hctzhjcwtdq4h6ew9stk8", "shuntaka")

主要 execution info(一部抜粋)

  • HashJoin_9: time:1.18ms, loops:2, build_hash_table:{total:804.7µs, fetch:799.9µs, build:4.83µs}, probe:{concurrency:5, total:5.01ms, max:1.02ms, probe:16.8µs, fetch and wait:4.99ms}

  • Point_Get_13: time:763µs, Get:{num_rpc:4, total_time:1.62ms}, total_kv_read_wall_time:270.1µs, tikv_wall_time:341µs

  • Selection_12: time:997.2µs, loops:2

  • Point_Get_11: time:967.2µs, loops:3

サマリ表

番号は データフローの依存方向 で振っている。Probe 側の Point_Get_11(articles)Selection_12 を #1–2、Build 側の Point_Get_13(users) を #3、合流地点の HashJoin_9 を #4 とした。

注意点:

  • EXPLAIN テキストの並びは HashJoin_9 Point_Get_13(Build) Selection_12(Probe) Point_Get_11 の順で、本書の番号順とは一致しない

  • 「どっちが先に走るか」は EXPLAIN ANALYZE の time だけでは厳密には分からない(Build/Probe は部分的に並行する)。あくまで依存関係上の上流/下流の話

#

Operator

actRows

time

備考

1

Point_Get

1

0.97ms

uq_articles_slugarticles を 1 件取得

2

Selection

1

1.00ms

status='published' で確認(既に 1 行)

3

Point_Get

1

0.76ms

uq_users_nameusers を 1 件取得 (Build 側)

4

HashJoin

1

1.18ms

users.user_id = articles.user_id で結合

所見

  • 既に最適: slug は UNIQUE なので Point_Get 1 発で 1 行確定し、users.name も同様。インデックスを追加して縮められる経路はない。

  • Selection は冗長気味だが許容: slug で 1 行確定した後に status='published' を再評価しているのは、下書き記事を公開 URL から弾くための仕様。actRows=1 のままなので実コストは 30µs 程度で問題なし。

  • HashJoin のオーバーヘッド: 1 行同士の結合に HashJoin(33.7KB)を使っているが、Build 側が 1 行なのでハッシュテーブル構築コストは無視できる。IndexJoin でも変わらない。

  • user_id 一致を結合で担保: articles.user_id = users.user_id のチェックを WHERE ではなく JOIN で行うことで、別ユーザーの slug 衝突(理論上は起きないが)に対しても安全。

一覧クエリ (2026-06-30) との比較

観点

詳細 (本書)

一覧 (関連 survey)

駆動経路

Point_Get × 2

Point_Get + IndexLookUp

合計時間

1.18ms

7.33ms

返却行数

1

40

最重ステップ

Point_Get (slug)

TableRowIDScan + Selection

改善余地

ほぼ無し

複合インデックスで Sort 削減可

詳細クエリは現状で問題なし。改善ポーチは一覧側に注力する。