T-SQL + 1С: как правильно удалять очень много записей




Принцип обмена данными из 1С с сайтом (на MySQL) и выдачи (публикации) этих данных по запросу.
PHP-Скрипт автоматической загрузки данных из файла данных в формате CSV в базу данных сайта работающего на WordPress.

В продолжение моей темы: 1С:Альфа-Авто Автосалон Автосервис: обмен с сайтом.
С помощью данного скрипта можно загружать в автоматическом режиме, по расписанию, данные сервисных книжек (ремонтов авто) из 1С:Альфа-Авто Автосалон Автосервис.
Также можно загружать данные в ручном режиме: для этого делается скрытая страница, где размещается специальная кнопка.
Комментарии размещенные внутри скрипта разъяснят логику и порядок действия.
Комментарии с "/////    echo" использовались для отладки.
Дополнительно создана таблица для журналирования результатов загрузки данных.
Скрипт включает в себя защиту от SQL инъекций (думаю безопасность соблюдена в полной мере).
В кратце:
1. Пишется скрипт, который запускает этот.
2. Создается регламентное задание в WordPress, по которому запускается скрипт из п.1. 
3. Этот скрипт осуществляет проверку на существование файла обмена в папке.
4. Если данные не новые, загрузка не производится.
5. Если данные новые, очищается таблица сервисных книжек.
6. Загружаются новые данные.

Собственно сам скрипт:

<?php // Полная загрузка сервисных книжек, создан 2024-01-05 12:44:55

global $wpdb2;
global $failure;
global $file_hist;

/////  echo '<H2><b>Старт загрузки</b></H2><br>';

$failure=FALSE;
//подключаемся к базе
$wpdb2 = include_once 'connection.php'; ; // подключаемся к MySQL
// если не удалось подключиться, и нужно оборвать PHP с сообщением об этой ошибке
if (!empty($wpdb2->error))
{
/////   echo '<H2><b>Ошибка подключения к БД, завершение.</b></H2><br>';
$failure=TRUE;
wp_die( $wpdb2->error );
}

$m_size_file=0;
$m_mtime_file=0;
$m_comment='';
/////проверка существования файлов выгрузки из 1С
////файл выгрузки сервисных книжек
$file_hist = ABSPATH.'/_1c_alfa_exchange/AA_hist.csv';
if (!file_exists($file_hist))
{
/////   echo '<H2><b>Файл обмена с сервисными книжками не существует.</b></H2><br>';
$m_comment='Файл обмена с сервисными книжками не существует';
$failure=TRUE;
}

/////инициируем таблицу лога
/////если не существует файла то возврат и ничего не делаем
if ($failure){
///включает защиту от SQL инъекций и данные можно передавать как есть, например: $_GET['foo']
/////   echo '<H2><b>Попытка вставить запись в лог таблицу</b></H2><br>';
$insert_fail_zapros=$wpdb2->insert('vin_logs', array('time_stamp'=>time(),'last_mtime_upload'=>$m_mtime_file,'last_size_upload'=>$m_size_file,'comment'=>$m_comment));
wp_die();
/////    echo '<H2><b>Возврат в начало.</b></H2><br>';
return $failure;
}
/////проверка лога загрузки, что бы не загружать тоже самое
$masiv_data_file=stat($file_hist);   ////передаем в массив свойство файла
$m_size_file=$masiv_data_file[7];    ////получаем размер файла
$m_mtime_file=$masiv_data_file[9];   ////получаем дату модификации файла
////создаем запрос на получение последней удачной загрузки
////выбираем по штампу времени создания (редактирования) файла загрузки AA_hist.csv, $m_mtime_file

/////   echo '<H2><b>Размер файла: '.$m_size_file.'</b></H2><br>';
/////   echo '<H2><b>Штамп времени файла: '.$m_mtime_file.'</b></H2><br>';
/////   echo '<H2><b>Формирование запроса на выборку из лога</b></H2><br>';
////препарируем запрос
$text_zaprosa=$wpdb2->prepare("SELECT * FROM `vin_logs` WHERE `last_mtime_upload` = %s", $m_mtime_file);
$results=$wpdb2->get_results($text_zaprosa);

if ($results)
{   foreach ( $results as $r)
{
////если штамп времени и размер файла совпадают, возврат
if (($r->last_mtime_upload==$m_mtime_file) && ($r->last_size_upload==$m_size_file))
{////echo '<H2><b>Возврат в начало, т.к. найдена запись в логе.</b></H2><br>';
$insert_fail_zapros=$wpdb2->insert('vin_logs', array('time_stamp'=>time(),'last_mtime_upload'=>$m_mtime_file,'last_size_upload'=>$m_size_file,'comment'=>'Загрузка отменена, новых данных нет, т.к. найдена запись в логе.'));
wp_die();
return $failure;
}
}
}
////если данные новые, пишем в лог запись о начале загрузки
/////echo '<H2><b>Попытка вставить запись о начале загрузки в лог таблицу</b></H2><br>';
$insert_fail_zapros=$wpdb2->insert('vin_logs', array('time_stamp'=>time(),'last_mtime_upload'=>0, 'last_size_upload'=>$m_size_file, 'comment'=>'Начало загрузки'));

////очищаем таблицу
$clear_tbl_zap=$wpdb2->prepare("TRUNCATE TABLE %s", 'vin_history');
$clear_tbl_zap_repl=str_replace("'","`",$clear_tbl_zap);
$results=$wpdb2->query($clear_tbl_zap_repl);
/////   echo '<H2><b>Очистка таблицы сервисных книжек</b></H2><br>';
if (empty($results))
{
/////   echo '<H2><b>Ошибка очистки таблицы книжек, завершение.</b></H2><br>';
//// если очистка не удалась, возврат
$failure=TRUE;
wp_die();
return $failure;
}

////загружаем данные
$table='vin_history';         // Имя таблицы для импорта
//$file_hist Имя CSV файла, откуда берется информация     // (путь от корня web-сервера)
$delim=';';          // Разделитель полей в CSV файле
$enclosed='"';      // Кавычки для содержимого полей
$escaped='\

39 Comments

  1. ture

    select 1

    while(@@ROWCOUNT>0)

    DELETE TOP(500) [_AccumRg1234] WHERE <…>;

    Ты был близок!

    Если все то транкейт.

    Если мало оставить, то копируй малую часть во времянку, транкейти, и копируй назад.

    Reply
  2. МихаилМ

    автор ни слова не сказал о кластером индексе либо о дополнительном индексировании.

    Reply
  3. zhichkin

    (2) Я прошу прощения, но я не понял Вашего комментария. У Вас какой-то вопрос, который Вы решили не задавать? Или у Вас есть уточняющая публикацию информация, которой Вы решили не делиться?

    По основному тексту статьи с индексами делать ничего не надо — свёртка выполняется «на бою». С таблицей работают пользователи. Фрагментация индекса неизбежна. Для этого существуют регламентные задания и прочее. Можно ещё поговорить о логах SQL Server и других сопутствующих темах …

    По поводу варианта с использованием TRUNCATE, да — там есть такая тема. Индексы на таблице save создавать вообще не нужно. На основной таблице, как вариант, можно их дропнуть и создать потом заново. Не стал об этом писать, так как основной мотив статьи — свёртка «на бою» параллельно основной работе пользователей.

    Reply
  4. ture

    (3) ээ-э.. индексы?

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

    …запрос не продолжается, а всякий раз начинается с начала (но это весчь очевидная)

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

    Reply
  5. МихаилМ

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

    поэтому все рекомендации относятся для случая , когда удаление записей нужно сделать

    по условию, не соответствующему кластерному индексу.

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

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

    Reply
  6. zhichkin

    (5) Идея понятна. Спасибо за дополнение.

    Честно говоря, мне даже в голову не пришло, что что-то можно удалять не по индексу =)

    Кстати сказать, пример в публикации использует отбор по полю _Period, которое является первым полем кластерного индекса для регистров накопления. В данном случае кластерный индекс будет использован.

    Reply
  7. Painted

    Конструкцию

    INSERT [_AccumRg1234_Save]

    SELECT * FROM [_AccumRg1234] WHERE [_Period] >= @Period;

    можно заменить на

    SELECT *

    INTO [_AccumRg1234_Save]

    FROM [_AccumRg1234] WHERE [_Period] >= @Period;

    которая работает, как будто бы, быстрее.

    Reply
  8. zhichkin

    (7) Зачем гадать что быстрее ? Планы запросов в студию …

    Reply
  9. Painted

    (8)

    Зачем гадать что быстрее ?

    С чего вы взяли, что я гадаю? Я не гадаю, я читаю обзоры.

    Даже не целиком обзоры, а лишь ключевые фразы.

    Пример ключевой фразы «As we can see the SELECT…INTO was considerably faster 489ms compared to 3241ms.» ))

    Reply
  10. zhichkin

    (9) Извините, но Вы написали: «которая работает, как будто бы, быстрее». Это выглядит как неуверенность в своих словах.

    Эта ссылка и объяснения показались мне более понятными и обстоятельными: http://stackoverflow.com/questions/6947983/insert-into-vs-select-into. Можно сказать, что Вы правы … частично, так как таблица [_AccumRg1234_Save] в моём примере не является временной. Если использовать временную таблицу, как Вы предлагаете, то нужно весь код удаления в обязательном порядке заключить в одну транзакцию между BEGIN TRANSACTION и COMMIT TRANSACTION. В примере этого нет, и это не просто так. Если выполнение программы вдруг «упадёт» после TRUNCATE, то в моём примере данные останутся в таблице [_AccumRg1234_Save] и не будут потеряны.

    Reply
  11. dotPRICE.ru

    Наверное, можно оптимизировать код и убрать 1 лишний цикл:

    DECLARE @RowsToDelete int = 4000;

    DECLARE @RowsAffected int = @RowsToDelete;

    WHILE @RowsAffected = @RowsToDelete



    Reply
  12. zhichkin

    (11) Простите, но я как-то с утра не могу понять где лишний цикл ? Итерация, наверное, точнее сказать …

    Reply
  13. dotPRICE.ru

    Если @RowsAffected > 0 но < @RowsToDelete, следущая итерация удалит нулевое кол-во записей.

    Reply
  14. zhichkin

    (13) Удаляем 5000 записей.

    1. итерация: удалили 4000, осталась 1000.

    2. итерация: удалили 1000, осталось 0.

    3. итерация: @RowsAffected = 1000, удаляем 0 записей …

    Вы правы …

    Однако, а что если, пока мы удаляем записи, другая транзакция их туда добавляет ?

    При определённых условиях этот цикл может быть вечным … =)

    Reply
  15. dotPRICE.ru

    (14) а при WHILE @RowsAffected > 0 он будет вечным + 1

    🙂

    Reply
  16. zhichkin

    (15) Чтобы расставить все точки над i:

    при WHILE @RowsAffected = @RowsToDelete цикл не будет вечным и после его выполнения в базе могут остаться записи, которые подходят под условие удаления.

    Мой пример был взят из боевой базы. Первая версия была вообще такая: WHILE EXISTS(SEL ECT 1 FR OM … отсюда и такая логика.

    Reply
  17. dotPRICE.ru

    (16) Все правильно. Выбор варианта зависит от задачи. 🙂

    Reply
  18. nofx

    А такое можно провернуть если таблица ( неподчиненный регистр) учавствует в РИБе?

    После truncate получится, что в соответсвующей таблице изменения (_ChnRg которая) будут записи на не существующие записи основной таблицы?

    Reply
  19. zhichkin

    (18) Всё верно. Удалять записи при помощи TRUNCATE нужно в обеих таблицах: основной и регистрации изменений. А в чём вопрос ?

    Reply
  20. nofx

    Да, собственно, имеется РИБ (порядка 30 баз) на 8.2. Одна таблица отключается из состава обмена, есть мысль её удалить на sql напрямую. Вопрос — обязательно таблицу изменений еще и чистить, её ведь платформа удалить и так должна. И еще вопрос — если удалить основные записи, а изменения нет, при функционирующих обменах с ней (еще), в таблицах изменения останутся ссылки на несуществующие строки видимо.(?) Как это отразится на обменах?

    Reply
  21. zhichkin

    (20) Добрый день, Сергей!

    В Вашем случае все операции типовые — ничего удалять в SQL напрямую не нужно.

    Таблица изменений объекта будет удалена автоматически платформой 1С в том случае, когда объект будет исключён из состава всех планов обмена, в которые он входит.

    При удалении объектов, они регистрируются в таблице обмена и выгружаются как тип данных УдалениеОбъекта — по сути это контейнер для хранения ссылки или ключа записи удалённого объекта.

    Reply
  22. nofx

    Немного не так. Сама таблица чистится (большая её часть, но не вся) чтобы её реструктуризация быстро прошла в последующем обновлении (в неё добавляется колонка и она исключается из обмена). Сама таблица в РИБе, функционирует обмен данными. Скорее всего, будет сделан способ c truncate через вспомогательную таблицу (вот). Вопрос — обмены не остановятся, битых ссылок не понаделается? Нужно ли еще удалять записи в другой таблице (пока она еще есть в базе)? Вот это непонятно мне…

    Reply
  23. triviumfan

    (21) Дмитрий, подскажите, пожалуйста, что за это за зверь «контейнер» (УдалениеОбъекта) и для чего он служит (имеется ввиду в обменах типовых конфигураций)?

    Вот удалил я объект в источнике, далее он регистрируется в таблице изменений как «Объект удалён <УИД>». Что должно произойти после совершения обмена? Удаляться соответствия на эту ссылку в базе приёмнике?

    Reply
  24. zhichkin

    (23) УдалениеОбъекта (встроенная справка 1С):

    Предназначен для удаления объекта базы данных. Объект хранит ссылку на объект базы данных и при обращении к методу Записать() производит удаление объекта базы данных, на который показывает ссылка. Кроме того, данный объект имеет свойство ОбменДанными, которое содержит параметры обмена данными, используемые при удалении объекта, на который показывает ссылка. Основное назначение данного объекта — переносить удаление объектов базы данных при использовании в составе других механизмов обмена данными.
    Reply
  25. zhichkin

    (22) Стало быть вопрос в реструктуризации. Так бы сразу и сказали.

    Можно для начала почитать это: Ускорение реструктуризации таблиц

    и это: Миллионы строк в таблицах 1С? Быстрая реструктуризация — не проблема!

    Reply
  26. nofx

    Нет, вопрос не в реструктуризации.Еще раз процитирую мой вопрос: «…обмены не остановятся, битых ссылок не понаделается? Нужно ли еще удалять записи в другой таблице (пока она еще есть в базе)?» Под «другой» таблицей имеется ввиду таблица изменений.

    Reply
  27. zhichkin

    (26) Задача не понятна. Удаляем записи ради чего ? Свёртка ?

    Я понял так: Вы исключаете объект из состава плана обмена, который является единственным, в котором этот объект участвует. В таком случае платформа сама всё за Вас сделает: удалит регистрацию изменений для этого объекта. Затем возник вопрос добавления нового реквизита и реструктуризации. По этому поводу я дал ссылки.

    Если всё-таки есть желание поработать на уровне SQL, то отвечаю на вопросы:

    Обмены не остановятся ?

    Непонятна суть вопроса. Если вопрос про блокировки и прочее, то зависит от уровня изоляции и гранулярности налагаемых блокировок. Для меня термин «обмены остановились» слишком широк. Нужна конкретика.

    Битых ссылок не понаделается ?

    Если не выполнять контроля ссылочной целостности, то конечно же понаделается.

    Нужно ли удалять записи в таблице изменений ?

    Ну конечно же нужно. Это тоже касается контроля ссылочной целостности базы.

    Reply
  28. triviumfan

    (24) Но ведь удаление объекта не происходит, если в источнике его удалили и был выполнен обмен =\r

    Тем более, если в приёмнике имеются ссылки на него.

    Reply
  29. zhichkin

    (28) Дело происходит так:

    1. В источнике удалили объект.

    2. В источнике в таблице регистрации изменений зарегистрировалась ссылка на удалённый объект.

    3. При выгрузке из источника платформа видит, что ссылка в таблице регистрации изменений «битая» — объект-то удалили. Такую ссылку она выгружает, «завернув» её в УдалениеОбъекта.

    4. Приёмник принимает УдалениеОбъекта и знает, что внутри ссылка, которая была удалена в источнике.

    Вот для такого сценария в основном и нужен тип «УдалениеОбъекта».

    Reply
  30. nofx

    Спасибо за ответы.

    Задам еще один: Что будет, если в базе удалить «напрямую» данные из независимого регистра сведений (без регистратора, не имеющего внешних ссылок),и на момент удаления эта таблица имеет «зарегистрированные изменения» в плане обмена. То есть, генерации типа «Удаление объекта» не будет. Как себя поведет 1С при загрузке,выгрузке данных? Что в таком случае придет в «приемник»?

    Reply
  31. triviumfan

    (29) подскажите, пожалуйста, что происходит после приема такого рода сообщения в приёмнике? Не могу в бсп (1.2.х) разобраться.

    Reply
  32. zhichkin

    (30) В таком случае регистрация изменений останется. Источник выгрузит в целевую базу объекты типа УдалениеОбъекта. Приёмник примет их и удалит у себя. Дело в том, что УдалениеОбъекта генерируется при выгрузке. Логика примерно такая: у меня есть изменение, ищу объект по этому изменению в основной таблице, объекта нет — выгружаю УдалениеОбъекта.

    Reply
  33. zhichkin

    (31) Происходит удаление объекта по ссылке или ключу записи.

    Reply
  34. nofx

    Ага, спасибо за информацию. Соответсвенно, при массовом удалении будет «куча» таких объектов в обменах.

    Reply
  35. triviumfan

    (33) Без проверки ссылочной целостности???

    Reply
  36. zhichkin

    (35) Конечно же с проверками средствами самой платформы 1С.

    Reply
  37. zhichkin

    (34) Да. Всё верно.

    Reply
  38. nofx

    Попробовал это проверить. Удалил, выгрузил из источника, посмотрел xml пакет, а там ничего нового не видно.(1С 8.2.19)

    Reply
  39. zhichkin

    (38) Чтобы понять что и как нужно показать состояние системы до, показать код и состояние системы после выполнения кода. Так я Вам ничего не скажу.

    Reply

Leave a Comment

Ваш адрес email не будет опубликован. Обязательные поля помечены *