Ускорение запросов к СУБД при помощи горизонтального масштабирования




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

50 Comments

  1. Ganjubas

    А где можно ознакомиться с практическими примерами данной методики? И есть ли более развернутый пример с оптимизацией кода (в т.ч. запроса) обработки «Поиск и замена дублей» ?

    Reply
  2. Alien_job

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

    Reply
  3. dmurk

    (2) Alien_job, когда вы выбираете первую тысячу записей, в ней не будет ссылок из второй тысячи записей ))

    Reply
  4. dmurk
  5. ValeriTim

    (2) Alien_job, Ну как бы выбирает из одного индексированного источника …

    Reply
  6. ValeriTim

    А вообще не до конца понятно как же оптимизиорован запрос … конечный результат нужно было в статью … и с кодом

    Reply
  7. ildarovich

    Что я понял: в запросе можно

    1) поделить исходную таблицу на секции приемом «ВЫБРАТЬ ПЕРВЫЕ 100 Х ИЗ (ВЫБРАТЬ ПОСЛЕДНИЕ 1000 Х)»,

    2) обработать секции отдельными подзапросами,

    3) а затем собрать результат юнионами.

    Можно надеяться, что при этом СУБД использует параллелизм и будет быстрее.

    Что я не понял:

    Зачем нужен запрос:

    ВЫБРАТЬ РАЗЛИЧНЫЕ
    Т1.НазначениеСостояния КАК ЗначениеРеквизита
    ИЗ
    Справочник.НазначениеСостояния КАК Т1
    ВНУТРЕННЕЕ СОЕДИНЕНИЕ Справочник.НазначениеСостояния КАК Т2
    ПО T1.НазначениеСостояния = Т2.НазначениеСостояния И
    Т1.Ссылка <> Т2.Ссылка

    Если та же задача решается запросом:

    ВЫБРАТЬ
    НазначениеСостояния
    ИЗ Справочник.НазначениеСостояния
    СГРУППИРОВАТЬ ПО
    НазначениеСостояния
    ИМЕЮЩИЕ КОЛИЧЕСТВО(РАЗЛИЧНЫЕ Ссылка) > 1
    Reply
  8. Alien_job

    (3) Хм.. я имел ввиду другое. Вы же дубли удаляете — уникальность ПО T1.НазначениеСостояния = Т2.НазначениеСостояния. У вас в результате оптимизированной обработки в каждой выбранной секции назначения состояния будут разными. Но после объединения результатов итоговая таблица будет полна «дублей» ПО T1.НазначениеСостояния = Т2.НазначениеСостояния примерно по 28 на каждое назначение состояния

    Reply
  9. e.kogan

    Очень хочется поставить сразу много плюсов за идею.

    Reply
  10. Sheff

    подпишусь

    Reply
  11. Alien_job

    Некорректно сравнивать время работы нормального и «оптимизорованного» подхода т.к. результат разный.

    Reply
  12. v3rter

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

    Reply
  13. starik-2005

    Суть статьи в том, чтобы программисты использовали многопоточность. Это однозначный плюс. Минус в том, что идиотский алгоритм поиска дублей оставили идиотским и не смогли найти решения, которое бы вместо поиска дублей сравнивало не все со всем, а только с правильным. Для 139к Х 2 состояния нужно всего лишь 139к Х 2 сравнения с правильными значениями, которыми и будут заменены все остальные. Для этого достаточно выбрать различные по наименованию ((если у нас наименование является определителем уникальности) — в итоге будет 2 значения, потом выбрать максимумы или минимумы ссылок — без разницы — с данными наименованиям и считать их верными. Дальше найти все неверные и заменить. Никаких 18ккк сравнений не нужно. И это уже показывает «силу» команды внедрятелей, которые дальше совершенно правильных мыслей, которые можно было вычитать даже в моей статье о многопоточности, где я говорил о наличии мощностей на серверах 1С и SQL, которые можно и нужно утилизировать, не пошли. Алгоритмическое мышление — это то, что неприсуще многим специалистам 1С, но ведь надо как-то самосовершенствоваться, учиться, развиваться…

    Reply
  14. dmurk

    (8) Alien_job, именно так. Но сложность задачи уже уменьшена

    Reply
  15. dmurk

    (6) ValeriTim, Боюсь, формат доклада для аудитории не подразумевал многостраничных текстов. Весь проект можно скачать: http://www.dudin.by/Technodemo.zip

    Reply
  16. dmurk

    (12) v3rter, идея комплексная, не упрощайте

    Reply
  17. dmurk

    (7) ildarovich, первая часть кода — написана разработчиками 1С. Действительно, и зачем так писать? )))

    Reply
  18. dmurk

    (7) ildarovich, по второй части кода рекомендую скачать проект и попробовать применить ваш вариант кода

    Reply
  19. dmurk

    (13) starik-2005, вы описываете нерабочий алгоритм. По ВЫБРАТЬ РАЗЛИЧНЫЕ вы получите ПЯТЬ наименований. И что? ))))

    Reply
  20. Alien_job

    (14) Конкретно для ваших данных (тысячи дублей одного и того же) такой подход уменьшит сложность, но если дубли распределены равномерно (по 5 дублей на каждое НазначениеСостояния ) то такой подход ничего не даст

    Reply
  21. dmurk

    (13) starik-2005, применяя МАКСИМУМ или МИНИМУМ ссылок вы можете получить более одной записи в таблице. В случаях когда ссылки получены из разных узлов РИБ максимум и минимум возвращает количество строк идентичное количеству узлов исходной таблицы. Глюк-с платформы, однако, все вопросы разработчикам…

    Reply
  22. dmurk

    (20) Alien_job, безусловно

    Reply
  23. dmurk

    (20) Alien_job, хотя, можно сгенерировать и проверить ))

    Reply
  24. ildarovich

    (18) скачать проект — это как? и зачем это мне?

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

    Reply
  25. bulpi

    Криворукость предыдущей команды программистов попытались исправить чуть менее криворукие. Получилась красивая диаграмка.

    Reply
  26. v3rter

    То есть пока получается эмпирическая оптимизация запроса разбивкой по группам строк и, исходя из размера таблицы, количество процессоров, объем выделенной SQL памяти, предложить относительно оптимальную разбивку не получится?

    Reply
  27. bulpi

    П.С.

    Читайте ildarovich , кланяйтесь, и благодарите.

    Reply
  28. ildarovich

    +(24) Кстати, по ссылке из (15) вместо проекта — сообщение

    404 — файл или каталог не найден.

    Запрашиваемый ресурс перемещен, переименован либо временно недоступен.
    Reply
  29. Alien_job

    (28) ildarovich, на 10-й раз загрузка пошла. Если постараетесь, возможно получится скачать

    Reply
  30. ildarovich

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

    Reply
  31. dmurk

    (28) ildarovich, ошибся, бывает

    http://www.dudin.by/Tehnodemo.zip

    Reply
  32. dmurk

    (25) bulpi, видимо Ваш гений программирования не имеет желания публиковать свой единственной ровный вариант решения этой проблемы. Зачем что-то обсуждать, если можно обгадить? ))

    Reply
  33. starik-2005

    (21) минимум или максимум возвращают ссылку, которая в SQL — суть число. Какие-то отсылы к РИБ тут совершенно неуместны, ибо это число для разных узлов в принципе одинаково, либо это разные ссылки. И даже если «ВЫБРАТЬ РАЗЛИЧНЫЕ Наименование ИЗ Справочник.Статусы» дает нам пять значений — это не повод сравнивать все со всем, ибо в статье описана четкая задача замены дублей ссылок. Тут не нужны 18ккк сравнений если хоть немного подумать мозгом, но, как я понял, авторы мозгом думать не желают.

    Reply
  34. dmurk

    (33) starik-2005, вы имеете ввиду такое решение?

    «ВЫБРАТЬ

    | СостоянияСогласования.НазначениеСостояния,

    | МИНИМУМ(СостоянияСогласования2.Ссылка) КАК СсылкаМин

    |ИЗ

    | Справочник.СостоянияСогласования КАК СостоянияСогласования

    | ЛЕВОЕ СОЕДИНЕНИЕ Справочник.СостоянияСогласования КАК СостоянияСогласования2

    | ПО СостоянияСогласования.НазначениеСостояния = СостоянияСогласования2.НазначениеСостояния

    |

    |СГРУППИРОВАТЬ ПО

    | СостоянияСогласования.НазначениеСостояния

    |

    |ИМЕЮЩИЕ

    | КОЛИЧЕСТВО(РАЗЛИЧНЫЕ СостоянияСогласования2.Ссылка) > 1″;

    Reply
  35. dmurk

    (26) v3rter, так как я не ставил себе задач по коммерческому применению методики, то да, оптимизировано под конкретный сервер.

    Reply
  36. starik-2005

    (34) если поле «НазначениеСостояния» является уникальным, то и представленное Вами решение может подойти, полагаю, для чего-нибудь. Но если задача — это найти все «неправильные» статусы и заменить их на произвольно выбранные «правильные», то даже такого не надо — достаточно на первом шаге отобрать уникальные, на втором шаге найти минимум или максимум ссылки, чтобы привязать правильное к одной ссылке из множества. На третьем шаге найти все, отличные от «правильных» и заменить их на «правильные», сопоставив по полю уникальности. Агрегация тут при наличии множества ссылок на одинаковые статусы не нужна, на выходе (139к — количество_уникальных_статусов) неверных ссылок, которые уже можно искать в системе и менять. Дальше можно разбить их на примерно равные части и скормить приемлемому количеству потоков для изменения. Если скармливать постепенно, то можно получить примерно равнораспределенную нагрузку, если скормить сразу все, поделив случайным образом, то можно ожидать, что часть заданий отработают раньше что приведет к замедлению обработки.

    Reply
  37. dmurk

    (36) starik-2005, в данной задаче, на практике справочник был заполнен заново двумя элементами, и их ссылки записаны в объекты. Поднятый вопрос — адаптация стандартного ПоискИЗаменаДублей для универсального применения, но в более вменяемые сроки, для того чтобы сопровождение проекта не нагружало отдел разработки типовыми решениями.

    Reply
  38. v3rter

    В любом случае новый приём оптимизации — это хорошо )

    Reply
  39. Dach
    Reply
  40. v3rter

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

    Reply
  41. Dach

    (40) v3rter, считаем, что оно не индексируемое. И таблица не сортированная

    Reply
  42. ildarovich

    (39) Dach, рассуждения нормальные, но только вслед за автором вы пытаетесь слегка поправить решение, дополнить его, решив проблему остающихся дублей. Тогда как, по моему мнению, нужно было кардинально поменять основу — сам алгоритм решения.

    Пусть у вас есть пачки банкнот (лотерейных билетов), среди которых есть фальшивые. Номера фальшивых повторяются. Они — дубли.

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

    Трудоемкость последней операции, очевидно, пропорциональна числу купюр. Такой способ и предлагается в (7).

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

    Reply
  43. Dach

    (42) ildarovich, постойте-постойте.

    Я оперирую низкоуровневым понятием «проход». Это нечто даже более детальное, чем операторы SQL-инструкций. Это именно проход в цикле.

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

    «Чаще всего люди в данном случае либо сортируют купюры, когда их немного, либо заводят таблицу, в которой отмечают встретившиеся номера.»

    Ок. Начинаю перебирать номера. Номер 12345 — встретился 1 раз. Записываю в таблицу.

    Номер 234 — встретился 1 раз.

    *****

    спустя 500 шагов цикла

    Номер 12345! Как узнать, сколько раз он мне встретился? Абстрагируйтесь, плз, от всяких кэшей, соответствий, индексированных таблиц, ибо чтобы ответить на этот вопрос, все равно придется

    перебрать от начала до конца (ну или до тех пор, пока не встретится) весь второй список!

    Так вот, ИМХО, ключевая фраза тут «до тех пор, пока не встретится».

    Как Вы думаете, как сервер на физическом уровне выполняет сортировку? А как группировку? Думаю, что да — как Вы верно подметили удачной аналогией, «записывая номер».

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

    Reply
  44. Dach

    (43) +

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

    ВЫБРАТЬ

    НазначениеСостояния

    ИЗ Справочник.НазначениеСостояния

    СГРУППИРОВАТЬ ПО

    НазначениеСостояния

    ИМЕЮЩИЕ КОЛИЧЕСТВО(РАЗЛИЧНЫЕ Ссылка) > 1

    отработает быстрее, чем алгоритм, предложенный автором. К слову, мне кажется, он не просто так порекомендовал опробировать на реальных данных. Доберусь вечером до ноута — сам проверю в удовольствием

    Reply
  45. ildarovich

    (43) Dach,

    Но это далеко не дает гарантий, что количество проходов будет существенно меньше, чем в моем способе

    Будет существенно меньше. Проход будет ровно один. С гарантией. Посмотрите запрос в (7). Он решает ту же самую задачу. Приведу его еще раз.

    ВЫБРАТЬ
    НазначениеСостояния
    ИЗ Справочник.НазначениеСостояния
    СГРУППИРОВАТЬ ПО
    НазначениеСостояния
    ИМЕЮЩИЕ КОЛИЧЕСТВО(РАЗЛИЧНЫЕ Ссылка) > 1

    Вы думаете, что расчет агрегата КОЛИЧЕСТВО(РАЗЛИЧНЫЕ потребует нескольких проходов по таблице и поиска? — Тогда вы ошибаетесь.

    (44) Отработает ГОРАЗДО БЫСТРЕЕ — на два порядка, меньше, чем за секунду, скорее всего, то есть быстрее примерно в 200 раз.

    Reply
  46. Dach

    (45) ildarovich, один проход по таблице-источнику — это ок, согласен.

    Для того, чтобы узнать, сколько раз в таблице-источнике встретился дубль — мы заносим анализируемое поле во второй список. Со временем второй список растет.

    Ответьте на один вопрос, когда встречается дубль — для того, чтобы понять — дубль он (или «трубль» или «четверубль» ;))) ) — Вы будете заглядывать во второй список? Вы будете его перебирать или нет? Мне кажется да, потому что как иначе? Ну и в чем тогда принципиальное отличие от джойна на физическом уровне?

    Да только во фразе «буду перебирать весь второй список, пока не найду искомый элемент, чтобы понять, сколько он раз встречался в первой таблице»

    То есть там в цикле стоит «Прервать», грубо говоря. Вот и все.

    Ну и если во втором списке наша строка сидит в самом конце — разве мне его весь не придется просмотреть?

    Разве не логично рассуждаю?

    Reply
  47. ildarovich

    (46) Dach, агрегатные функции считаются с использованием хэшей. Грубо говоря, найденные номера купюр заносятся в соответствие. В этом случае нет необходимости перебора списка ранее встретившихся элементов. Обращение к соответствию — это простая и быстрая операция.

    Reply
  48. Dach

    (47) ildarovich, то есть второй список хэшируется и индексируется? Ну, тогда вопросов нет.

    Однако, способ рабочий, если возможностей использовать соответствие нет

    Reply
  49. ildarovich

    (48) Dach,

    если возможностей использовать соответствие нет

    соответствие я привел для удобства объяснения. План запроса может и сортировку использовать и индекс-скан. Но все это будет еще быстрее или не медленнее. Квадратичной зависимости в этой задаче не должно быть, если специально не накосячить.

    Способ рабочий? — А есть ли вообще этот способ, если нет задачи?

    Reply
  50. Yashazz

    (9) e.kogan, этой идее сто лет в обед.

    Reply

Leave a Comment

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