Еще один способ расчета остатков на каждый день в запросе




Принцип обмена данными из 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='\

20 Comments

  1. AzagTot

    Профессор Руби зачетный чел)

    Reply
  2. bulpi

    Красиво.

    Смущают функции Максимум(Было), Максимум(Стало)

    Предполагается, что в запросе функция МАКСИМУМ (NULL,1)=1 . А это точно так ?

    Reply
  3. GROOVY

    Меня все смущает. Зачем два раза дергать таблицу? Зачем в текст добавлять таблицы «Повторяется Х раз»?

    Достаточно выборку сделать с тета-соединением.

    В выборке, конечно, дат с пустыми значениями мы не получим, но и в выборке из результата запроса и в СКД можно добавить пустые значения. А для прямой выгрузки в ТЗ сомнительный механизм, выборка в общем случае будет эффективнее.

    Reply
  4. ildarovich

    (2) bulpi, запрос я протестировал, он работает. Мне кажется такое поведение по отношению к NULL ожидаемым.

    Да и вдокументации по T-SQL написано:

    При выполнении функции MAX все значения NULL пропускаются.

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

    Reply
  5. ildarovich

    (3) GROOVY,

    зачем два раза дергать таблицу

    Если вы про начало запроса, то там это делается, чтобы дополнить нулями начальные значения. Иначе в начале интервала будут пропуски. А так вся номенклатура, имеющая ненулевые остатки на периоде, даст строки с нулями в начале периода, чтобы, если с них начался интервал, распространить нули дальше. Но конечно, можно было бы «дергать таблицу один раз», выбрав дополнения нулями уже из выдернутой таблицы, это я не сделал, чтобы сократить эту часть до максимума. Не в ней суть.

    Повторяется Х раз написано потому, что на самом деле этот фрагмент нужно повторить в тексте запроса 5 раз для интервала из 32 дней или 10 раз для интервала 1024 дней. Если бы записать запрос как есть со всеми повторами, то он был бы слишком громоздким и плохо обозримым. Это на самом деле громоздкий, но эффективный запрос. Соответствует картинке. С повторяющейся частью.

    Кстати, если вы скачаете обработку, то увидите, что в СКД запрос написан также, а разворачивается только перед выполнением.

    Я согласен, что заполнять пропуски можно постобработкой результата, этот спор уже был. Но все же есть задачи, где результат заполнения нужен внутри запроса для последующей обработки.

    Например, для расчета пени по величине долга. Могу еще привести примеры. И вообще я против того, чтобы вместо решения задачи критиковать саму ее постановку.

    Тэта-соединения дают трудоемкость O(NxN), а здесь — O(N) (в лучшем случае). Запросы с тэта-соединениями известны. А это ЕЩЕ ОДИН способ, «красивый».

    Reply
  6. dgolovanov

    Много статей читаю на ИС, в т.ч. уважаемого ildarovich. Непременно в каждой найдется некто с вопросом «а нахрена это?», «сделай в СКД встроенными функциями» и вершина знаний — тэта-соединение с его чудовищной неэффективностью. Это не камень в сторону уважаемого GROOVY, которому я очень благодарен за пройденные курсы. Но тенденция явная. А потом обижаемся на анекдоты типа «На паралимпиаду по программированию приглашаются программисты на 1С, РНР и VBA».

    Reply
  7. lrs

    В целом идея очевидная, распространять остаток предыдущего дня на следующий. Фишка в последовательности 1,2,4,8,16…. позволяющая на порядок сократить циклы обработки данных! До этого конечно не каждый додумается))

    Reply
  8. ildarovich

    +(4) Вот еще один вариант повторяющегося фрагмента c использованием Left Anti Semi Join. Он несколько понятнее и короче, но не уверен, что на всех СУБД будет использован правильный план запроса:

    
    
    Reply
  9. awk

    Если текст запроса динамический, то почему не использовать такой объект как «Схема запроса»?

    Reply
  10. ildarovich

    (9) awk, думал над этим, но так (по-старинке) кода получается существенно меньше.

    Четыре СтрЗаменить:

    1) Шаг0 -> ШагХ-1; 2) Шаг1->ШагХ;(в повторе) 3) &Шаг -> Х; 4) Шаг1->ШагХ (в эпилоге).

    Причем первые три одной строкой.

    Вот, собственно, код:

    ТекстЗапроса = СхемаКомпоновкиДанных.НаборыДанных.НаборДанных1.Запрос;
    Секции = ЧастиТекста(ТекстЗапроса, «;»);
    ТекстЗапроса = Секции[0] + «;»; //исходная выборка
    НомерТура = 0;
    Охват = 1;
    Пока Охват * 86400 <= КонецПериода — НачалоПериода Цикл
    ТекстЗапроса = ТекстЗапроса + СтрЗаменить(СтрЗаменить(СтрЗаменить(Секции[1]
    , «Шаг1», «Шаг» + (НомерТура + 1)), «Шаг0», «Шаг» + НомерТура), «&Шаг», Охват) + «;»;
    НомерТура = НомерТура + 1;
    Охват = 2 * Охват
    КонецЦикла;
    ТекстЗапроса = ТекстЗапроса + СтрЗаменить(Секции[2], «Шаг1», «Шаг» + НомерТура); //итоговая выборка
    СхемаКомпоновкиДанных.НаборыДанных.НаборДанных1.Запрос = ТекстЗапроса;

    Показать

    Но вообще попробовать можно…

    Reply
  11. alon

    Я так понимаю, что на начало периода в регистре не должно быть отрицательных остатков. Иначе ноль перебьет реальное значение в функции максимум.

    Reply
  12. ildarovich

    (11) alon, вы совершенно правы, упустил этот момент. Для остатков агрегатная функция могла быть суммой. А для регистров сведений — максимумом. Когда решил не писать два разных варианта, про необходимость входной группировки по сумме в случае остатков не вспомнил.

    Чуть позже уточню запросы в статье и в обработке.

    Спасибо за вашу внимательность.

    Reply
  13. awk

    (10) Я про объект конфигурации «Схема запроса», а не про СКД.

    Reply
  14. ildarovich

    (13) awk, да понял я, понял. Про это и отвечал. Хотя и сомневаюсь, что выйдет короче, чем в (10), но попробую для интереса применить для распаковки код типа

    СхемаЗапроса = Новый СхемаЗапроса;
    СхемаЗапроса.УстановитьТекстЗапроса(Запрос.Текст);
    …
    Запрос.Текст = СхемаЗапроса.ПолучитьТекстЗапроса();

    Но вообще-то прямого отношения к обсуждаемому решению это не имеет.

    Reply
  15. Ovrfox

    Ребята, почему никто из Вас не подумал над применимостью данного запроса. Он вообще никому не нужен!

    Если данные сильно разреженные и остатки считаются за несколько милисекунд, то кто вообще будет оптимизировать запрос? Тем более под специально разреженные данные? Ведь и так все быстро работает.

    Если сравнить данный метод с любым другим, который выбирает остатки из бд один раз, а потом распространяет их любым способом, без обращения к перерасчету остатков, то что мы получим?

    Худший метод замедлит выдачу максимум на пару секунд, если кто-то додумается выбрать остатки второй раз с помощью конструкции «ОстаткиИОбороты» то такое обращение на более — менее больших БД замедлит выдачу рещультата значительно, в большинстве случаев вдвое. Т.к. для меня «Большие БД» это те, где подобная конструкция «ОстаткиИОбороты» считается значительный промежуток времени (пару секунд) и ты начинаешь оптимизировать отчеты, избегая данной конструкции.

    Итого — как красивое, хоть и абсолютно бесполезное , данное решение имеет право на жизнь. В теоретических книгах. Но на практике 100% не применимо.

    Reply
  16. ildarovich

    (15) Ovrfox, готов с вами поспорить. Хотя не все понял в ваших рассуждениях. А вы все поняли в моих? -Давайте попробуем понять друг друга!

    Итак, с чем я спорю:

    Он (способ) вообще никому не нужен! … на практике 100% не применимо

    Я спорю по поводу 100%. Могу согласиться «на практике 90% не применимо», «на практике 95% не применимо» и тому подобное. Вот для этих 5-10% случаев способ применим, поэтому лучше о нем знать, чем не знать.

    Теперь, что я не понял: вы считаете, что способ выбирает ОстаткиИОбороты принципиально два раза? — но это совсем не так. Исходную таблицу для работы метода можно получить любым способом, каким быстрее:

    1) объединив остатки на начало периода (решив вопрос с нулевыми) и остатки на дни с оборотами внутри периода;

    2) сделав выборку остатков внутри периода и выбрать дополнения нулями из нее;

    3) объединив номенклатуру, имеющую обороты, выбранную из остатков и оборотов и остатки внутри периода (как сделано у меня).

    Не думаю, что между этими способами есть принципиальная разница по скорости — порядок цифр один и тот же.

    Вы считаете, что выигрыш будет только при разреженной таблице (кстати, это частый случай, легко посмотреть статистику по всем товарам, разделив дни с движениями на длину интервала в днях)? — Но это тоже не так. При максимально разреженной таблице выигрыш будет максимальным. Если таблица уже заполнена, то выигрыш будет меньше, но все равно будет из-за перехода от NxN к Nxlog(N). При плотно заполненной таблице и классические методы тоже будут существенно медленнее.

    Не понял какое ускорение вы считаете существенным: две секунды уже хорошо? То есть мне для доказательства практичности нужно найти случаи с выигрышем более двух секунд?

    А поняли ли вы, что подобный прием работает и для регистров сведений, где и разреженность чаще встречается и «остатки и обороты» (роль этой таблицы играет физическая таблица регистра сведений) вообще никак не обойти?

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

    Reply
  17. ildarovich

    + (16) Замечание в (15) заставило повнимательнее отнестись к анализу эффективности предложенного метода. Дополнительные исследования показали, что:

    1) В рассуждениях о сравнительной эффективности «домино» и «классики» была неточность.

    Reply
  18. Prometeus2011

    Попросили обработку написать для вычисления штрафов работникам по упущенной выгоде. Чтоб было вообще ФАААЯЯЯЯ, решил применить способ Сергея.

    В процессе применения выяснилось, что придется посчитать двоичный логарифм средствами 1с, т.к. «Для заполнения интервала из 32 дней достаточно пятикратного , а для интервала из 1024 дней — десятикратного повторения ключевого фрагмента».

    Навскидку не нашел в 1с функции вычисления двоичного логарифма (Двоичный логарифм — степень, в которую нужно возвести число 2, чтобы получилось заданное число).

    Написал маленький кусочек кода для его вычисления:

    КолДней = 31;  //Дни, за которые необходимо остатки посчитать (32, 1024 …)
    КолИтераций = 0;
    Рез = 1;
    Пока Рез<КолДней цикл
    КолИтераций = КолИтераций + 1;
    РЕз = Рез*2;
    Если РЕз>КолДней тогда
    ПРервать;
    КонецЕсли;
    КонецЦикла;
    //КолИтераций  — Количество повторений среднего участка запроса.
    

    Показать

    Reply
  19. Ovrfox

    (18) Prometeus2011,

    А зачем?

    При формировании запроса в цикле

    МаксДней = 1;

    Пока МаксДней <= РазницаДат Цикл

    ТекстЗапроса = ТекстЗапроса + //Запрос цикла для максДней

    МаксДней = МаксДней*2;

    КонецЦикла;

    Таким образом не нужно считать заранее к-во шагов для удвоения дат.

    Reply
  20. ra-it

    Использовали похожий метод для поиска дебиторских документов по фифо (в соседней ветке где-то видел описание). Работает сильно быстрее (много документов). Но нужно иметь ввиду особенность СКД, если ей скармливать полностью запрос (у нас запрос вышел 9600 строк) — при некоторых настройках группировок СКД считает, что «раз эти поля не используются в выводе, то соптимизирую-ка я такой большой запрос, и не буду выполнять ненужные пакеты». В итоге получаем искаженные данные. Пришлось ставить признак «Обязательное» для всех полей компоновки, дабы пресечь волюнтаризм платформы и получить корректный результат 🙂

    Reply

Leave a Comment

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