Concrete CMS 8.5.21:Express 大量データ環境でのランダムソート更新と UI 障害の原因・解決策
はじめに
Concrete CMS 8.5 系で Express エントリーを大量(2 万件以上)に扱う環境で、
ランダムソート用の random_value を更新するジョブを実行すると、編集モードの UI(クリップボード)が壊れる
という現象に遭遇しました。
- クリップボードが空になる
- ブロック追加パネルが開かない
- エラーは出ないが UI が機能しない
最初は Concrete CMS 側の不具合を疑いましたが、原因は MySQL のロック と UPDATE の仕様 にありました。
本記事では、発生した現象、原因、そして最終的に安定して動作した解決策をまとめます。
発生した問題
現象
以下のような「全件 UPDATE」を行うジョブを実行すると UI が壊れました。
UPDATE IllustRandomValue
SET random_value = FLOOR(1 + RAND() * 10000000)
INSERT のみでは問題は起きず、
UPDATE を追加した途端に UI が壊れる という状況でした。
原因①:MySQL の「全件 UPDATE」によるロックが UI を巻き込んでいた
Concrete CMS の編集モードは、
クリップボードやブロック追加 UI を表示するために
大量の SELECT を即時実行します。
そこに 2 万件の全件 UPDATE が走ると:
- InnoDB が長時間ロックを保持
- 編集モードの SELECT がロック待ちに入る
- UI が「空」と誤認して表示されない
という流れになります。
INSERT だけでは問題が出なかったのは:
- INSERT はロックが軽い
- UPDATE は全件ロックで重い
という違いによるものです。
原因②:LIMIT UPDATE は「同じ行ばかり更新される」仕様がある
ロックを避けるために LIMIT を使ったバッチ処理を試すと、
次の問題が発生しました。
UPDATE IllustRandomValue
SET random_value = ...
ORDER BY exEntryID
LIMIT 2000
これは 毎回同じ 2000 行だけが更新される という MySQL の仕様があり、
全件更新ができません。
そのため、random_value が「順送りのように見える」現象も発生しました。
原因③:RAND() のシードをいじると「ランダムに見えない」ことがある
RAND(seed) は
- 同じ seed → 同じ乱数
- seed が規則的に増える → 乱数も規則的に増える
という性質があるため、
ソート用途では RAND(seed) は不向き です。
RAND(exEntryID + 時間) のような工夫も、
実際には「ランダムに見えない」ことがあります。
最終的に最も安定した解決策:
???? exEntryID の範囲で分割し、RAND() を素直に使うバッチ方式
最終的に最も安定し、UI に影響を与えず、
random_value も十分にバラけたのは次の方式です。
✔ ポイント
- LIMIT を使わない(同じ行ばかり更新される問題を回避)
- exEntryID の範囲で分割する
- RAND() を素直に使う(行ごとに独立して評価される)
- 1 回の UPDATE は 2000 行程度に抑える(ロック時間を最小化)
実際に採用したジョブ(最終版)
<?php
namespace Application\Job;
use Concrete\Core\Job\Job;
use Concrete\Core\Support\Facade\Database;
class UpdateRandomValueJoinsort extends Job
{
public function getJobName()
{
return t("日次ランダム値更新(範囲バッチ方式)");
}
public function getJobDescription()
{
return t("IllustRandomValue テーブルの random_value を、安全にバラけさせて更新します。");
}
public function run()
{
$db = Database::connection();
// ① 新規エントリーの追加(random_value は後でまとめて更新)
$db->executeQuery("
INSERT INTO IllustRandomValue (exEntryID, random_value)
SELECT e.exEntryID, 0
FROM ExpressEntityEntries e
LEFT JOIN IllustRandomValue rv ON rv.exEntryID = e.exEntryID
WHERE rv.exEntryID IS NULL
AND e.exEntryEntityID = (SELECT id FROM ExpressEntities WHERE handle = 'illust')
");
// ② exEntryID の最小・最大を取得
$minId = (int) $db->fetchColumn("SELECT MIN(exEntryID) FROM IllustRandomValue");
$maxId = (int) $db->fetchColumn("SELECT MAX(exEntryID) FROM IllustRandomValue");
if ($minId === 0 && $maxId === 0) {
return t("IllustRandomValue に対象データがありません。");
}
// ③ exEntryID の範囲でバッチ更新(RAND() を素直に使う)
$step = 2000;
for ($start = $minId; $start <= $maxId; $start += $step) {
$end = $start + $step - 1;
$db->executeQuery("
UPDATE IllustRandomValue
SET random_value = FLOOR(1 + RAND() * 10000000)
WHERE exEntryID BETWEEN ? AND ?
", [$start, $end]);
}
return t("ランダム値更新完了(範囲バッチ方式)");
}
}
この方式のメリット
- UI が壊れない(ロック時間が短い)
- 全件確実に更新される(LIMIT の罠を回避)
- RAND() が行ごとに独立して評価されるため、見た目が十分バラける
- Express の JOIN ソートとも相性が良い
- データ量が増えてもスケールする
まとめ
今回の問題は、
「大量データ × Concrete CMS × Express × MySQL のロック × UPDATE の仕様」
という複数の要素が絡んだ、現場でしか気づけないタイプの罠でした。
最終的に安定して動いたのは:
- LIMIT を使わず
- exEntryID の範囲で分割し
- RAND() を素直に使う
というシンプルな方式でした。
この知見は、Concrete CMS で大量データを扱う現場にとって非常に有用だと思います。


