Введение
Бренд сменил название, сайт переехал на другой домен, в сотнях карточек товаров одна и та же опечатка — текст нужно поправить сразу во множестве строк. Выгружать данные в скрипт не обязательно: хватит UPDATE со строковой функцией REPLACE().
Запрос меняет множество строк разом, и без резервной копии прежние значения не вернуть. Минимальная страховка — mysqldump нужной таблицы или транзакция, о которой ниже.
Как устроена функция REPLACE()
REPLACE(str, from_str, to_str)
str— строка, в которой ищем (обычно имя колонки);from_str— искомый фрагмент;to_str— чем его заменить.
На выходе получается копия str, где каждое вхождение from_str заменено на to_str. Стоит одному из аргументов оказаться NULL — и весь результат будет NULL. Функция лишь вычисляет новое значение — в таблицу его записывает UPDATE.
Одноимённый оператор REPLACE INTO к функции отношения не имеет. Это разновидность INSERT: он удаляет строку с совпадающим первичным ключом или уникальным индексом и вставляет на её место новую.
Замена одного слова
Допустим, в каталоге товаров слово «диск» в названиях нужно поменять на «накопитель»:
UPDATE products
SET name = REPLACE(name, 'диск', 'накопитель')
WHERE name LIKE '%диск%';
Формально WHERE здесь не обязателен: там, где слова нет, REPLACE() вернёт значение без изменений. И всё же условие лучше писать всегда:
UPDATEзатронет только строки, где слово есть. Это важно для триггеровBEFORE/AFTER UPDATE: MySQL вызывает их для каждой обработанной строки, даже если значение не изменилось;- тем же условием удобно заранее проверить выборку через
SELECT; - большую таблицу проще обновлять порциями по диапазону ключей:
WHERE id BETWEEN 1 AND 10000 AND name LIKE '%диск%'и так далее.
Частый случай — переезд на новый домен и обновление ссылок в записях:
UPDATE posts
SET content = REPLACE(content, 'http://old.ru', 'https://new.ru')
WHERE content LIKE '%http://old.ru%';
Предпросмотр и транзакция
Тот же вызов REPLACE() внутри SELECT покажет новые значения рядом со старыми, ничего не записав:
SELECT id, name, REPLACE(name, 'диск', 'накопитель') AS preview
FROM products
WHERE name LIKE '%диск%'
LIMIT 20;
Для таблиц InnoDB есть второй уровень защиты — выполнить UPDATE в транзакции, проверить строки и лишь затем зафиксировать или откатить:
START TRANSACTION;
UPDATE products
SET name = REPLACE(name, 'диск', 'накопитель')
WHERE name LIKE '%диск%';
SELECT id, name FROM products WHERE name LIKE '%накопитель%' LIMIT 20;
-- результат устраивает — фиксируем
COMMIT;
-- если нет — отменяем: ROLLBACK;
Бэкап транзакция не заменяет: пока она открыта, изменённые строки заблокированы, поэтому затягивать её не стоит, а после COMMIT пути назад нет.
Замена в нескольких полях
В SET можно перечислить несколько присваиваний и заменить слово в нескольких колонках за один проход. Условия в WHERE объединяются через OR:
UPDATE site_content
SET pagetitle = REPLACE(pagetitle, 'диск', 'альбом'),
longtitle = REPLACE(longtitle, 'диск', 'альбом'),
introtext = REPLACE(introtext, 'диск', 'альбом'),
content = REPLACE(content, 'диск', 'альбом'),
description = REPLACE(description, 'диск', 'альбом')
WHERE pagetitle LIKE '%диск%'
OR longtitle LIKE '%диск%'
OR introtext LIKE '%диск%'
OR content LIKE '%диск%'
OR description LIKE '%диск%';
Если слово нашлось, скажем, только в content, остальные колонки этой строки останутся как были: для них REPLACE() просто вернёт исходное значение.
Несколько слов одним запросом: цепочка REPLACE
Функции, принимающей список пар «что — на что», в MySQL нет. Выход — вложенные вызовы: внутренний REPLACE() отдаёт строку внешнему, тот делает свою замену.
UPDATE products
SET name = REPLACE(REPLACE(REPLACE(name,
'диск', 'накопитель'),
'флешка', 'USB-накопитель'),
'Самсунг', 'Samsung')
WHERE name LIKE '%диск%'
OR name LIKE '%флешка%'
OR name LIKE '%Самсунг%';
Вычисление идёт от самого внутреннего вызова к внешнему, и от этого порядка зависит результат. Когда то, что вставила одна замена, совпадает с тем, что ищет следующая, текст будет заменён повторно:
-- задача: кот → кошка, кошка → собака
SELECT REPLACE(REPLACE('кот и кошка', 'кот', 'кошка'), 'кошка', 'собака');
-- собака и собака
SELECT REPLACE(REPLACE('кот и кошка', 'кошка', 'собака'), 'кот', 'кошка');
-- кошка и собака
Отсюда практическое правило: первыми ставить замены тех слов, которые могут появиться в тексте после других замен. А если два значения нужно поменять местами (A ↔ B), без промежуточного маркера не обойтись — временной строки, которой гарантированно нет в данных.
Регистр и частичные совпадения
Неожиданный результат REPLACE() обычно объясняется одной из двух особенностей.
Регистр. По документации MySQL, REPLACE() сравнивает подстроки с учётом регистра независимо от collation колонки. Значит, слово «Диск» с заглавной буквы в начале предложения не заменится. Условие LIKE при этом ведёт себя иначе: с collation utf8mb4_0900_ai_ci регистр ему безразличен, и WHERE name LIKE '%диск%' отберёт в том числе «Дискета», хотя REPLACE() в этой строке ничего не поменяет. Предпросмотр это расхождение покажет. Заглавный вариант проще обработать отдельным звеном цепочки: REPLACE(REPLACE(name, 'диск', 'накопитель'), 'Диск', 'Накопитель').
Части слов. Функция ищет подстроку, о границах слов она ничего не знает. Поэтому «дискета» превратится в «накопительета». Чтобы заменять только слово целиком, понадобится REGEXP_REPLACE() и граница слова \b в шаблоне:
UPDATE products
SET description = REGEXP_REPLACE(description, '\\bдиск\\b', 'накопитель', 1, 0, 'c')
WHERE description REGEXP '\\bдиск\\b';
Полная сигнатура — REGEXP_REPLACE(expr, pat, repl[, pos[, occurrence[, match_type]]]). В примере 1 означает поиск с первого символа, 0 — замену всех совпадений, а 'c' включает учёт регистра ('i' его отключает).
На что обратить внимание:
- функция появилась в MySQL 8.0.4 (работает на библиотеке ICU) и в MariaDB 10.0.5 (на PCRE);
- обратный слэш внутри строкового литерала SQL нужно удваивать:
'\\bдиск\\b'. С'\bдиск\b'MySQL примет\bза управляющую последовательность, и замена молча не произойдёт; - с регистром
REGEXP_REPLACE()по умолчанию обходится не так, какREPLACE(): она следует collation. При*_ciпод замену попадёт и «Диск», а на его месте окажется «накопитель» со строчной буквы. Чтобы не зависеть от collation,match_typeлучше указывать явно; - в MariaDB у функции всего три аргумента. Регистр там включают флагом в самом шаблоне —
'(?-i)\\bдиск\\b', а на группы в строке замены ссылаются через\\1(в MySQL для этого служит$1).
Работу \b с кириллицей проверили на MySQL 8.0.46, 8.4.11 и MariaDB 10.8: отдельное слово «диск» заменяется, а «дискета» не затрагивается.
| Задача | REPLACE() | REGEXP_REPLACE() |
|---|---|---|
| Регистр | всегда учитывается | по collation или флагу c/i |
| Целое слово | нет, заменяет подстроку | да, через \b (в строке SQL — \\b) |
| Версии | все | MySQL 8.0.4+, MariaDB 10.0.5+ |
| Скорость | быстрее | медленнее, регулярное выражение |
Общее ограничение у обеих функций одно: для них содержимое колонки — просто строка. В HTML замена доберётся до атрибутов и адресов ссылок, а в JSON-колонке способна сделать документ невалидным. Для таких полей предпросмотр особенно важен.
Замена из Laravel
Проще всего выполнить тот же SQL через DB::update(), передав значения через привязку параметров. В ответ метод отдаёт количество изменённых строк:
use Illuminate\Support\Facades\DB;
$old = 'диск';
$new = 'накопитель';
// экранируем % и _ для LIKE
$like = '%' . addcslashes($old, '%_\\') . '%';
$affected = DB::update(
'UPDATE products SET name = REPLACE(name, ?, ?) WHERE name LIKE ?',
[$old, $new, $like],
);
Склеивать запрос из строк с $old и $new нельзя: так открывается дорога SQL-инъекции. Query builder тоже умеет делать такую замену, однако DB::raw() внутри update() не связывает плейсхолдеры. Поэтому этот вариант подходит лишь для фиксированных литералов, но не для данных от пользователя:
// только константы, без пользовательского ввода
DB::table('products')
->where('name', 'like', '%диск%')
->update(['name' => DB::raw("REPLACE(name, 'диск', 'накопитель')")]);
Оба способа идут в базу мимо Eloquent. Если на изменение модели завязаны события и обсерверы, сброс кэша или переиндексация Scout, ничего из этого не выполнится. Тогда записи обходят пакетами через chunkById() и правят текст в PHP — медленнее, зато логика приложения отработает как при обычном сохранении:
use App\Models\Product;
Product::query()
->where('name', 'like', '%диск%')
->chunkById(200, function ($products) {
foreach ($products as $product) {
$product->name = str_replace('диск', 'накопитель', $product->name);
$product->save(); // сработают события модели
}
});
Разовую замену удобно оформить миграцией данных: она останется в истории проекта и одинаково выполнится на всех окружениях.
Итог
Безопасная массовая замена текста в MySQL укладывается в четыре шага:
- Сделать бэкап таблицы.
- Посмотреть результат заранее:
SELECT … REPLACE(…) AS preview … LIMIT 20. - Открыть транзакцию (InnoDB) и выполнить
UPDATEс условиемWHERE: несколько слов — цепочкойREPLACEс продуманным порядком, целые слова — черезREGEXP_REPLACE(). - Выборочно проверить изменённые строки и только потом сделать
COMMIT(илиROLLBACK).