キャッシュカラムによる SQL 最新値検索

以下のようなテーブルがあるとします。

CREATE TABLE nodes (id INTEGER PRIMARY KEY AUTO_INCREMENT);
CREATE TABLE graphs (id INTEGER PRIMAR KEY AUTO_INCREMENT);
CREATE TABLE logs (id INTEGER PRIMARY KEY AUTO_INCREMENT, node_id INTEGER NOT NULL, created_at DATETIME DEFAULT now());
CREATE TABLE graph_log (id INTEGER PRIMARY KEY AUTO_INCREMENT, graph_id INTEGER NOT NULL, log_id INTEGER NOT NULL);

logs レコードが追加されるたびに graphs への追加が判定され、graph_log に結果が追加されるとします。

この時に、「各 node について最新のlogsレコードに紐づく graphs.id を取得する」には、以下のような SQL 文を書くと思います。

SELECT l.node_id, g.id graph_id FROM graphs g INNER JOIN graph_log gl ON gl.graph_id=g.id INNER JOIN logs l ON gl.log_id=l.id GROUP BY l.node_id LEFT JOIN (SELECT node_id, MAX(id) id FROM logs GROUP BY node_id) lc ON lc.node_id=l.node_id AND lc.log_id=l.id;

しかし、graph_log が数千万行存在する場合、INDEX を張っても検索に時間がかかりすぎることがあります。

しばらく考えましたがうまい解決策はないので、おきて破りとなりますが、nodes テーブルに最新のlog_id を保存するカラムを追加し、logs を追加したときに、このカラムも更新することとしました。Laravel で書くと以下のようになります。

ALTER TABLE nodes ADD COLUMN max_log_id INTEGER;
public function createLog($node_id, $graphs_id) {
  $log = new Log;
  $log->node_id = $node_id;
  $log->save();
  if ($graphs_id !== null) {
    $log->graphs()->sync($graphs_id);
  }
  $node = Node::find($node_id);
  if ($node) {
    $node->max_log_id = $log->id;
    $node->save();
  }
}

こうすると、最新 graphs.id の検索は単純化されます。

SELECT n.id, gl.graph_id FROM nodes n LEFT JOIN graph_log gl WHERE n.max_log_id=gl.log_id;

実測したところ、以前の書き方だと30秒程度要していたのが数百ミリ秒で返されるようになりました。

投稿者について
みのしす

小さいときは科学者になろうとしたのに、その時にたまたま身に着けたプログラミングで未だに飯を食っているしがないおじさんです。(年齢的にはもうすぐおじいさん)

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です