Concrete CMS Express 大規模データ運用:ランダムソート高速化の実践事例
本記事では、15,000 件以上の Express エントリーを高速にランダム表示するための最適解を、実際の改善プロセスとともに紹介します。
1. Express 属性を 1 件ずつ save() すると 24 時間以上かかる問題
Express の属性値を更新する際、通常は以下のように $entry->setAttribute() → $entry->save() を行います。
しかし、この save() は内部で次のような重い処理を伴います。
- Doctrine ORM によるエンティティ生成・永続化
- Express 属性値テーブルへの INSERT/UPDATE
- 検索インデックスの再構築
- バージョン管理
- キャッシュの更新
- イベントフックの発火
これらが 1 件ごとに発生するため、
「1 分で 10 件」→「15,000 件で 24 時間以上」という結果になりました。
大量データの一括更新には、Express の属性更新は向いていません。
2. SQL で直接 random_value を書き換えてもソートできない理由
「では SQL で直接 random_value を更新すれば高速なのでは?」
と考えるのは自然ですが、Express の属性は単純な値ではありません。
Express 属性は以下の複数レイヤーで管理されています。
- 属性値テーブル
- 検索インデックス
- Doctrine のキャッシュ
- エンティティのメタ情報
そのため、SQL で値だけ書き換えても Express 側が変更を認識しません。
結果として、ソートに使われる値が更新されず、表示順が変わらないという問題が発生します。
3. 解決策:独立した random_value テーブルを作り、JOIN でソートする方式へ
Express の属性 random_value を使うのではなく、
完全に独立したテーブルを作成し、そこにランダム値を保持する方式に切り替えました。
● 新しいテーブル構造(例)
IllustRandomValue
exEntryID (Express のエントリーID)
random_value (ランダムソート用の値)
● ソートは INNER JOIN で実現
ExpressEntityEntries e
JOIN IllustRandomValue rv ON rv.exEntryID = e.exEntryID
ORDER BY rv.random_value ASC
これにより:
- Express の属性システムに依存しない
- Doctrine のキャッシュ問題も回避
- SQL で高速に更新可能
- 15,000 件でも 0.1 秒でランダム値更新が完了
という大幅な改善が実現しました。
4. 新規エントリーにも自動で random_value を付与するジョブを作成
CSV インポートで新規エントリーが追加された場合、
IllustRandomValue に対応する行がまだ存在しません。
そこで、次の処理を行うジョブを作成しました。
- IllustRandomValue に存在しない exEntryID を検出
- INSERT して初期 random_value を付与
- 既存行は UPDATE でランダム値を更新
これにより、新規エントリーも自動でランダムソートに組み込まれるようになりました。
5. 最終的な運用フロー
① CSV インポート(Express に新規エントリーが追加される)
② update_random_value_joinsort ジョブを実行
→ 新規エントリーに random_value を付与
→ 既存エントリーも更新
③ 表示テンプレートで INNER JOIN ソート
この構成により:
- 大量データでも高速
- 表示順が安定
- 新規データも確実に反映
- Express の属性構造に依存しない
- Concrete CMS のバージョン差異にも強い
という、長期運用に耐える仕組みが完成しました。
まとめ
Concrete CMS の Express は便利ですが、大量データの更新には工夫が必要です。
今回の改善では、Express の属性更新の重さを回避し、
独立テーブル + JOIN ソートというシンプルで高速な方式に切り替えることで、
15,000 件以上のデータを快適に扱えるようになりました。
同様の課題を抱える開発者の参考になれば幸いです。








