Closed LecronRu closed 4 days ago
Да, согласен, надо приделать. Сам когда-то этой штукой пользовался.
Пообщался сегодня с executemany, и вообще расхотелось добавлять это в noorm. Всего-то мне нужно было запихнуть жалкий миллион маленьких записей в Постгрес через psycopg2. Выяснилось, что у себя под капотом executemany тупо крутит цикл и скармливает записи в сервер по одной. Ожидаемое время на миллионе записей было что-то вроде 2 часов. Оказалось быстрее заимплементить честный батчинг и насладиться тем, что процесс занял меньше минуты.
Это я к тому, что на маленьком количестве "many" сделать в цикле через одиночный "execute" получается проще и нагляднее. А на большом "many" лучше этим самым executemany вообще не пользоваться, а выкручиваться как-то по-другому.
Этот executemany обладает мерзким свойством – он эффективно прячет от глаз разработчика ситуацию "запрос в цикле", и поэтому автоматически попадает в графу "уродливые опасные антипаттерны". Добавлять это в noorm, тем самым провоцируя пользователя библиотеки на использование этой дряни, нет никакого желания. Мир будет чуточку чище, если в нём будет меньше хитро спрятанных трудноидентифицируемых косяков.
Кстати, по итогу наших с вами обсужденй в #6 родилась идея, как ничего не ломая таки сделать удобняшку, уменьшающую бойлерплейт: #8
Не стоит делать вывод о поведении функции, по ее реализации в одном из драйверов.
Вставка в in-memory sqlite3 миллиона записей
'INSERT INTO test (text, data) VALUES (?, ?)'
1.22 сек executemany
1.88 сек execute в цикле
Плюс оверхед от noorm на миллион вызовов оберток. Впрочем согласен, при записе в файловую систему, с учетом нечастых случаев пакетной вставки таких объемов, он не столь заметен. Больше заметно неудобство, когда вставляемые данные уже́ в списке/генераторе. Напрашивается передача списка как аргумента.
Там не только оверхед от noorm, но и то, что, насколько я понимаю, в случае с executemany SQL парсится один раз в "prepared statement". В любом случае, 55% добавки погоды не делает.
Фундаментальная проблема executemany, по-видимому, проистекает из того, что невозможно сделать автоматический универсальный батчинг, надёжно работающий во всех мыслимых случаях жизни. Соответственно, даже считающийся эталоном psycopg2 не пытается занматься самодеятельностью и тупо крутит цикл. Что, конечно, работает, но если база где-то там далеко в облаке, миллион последовательно проделываемых I/O операций через сеть это капец как долго.
Если вставляемые данные в списке или генераторе, просто кладём insert под цикл. Всего одна доп. строчка. Зато сразу видно, что вот тут у нас запрос в цикле, что потенциально мощный тормоз.
Decided not to implement. Issue is closed.
DML запрос может использоваться как для одиночного исполнения, так и для порции данных. Поэтому не представляю, как это реализовать в рамках API вашей библиотеки.
Если параметры отдельные переменные, можно импортировать текст запроса и выполнить его напрямую, минуя библиотеку. Это всего лишь некрасиво, когда одиночное изменение выполняется функцией ins_data(conn, param1, param2), а множественное conn.executemany(stmt, params_sequence). Но совсем иначе выглядит, когда это последовательность dataclass-ов из выполненного ранее select запроса. При одиночной модификации через функцию, dataclass "распаковывается" в nm.params в нужном объеме, а для составления params_sequence, эту "распаковку" приходится дублировать (вне DB API модуля).
Сам считаю проблему низкоприоритеной. Где записей мало, можно вызвать функцию для каждого элемента последовательности. А где много, встречается не часто и executemany, даже с подготовкой данных, сильно код не засорят. Но обозначить проблему, также посчитал нужным.