b_crm_event — таблица, которая на живом портале растёт быстрее всех остальных: каждое изменение любого поля любой сущности CRM пишет туда строку. На портале с активным отделом продаж и интеграцией с сайтом это миллионы записей в год.
Чистить её можно и нужно, но прежде чем удалять — стоит понять, что именно вы удаляете, и убедиться, что проблема действительно в объёме таблицы, а не в отсутствии индекса.
Проблема
Со временем таблицы 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 — отдельная операция с простоем, а не завершающий штрих скрипта очистки.
Регулярная чистка лучше разовой. Агент, удаляющий понемногу каждую ночь, не создаёт нагрузки и не требует окна обслуживания — в отличие от разовой операции над накопленными за пять лет данными.
И вопрос, который стоит задать бизнесу до начала работы: какой срок хранения истории взаимодействий с клиентом им действительно нужен. Ответ «два года» превращает бесконечную задачу в понятное правило и настраиваемый агент. Ответ «навсегда» означает, что чистить нельзя, и решать вопрос производительности придётся индексами и архивированием, а не удалением.
Комментарии
Система комментариев скоро будет подключена. А пока вы можете написать мне в Telegram или на email.