b_crm_event — таблица, которая на живом портале растёт быстрее всех остальных: каждое изменение любого поля любой сущности CRM пишет туда строку. На портале с активным отделом продаж и интеграцией с сайтом это миллионы записей в год.
Чистить её можно и нужно, но прежде чем удалять — стоит понять, что именно вы удаляете, и убедиться, что проблема действительно в объёме таблицы, а не в отсутствии индекса.
Почему растёт таблица b_crm_event
Со временем таблицы b_crm_event и b_crm_event_relations разрастаются до миллионов записей. Каждое действие — звонок, письмо, изменение поля — создаёт новое событие. На крупных порталах это приводит к:
- Замедлению открытия карточек сделок и лидов
- Росту размера базы данных
- Увеличению времени резервного копирования
Сначала проверьте, что виноват объём. Замедление карточки сделки на большой b_crm_event чаще объясняется не количеством строк, а тем, что запрос идёт без подходящего индекса или выбирает историю целиком вместо страницы. Миллион строк для MySQL с правильным индексом — это доли миллисекунды.
Проверяется до всякого удаления: EXPLAIN на запросе, который выполняется при открытии карточки, и SHOW INDEX FROM b_crm_event_relations. Если в плане type: ALL — задача решается индексом, а не удалением данных. Удалить историю клиентов ради ускорения, которого можно было добиться индексом, — обидный сценарий.
Структура хранения событий
| Таблица | Назначение |
|---|---|
b_crm_event | Сами события (дата, тип, описание) |
b_crm_event_relations | Связи событий с сущностями CRM |
Внимание: Удаление событий необратимо. Перед очисткой сделайте резервную копию базы данных.
Удаление событий у конкретного элемента
Метод CCrmEvent::DeleteByElement() удаляет все события, связанные с элементом:
<?php
use Bitrix\Main\Loader;
Loader::includeModule('crm');
// Удаление событий у Лида
$leadId = 123;
CCrmEvent::DeleteByElement(CCrmOwnerType::Lead, $leadId);
// Удаление событий у Сделки
$dealId = 456;
CCrmEvent::DeleteByElement(CCrmOwnerType::Deal, $dealId);
// Удаление событий у Контакта
$contactId = 789;
CCrmEvent::DeleteByElement(CCrmOwnerType::Contact, $contactId);
// Удаление событий у Компании
$companyId = 101;
CCrmEvent::DeleteByElement(CCrmOwnerType::Company, $companyId);Типы сущностей CRM
| Константа | Значение | Сущность |
|---|---|---|
CCrmOwnerType::Lead | 1 | Лид |
CCrmOwnerType::Deal | 2 | Сделка |
CCrmOwnerType::Contact | 3 | Контакт |
CCrmOwnerType::Company | 4 | Компания |
CCrmOwnerType::Invoice | 5 | Счёт |
CCrmOwnerType::Quote | 7 | Предложение |
Массовая очистка старых событий
Для очистки событий старше определённой даты используем пакетную обработку:
<?php
namespace Local\Maintenance;
use Bitrix\Main\Loader;
use Bitrix\Crm\EventTable;
use Bitrix\Crm\EventRelationsTable;
class CrmEventCleaner
{
/**
* Размер пакета для обработки
*/
private const BATCH_SIZE = 5000;
/**
* Очистка событий старше указанной даты
*/
public function cleanOlderThan(string $date, int $maxBatches = 10): array
{
if (!Loader::includeModule('crm')) {
throw new \RuntimeException('Модуль CRM не установлен');
}
$stats = [
'events_deleted' => 0,
'relations_deleted' => 0,
'batches_processed' => 0,
];
for ($batch = 0; $batch < $maxBatches; $batch++) {
$events = $this->getEventsBatch($date);
if (empty($events)) {
break;
}
$eventIds = array_column($events, 'ID');
$relationsDeleted = $this->deleteRelations($eventIds);
$eventsDeleted = $this->deleteEvents($eventIds);
$stats['events_deleted'] += $eventsDeleted;
$stats['relations_deleted'] += $relationsDeleted;
$stats['batches_processed']++;
// Пауза между пакетами для снижения нагрузки
usleep(100000); // 100ms
}
return $stats;
}
/**
* Получение пакета событий для удаления
*/
private function getEventsBatch(string $beforeDate): array
{
$result = EventTable::getList([
'order' => ['ID' => 'ASC'],
'filter' => [
'<DATE_CREATE' => $beforeDate,
],
'select' => ['ID'],
'limit' => self::BATCH_SIZE,
]);
return $result->fetchAll();
}
/**
* Удаление связей событий
*/
private function deleteRelations(array $eventIds): int
{
$relations = EventRelationsTable::getList([
'filter' => ['EVENT_ID' => $eventIds],
'select' => ['ID'],
])->fetchAll();
$count = 0;
foreach ($relations as $relation) {
$crmEvent = new \CCrmEvent();
if ($crmEvent->Delete((int)$relation['ID'], ['CURRENT_USER' => 1])) {
$count++;
}
}
return $count;
}
/**
* Прямое удаление событий (после удаления связей)
*/
private function deleteEvents(array $eventIds): int
{
$count = 0;
foreach ($eventIds as $eventId) {
$result = EventTable::delete($eventId);
if ($result->isSuccess()) {
$count++;
}
}
return $count;
}
/**
* Подсчёт событий для очистки
*/
public function countEventsOlderThan(string $date): int
{
if (!Loader::includeModule('crm')) {
return 0;
}
$result = EventTable::getList([
'filter' => ['<DATE_CREATE' => $date],
'select' => ['ID'],
'count_total' => true,
]);
return $result->getCount();
}
/**
* Получение статистики по таблицам
*/
public function getTableStats(): array
{
global $DB;
$stats = [];
// Размер таблицы событий
$sql = "SELECT
COUNT(*) as row_count,
ROUND(DATA_LENGTH / 1024 / 1024, 2) as data_mb,
ROUND(INDEX_LENGTH / 1024 / 1024, 2) as index_mb
FROM information_schema.TABLES
WHERE TABLE_NAME = 'b_crm_event'";
$result = $DB->Query($sql);
if ($row = $result->Fetch()) {
$stats['b_crm_event'] = $row;
}
// Размер таблицы связей
$sql = "SELECT
COUNT(*) as row_count,
ROUND(DATA_LENGTH / 1024 / 1024, 2) as data_mb,
ROUND(INDEX_LENGTH / 1024 / 1024, 2) as index_mb
FROM information_schema.TABLES
WHERE TABLE_NAME = 'b_crm_event_relations'";
$result = $DB->Query($sql);
if ($row = $result->Fetch()) {
$stats['b_crm_event_relations'] = $row;
}
return $stats;
}
}Консольный скрипт очистки
Создайте файл /local/scripts/clean_crm_events.php:
<?php
$_SERVER['DOCUMENT_ROOT'] = realpath(__DIR__ . '/../../');
require $_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/prolog_before.php';
use Local\Maintenance\CrmEventCleaner;
$cleaner = new CrmEventCleaner();
// Показываем текущую статистику
echo "=== Статистика таблиц ===\n";
$stats = $cleaner->getTableStats();
foreach ($stats as $table => $info) {
echo "{$table}:\n";
echo " Записей: {$info['row_count']}\n";
echo " Данные: {$info['data_mb']} MB\n";
echo " Индексы: {$info['index_mb']} MB\n";
}
// Подсчёт событий старше года
$oneYearAgo = date('d.m.Y 00:00:00', strtotime('-1 year'));
$count = $cleaner->countEventsOlderThan($oneYearAgo);
echo "\nСобытий старше года: {$count}\n";
// Запуск очистки (раскомментируйте для выполнения)
// echo "\n=== Запуск очистки ===\n";
// $result = $cleaner->cleanOlderThan($oneYearAgo, 20);
// print_r($result);Запуск:
php /home/bitrix/www/local/scripts/clean_crm_events.phpАгент для регулярной очистки
<?php
// /local/php_interface/init.php
use Local\Maintenance\CrmEventCleaner;
// Регистрация агента (раз в сутки в 3:00)
CAgent::AddAgent(
'\\Local\\Maintenance\\CrmEventCleaner::runAgent();',
'main',
'N',
86400,
'',
'Y',
ConvertTimeStamp(strtotime('tomorrow 03:00'), 'FULL')
);Метод агента:
public static function runAgent(): string
{
$cleaner = new self();
// Очищаем события старше 6 месяцев
$sixMonthsAgo = date('d.m.Y 00:00:00', strtotime('-6 months'));
$cleaner->cleanOlderThan($sixMonthsAgo, 5);
// Возвращаем имя агента для повторного запуска
return __METHOD__ . '();';
}Рекомендация: Начните с очистки событий старше 1-2 лет. Более агрессивная очистка может удалить важную историю взаимодействий с клиентами.
Что сделать после очистки таблиц
После удаления большого количества записей место на диске само по себе не освободится: InnoDB оставляет его внутри файла таблицы для повторного использования. Вернуть его файловой системе можно пересозданием таблицы:
OPTIMIZE TABLE b_crm_event;
OPTIMIZE TABLE b_crm_event_relations;OPTIMIZE TABLE для InnoDB — это полная перестройка таблицы, и она блокирует запись. На таблице в несколько гигабайт операция выполняется десятки минут, и всё это время CRM не может записать ни одного события — то есть портал фактически не работает.
Плюс требуется свободное место: во время перестройки на диске существуют обе копии таблицы. Убедитесь, что места хватает, иначе операция упадёт на середине.
Практический порядок: только в окно минимальной нагрузки, с предупреждением пользователей, после проверки свободного места. На действительно больших таблицах — pt-online-schema-change, который делает то же самое без блокировки.
Итоги
Проверьте индексы до удаления. Часто это единственное, что нужно сделать.
Удаляйте порциями с LIMIT, а не одним запросом: DELETE на миллион строк держит блокировки и раздувает журнал транзакций.
Начните с событий старше двух лет и посмотрите на результат. Агрессивная очистка удаляет историю переписки с клиентом, а восстановить её будет неоткуда.
OPTIMIZE TABLE — отдельная операция с простоем, а не завершающий штрих скрипта очистки.
Регулярная чистка лучше разовой. Агент, удаляющий понемногу каждую ночь, не создаёт нагрузки и не требует окна обслуживания — в отличие от разовой операции над накопленными за пять лет данными.
И вопрос, который стоит задать бизнесу до начала работы: какой срок хранения истории взаимодействий с клиентом им действительно нужен. Ответ «два года» превращает бесконечную задачу в понятное правило и настраиваемый агент. Ответ «навсегда» означает, что чистить нельзя, и решать вопрос производительности придётся индексами и архивированием, а не удалением.
Частые вопросы
Почему разрастается таблица b_crm_event?
Потому что туда пишется строка на каждое изменение любого поля любой сущности CRM — звонок, письмо, правка поля. На портале с активным отделом продаж и интеграцией с сайтом это миллионы записей в год. Связи событий с сущностями хранятся отдельно, в таблице b_crm_event_relations.
Всегда ли нужно чистить историю событий CRM?
Нет, сначала проверьте, что виноват объём. Замедление карточки сделки на большой таблице чаще объясняется не количеством строк, а тем, что запрос идёт без подходящего индекса или выбирает историю целиком вместо страницы: миллион строк для MySQL с правильным индексом — это доли миллисекунды. Проверяется до всякого удаления командой EXPLAIN на запросе, который выполняется при открытии карточки, и просмотром индексов. Если в плане стоит type ALL, задача решается индексом, а не удалением данных, и удалять историю клиентов ради ускорения, которого можно было добиться индексом, — обидный сценарий.
Как удалять события CRM безопасно?
Порциями с ограничением по количеству, а не одним запросом: DELETE на миллион строк держит блокировки и раздувает журнал транзакций. Перед удалением обязательна резервная копия базы — операция необратима. И начинайте с событий старше двух лет, посмотрев на результат: агрессивная очистка удаляет историю переписки с клиентом, а восстановить её будет неоткуда.
Опасен ли OPTIMIZE TABLE после очистки?
Для InnoDB это полная перестройка таблицы, и она блокирует запись. На таблице в несколько гигабайт операция выполняется десятки минут, и всё это время CRM не может записать ни одного события — портал фактически не работает. Плюс требуется свободное место: во время перестройки на диске существуют обе копии таблицы, и при нехватке операция упадёт на середине. Выполнять только в окно минимальной нагрузки, с предупреждением пользователей и проверкой места; на действительно больших таблицах — через pt-online-schema-change, который делает то же самое без блокировки.
Разовая чистка или регулярная?
Регулярная лучше. Агент, удаляющий понемногу каждую ночь, не создаёт нагрузки и не требует окна обслуживания — в отличие от разовой операции над накопленными за пять лет данными. Разовая чистка нужна один раз, чтобы разобрать накопленное, дальше её место занимает расписание.
Какой срок хранения истории взаимодействий выбрать?
Это вопрос к бизнесу, а не к разработчику. Ответ «два года» превращает бесконечную задачу в понятное правило и настраиваемый агент. Ответ «навсегда» означает, что чистить нельзя, и вопрос производительности придётся решать индексами и архивированием, а не удалением. Задать этот вопрос стоит до начала работы.