Как удалить документы 1С, оставив по 10 записей за каждый день?

Программист 1С v8.3 (Управляемые формы) Бухгалтерский учет
← На главную

Дорогие коллеги-программисты, давайте вместе разберем актуальную задачу, с которой часто сталкиваются при работе с базами данных 1С: удаление документов с ограничением по количеству за определенный период, например, оставляя не более 10 документов за каждый день. Исходная проблема, с которой столкнулся автор, заключалась в том, что его код оставлял 10 документов суммарно за весь период, а не по 10 за каждый день.

Эта задача требует внимательного подхода к написанию запросов и обходу результатов, чтобы достичь желаемого результата без потерь важных данных и с учетом производительности. Мы рассмотрим несколько эффективных способов решения этой проблемы, проанализируем их преимущества и недостатки, а также дадим рекомендации по оптимизации.

Ошибки в первоначальной реализации автора и как их избежать

Прежде чем перейти к корректным решениям, проанализируем первоначальный код автора, чтобы понять, в чем заключалась логическая ошибка. Идея использования запроса с итогами по датам была правильной, но реализация счетчика оказалась некорректной.


Процедура УдалениеДокументовНаСервере()
    Запрос = Новый Запрос;
    Запрос.Текст = 
    "ВЫБРАТЬ 
    |    Ссылка
    |ИЗ
    |    Документ.ПриходныйКассовыйОрдер КАК ПриходныйКассовыйОрдер
    |ГДЕ
    |    ПриходныйКассовыйОрдер.Дата МЕЖДУ &НачальнаяДата И &КонечнаяДата
    |УПОРЯДОЧИТЬ ПО
    |    Дата
    |ИТОГИ ПО
    |    Дата ПЕРИОДАМИ(ДЕНЬ, &НачальнаяДата, &КонечнаяДата)";
    
    НачальнаяДата = Дата(2020,8,31);
    КонечнаяДата = Дата(2020,11,30);
    Запрос.УстановитьПараметр("НачальнаяДата",НачальнаяДата);
    Запрос.УстановитьПараметр("КонечнаяДата",КонечнаяДата);
    
    ВыборкаПоИтогам = Запрос.Выполнить().Выбрать(ОбходРезультатаЗапроса.ПОГруппировкам, "Дата");
    
    УстановитьМонопольныйРежим(Истина); // Хорошая практика для массовых операций
    Счетчик = 0; // Глобальный счетчик, который не сбрасывается для каждого дня
    
    // Этот запрос и условие ниже избыточны и некорректны для данной задачи
    Запрос = Новый Запрос;
    Запрос.Текст = 
    "ВЫБРАТЬ
    |    Количество(Ссылка) КАК Количество
    |ИЗ
    |    Документ.ПриходныйКассовыйОрдер КАК ПриходныйКассовыйОрдер
    |ГДЕ Дата >= ДАТАВРЕМЯ(2020,8,31)";
    РезультатЗапроса = Запрос.Выполнить();
    Выборка = РезультатЗапроса.Выбрать();
    
    Если Выборка.Следующий() Тогда
        Если Выборка.Количество > 10 Тогда // Проверка общего количества, а не по дням
            Пока ВыборкаПоИтогам.Следующий() Цикл
                ВыборкаДокументов = ВыборкаПоИтогам.Выбрать();
                Пока ВыборкаДокументов.Следующий() Цикл
                    Счетчик = Счетчик + 1;
                    Если Счетчик > 10 Тогда // Счетчик не сбрасывается, поэтому удаляет все после первых 10 документов за весь период
                        ДокОбъект = ВыборкаДокументов.Ссылка.ПолучитьОбъект();
                        ДокОбъект.Удалить();
                    Иначе
                        Прервать; // Это прервет обход текущего дня, что тоже неверно
                    КонецЕсли;
                КонецЦикла;
            КонецЦикла;
        КонецЕсли; 
    КонецЕсли;
КонецПроцедуры

Основная проблема заключалась в том, что переменная Счетчик объявлялась один раз и не сбрасывалась при переходе к новой группе (новому дню). В результате счетчик просто продолжал увеличиваться, и удаление начиналось после первых 10 документов за весь выбранный период, а не по 10 документов за каждый день. Кроме того, использование Прервать внутри внутреннего цикла приводило к тому, что обработка не удаляла ничего после первых 10 документов, если они были найдены.

Решение 1: Итерационный подход с корректным сбросом счетчика

Этот подход является наиболее интуитивным и представляет собой исправление логики, заложенной автором изначально. Мы будем использовать запрос с итогами по датам, чтобы получить сгруппированные данные, а затем для каждой группы (каждого дня) будем вести свой счетчик. Этот метод прост для понимания и реализации. Для повышения производительности, особенно при большом количестве документов для удаления, можно сначала собрать ссылки на удаляемые документы в список значений или массив, а затем использовать пакетное удаление, если это возможно для конкретного типа документа, или пройти по списку для удаления объектов.

Шаг 1: Формируем запрос с итогами по датам

Нам потребуется запрос, который выбирает все ссылки на документы ПриходныйКассовыйОрдер за указанный период. Важно использовать секцию ИТОГИ ПО с периодом ДЕНЬ, чтобы получить выборку, которую можно будет обойти по группам дней. Также обязательно указываем УПОРЯДОЧИТЬ ПО Дата, чтобы документы внутри каждого дня были отсортированы, что позволит нам выбрать "первые" 10 документов.


Запрос = Новый Запрос;
Запрос.Текст = 
"ВЫБРАТЬ 
|    ПриходныйКассовыйОрдер.Ссылка КАК Ссылка,
|    ПриходныйКассовыйОрдер.Дата КАК Дата
|ИЗ
|    Документ.ПриходныйКассовыйОрдер КАК ПриходныйКассовыйОрдер
|ГДЕ
|    ПриходныйКассовыйОрдер.Дата МЕЖДУ &НачальнаяДата И &КонечнаяДата
|УПОРЯДОЧИТЬ ПО
|    ПриходныйКассовыйОрдер.Дата
|ИТОГИ ПО
|    НАЧАЛОПЕРИОДА(ПриходныйКассовыйОрдер.Дата, ДЕНЬ) ПЕРИОДАМИ(ДЕНЬ, &НачальнаяДата, &КонечнаяДата)";

Шаг 2: Устанавливаем параметры и выполняем запрос

Определим начальную и конечную даты для фильтрации документов. Эти даты будут использоваться как в секции ГДЕ, так и в секции ИТОГИ ПО.


НачальнаяДата = Дата(2020,8,31);
КонечнаяДата = Дата(2020,11,30);

Запрос.УстановитьПараметр("НачальнаяДата", НачальнаяДата);
Запрос.УстановитьПараметр("КонечнаяДата", КонечнаяДата);

ВыборкаПоДням = Запрос.Выполнить().Выбрать(ОбходРезультатаЗапроса.ПОГруппировкам);

Мы используем ОбходРезультатаЗапроса.ПОГруппировкам, чтобы получить выборку верхнего уровня, которая будет представлять каждый день. Затем мы сможем получить детальную выборку для каждого дня.

Шаг 3: Обходим результат запроса и удаляем лишние документы

Теперь самое важное: обходим результат. Нам потребуется два вложенных цикла: один для обхода групп (дней), другой — для обхода документов внутри каждого дня. Внутри цикла по дням мы сбрасываем счетчик документов.


УстановитьМонопольныйРежим(Истина); // Рекомендуется для массовых операций удаления
НачатьТранзакцию(); // Начинаем транзакцию для атомарности операции

Попытка
    Пока ВыборкаПоДням.Следующий() Цикл
        СчетчикДокументовВДень = 0; // Сбрасываем счетчик для каждого нового дня
        
        ВыборкаДокументов = ВыборкаПоДням.Выбрать(); // Получаем выборку документов для текущего дня
        Пока ВыборкаДокументов.Следующий() Цикл
            СчетчикДокументовВДень = СчетчикДокументовВДень + 1;
            
            Если СчетчикДокументовВДень > 10 Тогда // Если документов больше 10, начинаем удаление
                ДокОбъект = ВыборкаДокументов.Ссылка.ПолучитьОбъект();
                ДокОбъект.Удалить();
            КонецЕсли;
        КонецЦикла;
    КонецЦикла;
    
    ЗафиксироватьТранзакцию(); // Если все прошло успешно, фиксируем изменения
Исключение
    ОтменитьТранзакцию(); // При ошибке отменяем все изменения
    Сообщить("Произошла ошибка при удалении документов: " + ОписаниеОшибки());
КонецПопытки;

УстановитьМонопольныйРежим(Ложь); // Возвращаем обычный режим работы

Важные моменты:

  1. Мы используем УстановитьМонопольныйРежим(Истина). Это гарантирует, что никто другой не сможет работать с базой данных во время выполнения операции, что критично для массового удаления.
  2. Операция обернута в НачатьТранзакцию() / ЗафиксироватьТранзакцию() / ОтменитьТранзакцию(). Это обеспечивает атомарность: если в процессе удаления произойдет ошибка, все изменения можно будет откатить.
  3. СчетчикДокументовВДень сбрасывается для каждого нового дня, что является ключевым отличием от исходного кода автора.
  4. Для каждого документа, который нужно удалить, мы получаем его объект ВыборкаДокументов.Ссылка.ПолучитьОбъект() и вызываем метод Удалить().

Решение 2: Использование временных таблиц и эмуляция нумерации строк

Этот подход является более "SQL-ориентированным" и часто более производительным для больших объемов данных, поскольку основная работа выполняется на стороне СУБД. Он позволяет получить список ссылок на удаляемые документы одним запросом.

Шаг 1: Создаем временную таблицу с документами и их днями

Сначала мы выбираем все документы за интересующий период и помещаем их во временную таблицу. Добавляем поле НачалоДня для удобства группировки и последующего соединения. Индексирование временной таблицы по ключевым полям может значительно ускорить операции соединения и фильтрации.


Запрос = Новый Запрос;
Запрос.Текст = 
"ВЫБРАТЬ
|    НАЧАЛОПЕРИОДА(ПриходныйКассовыйОрдер.Дата, ДЕНЬ) КАК НачалоДня,
|    ПриходныйКассовыйОрдер.Дата КАК Дата,
|    ПриходныйКассовыйОрдер.Ссылка КАК Ссылка
|ПОМЕСТИТЬ ВТ_Документы
|ИЗ
|    Документ.ПриходныйКассовыйОрдер КАК ПриходныйКассовыйОрдер
|ГДЕ
|    ПриходныйКассовыйОрдер.Дата МЕЖДУ НАЧАЛОПЕРИОДА(&ДатаН, ДЕНЬ) И КОНЕЦПЕРИОДА(&ДатаК, ДЕНЬ)
|ИНДЕКСИРОВАТЬ ПО
|    НачалоДня,
|    Дата,
|    Ссылка
|;";
Запрос.УстановитьПараметр("ДатаН", Дата(2020,8,31));
Запрос.УстановитьПараметр("ДатаК", Дата(2020,11,30));
Запрос.Выполнить();

Шаг 2: Эмулируем нумерацию строк и отбираем документы для удаления

В 1С нет встроенной функции ROW_NUMBER(), но ее можно эмулировать с помощью левого соединения временной таблицы самой с собой. Присоединение происходит по условию ВТ_Документы.НачалоДня = ВТ_Документы1.НачалоДня (чтобы нумеровать в рамках одного дня) и ВТ_Документы.Дата > ВТ_Документы1.Дата или ВТ_Документы.Ссылка > ВТ_Документы1.Ссылка (для обеспечения уникального порядка). Результат соединения агрегируется с помощью КОЛИЧЕСТВО(ВТ_Документы1.НачалоДня) + 1, что дает порядковый номер документа внутри дня. Затем мы отбираем те документы, чей номер превышает заданное количество (в нашем случае 10).


Запрос.Текст = 
"ВЫБРАТЬ
|    ВТ_Документы.Ссылка КАК СсылкаНаУдаление
|ИЗ
|    ВТ_Документы КАК ВТ_Документы
|        ЛЕВОЕ СОЕДИНЕНИЕ ВТ_Документы КАК ВТ_Документы1
|        ПО ВТ_Документы.НачалоДня = ВТ_Документы1.НачалоДня
|            И ВТ_Документы.Дата > ВТ_Документы1.Дата
|            И ВТ_Документы.Ссылка > ВТ_Документы1.Ссылка
|СГРУППИРОВАТЬ ПО
|    ВТ_Документы.НачалоДня,
|    ВТ_Документы.Дата,
|    ВТ_Документы.Ссылка
|ИМЕЮЩИЕ
|    КОЛИЧЕСТВО(ВТ_Документы1.НачалоДня) + 1 > &КолДокументовВДнеОставить";

Запрос.УстановитьПараметр("КолДокументовВДнеОставить", 10);
ВыборкаУдаляемыхДокументов = Запрос.Выполнить().Выбрать();

Шаг 3: Удаляем отобранные документы

После получения выборки со ссылками на удаляемые документы, нам останется только пройтись по ней и удалить каждый документ. Здесь также критически важно использовать монопольный режим и транзакции.


УстановитьМонопольныйРежим(Истина);
НачатьТранзакцию();

Попытка
    Пока ВыборкаУдаляемыхДокументов.Следующий() Цикл
        ДокОбъект = ВыборкаУдаляемыхДокументов.СсылкаНаУдаление.ПолучитьОбъект();
        Если ДокОбъект <> Неопределено Тогда
            ДокОбъект.Удалить();
        КонецЕсли;
    КонецЦикла;
    ЗафиксироватьТранзакцию();
Исключение
    ОтменитьТранзакцию();
    Сообщить("Произошла ошибка при удалении документов: " + ОписаниеОшибки());
КонецПопытки;

УстановитьМонопольныйРежим(Ложь);

Этот метод обычно эффективнее при обработке значительного количества данных, так как оптимизатор запросов СУБД может лучше справиться с этой задачей, выполняя основную работу на уровне базы данных.

Решение 3: Использование коррелированных подзапросов с ВЫБРАТЬ ПЕРВЫЕ

Это решение выглядит очень компактным и на первый взгляд привлекательным, однако, как показала практика на форуме, оно может быть крайне неэффективным на больших объемах данных и даже привести к зависанию сервера.

Принцип работы

Идея заключается в том, чтобы основной запрос выбирал все документы, а вложенный (коррелированный) подзапрос для *каждой* строки внешнего запроса выбирал первые 10 документов за *тот же день*. Затем внешний запрос отбирает те документы, ссылки которых *не* входят в результат подзапроса, то есть те, которые не являются одними из первых 10.


Запрос = Новый Запрос;
Запрос.Текст = 
"ВЫБРАТЬ
|    ПриходныйКассовыйОрдер.Ссылка КАК Ссылка
|ИЗ
|    Документ.ПриходныйКассовыйОрдер КАК ПриходныйКассовыйОрдер
|ГДЕ
|    НЕ ПриходныйКассовыйОрдер.Ссылка В
|            (ВЫБРАТЬ ПЕРВЫЕ 10
|                Т.Ссылка
|            ИЗ
|                Документ.ПриходныйКассовыйОрдер КАК Т
|            ГДЕ
|                НАЧАЛОПЕРИОДА(Т.Дата, ДЕНЬ) = НАЧАЛОПЕРИОДА(ПриходныйКассовыйОрдер.Дата, ДЕНЬ)
|            УПОРЯДОЧИТЬ ПО
|                Т.Дата)"; // Здесь можно добавить условие по периоду

Проблемы с производительностью и использованием

Как отмечалось в обсуждении, коррелированные подзапросы могут быть крайне неэффективными на больших объемах данных. Это связано с тем, что вложенный запрос выполняется для каждой строки внешнего запроса, что приводит к многократному выполнению запроса к базе данных. На файловой базе данных с 8000 записей такой запрос выполнялся более минуты, что на два порядка медленнее, чем другие подходы. На продуктивном сервере с большим объемом данных это может привести к очень медленной работе и даже "повесить" сервер 1С.

Кроме того, иногда УПОРЯДОЧИТЬ ПО в коррелированных подзапросах может работать непредсказуемо, особенно в различных режимах работы (например, в СКД). Это может привести к тому, что будут оставаться не совсем те 10 документов, которые подразумевались.

Мы настоятельно не рекомендуем использовать коррелированные подзапросы для массовых операций удаления из-за их потенциальных проблем с производительностью и непредсказуемостью.

Дополнительные рекомендации и лучшие практики

Независимо от выбранного подхода, при выполнении массовых операций удаления документов всегда следуйте этим рекомендациям:

  1. Тестирование на копии базы данных: Никогда не запускайте операции массового удаления на продуктивной базе без предварительного тщательного тестирования на ее точной копии. Это поможет выявить ошибки и оценить производительность.
  2. Монопольный режим: Всегда используйте УстановитьМонопольныйРежим(Истина). Это предотвращает одновременное изменение данных другими пользователями и гарантирует целостность операции. После завершения операции не забудьте вернуть обычный режим с помощью УстановитьМонопольныйРежим(Ложь).
  3. Транзакции: Оберните всю операцию удаления в блок НачатьТранзакцию(), ЗафиксироватьТранзакцию(), ОтменитьТранзакцию(). Это обеспечит атомарность: либо все документы будут удалены, либо, в случае ошибки, ни один из них.
  4. Прямое удаление vs. Пометка на удаление: Метод ДокОбъект.Удалить() выполняет непосредственное удаление объекта из базы данных. Альтернативой является ДокОбъект.ПометитьНаУдаление(Истина), который лишь устанавливает пометку. Фактическое удаление в этом случае происходит позже с помощью специальной обработки "Удаление помеченных объектов". Для разовых операций часто предпочтительнее прямое удаление, но при этом необходимо быть уверенным в необратимости операции.
  5. Резервное копирование: Перед началом любой массовой операции убедитесь, что у вас есть актуальная резервная копия базы данных.
  6. Оценка производительности: Для очень больших объемов данных может потребоваться оценка плана запроса и дополнительная оптимизация, например, создание временных индексов.

Выбирая между итерационным подходом (Решение 1) и использованием временных таблиц (Решение 2), мы рекомендуем склоняться к Решению 2 для больших баз данных и объемов удаляемых документов из-за потенциально лучшей производительности на стороне СУБД. Решение 1 является хорошим вариантом для средних объемов данных или когда требуется более гибкая логика обработки в коде 1С. От использования коррелированных подзапросов (Решение 3) для массового удаления лучше воздержаться.

← На главную