Главная/Статьи/Битрикс24: очистка истории событий CRM

Битрикс24: очистка истории событий CRM

Удаление старых событий у лидов, сделок, контактов и компаний. Безопасная очистка таблиц b_crm_event и b_crm_event_relations.

ДМ
Дмитрий Мещеряков
📅 10 сентября 2024 г.📖 7 мин чтения

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
<?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::Lead1Лид
CCrmOwnerType::Deal2Сделка
CCrmOwnerType::Contact3Контакт
CCrmOwnerType::Company4Компания
CCrmOwnerType::Invoice5Счёт
CCrmOwnerType::Quote7Предложение

Массовая очистка старых событий

Для очистки событий старше определённой даты используем пакетную обработку:

php
<?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
<?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);

Запуск:

bash
php /home/bitrix/www/local/scripts/clean_crm_events.php

Агент для регулярной очистки

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')
);

Метод агента:

php
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 оставляет его внутри файла таблицы для повторного использования. Вернуть его файловой системе можно пересозданием таблицы:

sql
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.