Как стать автором
Обновить

Комментарии 19

3 сек. Пожалуй, это лучший результат здесь.

Вы не добавили индекс на столбец кода. Это может ускорить sqlite ещё во много раз.

не может, а точно ускорит.


вообще есть ощущение, что автору надо не статьи писать, а получать базовые знания по базам данных (о большое линейного поиска, поиска по b-tree, поиска по хэш-таблицам).

Проверим.
cur.execute("CREATE INDEX IF NOT EXISTS my_big ON t (UPCEAN);") #создание индекса
Размер на диске - 943Мб (было 774Мб в сsv)
Время поиска:

Воистину!
Спасибо.

с памятью тоже все ок:

Ну раз уж SQLite помогли индексом, то будет честно и Pandas помочь (им же). Для роста скорости загрузки данных и выборки можно сделать это:

1) Избавиться от типов object (например, преобразовать их в string): df.Name.astype('string'). Это же избавит от ошибок при сортировке, сравнении итд по Name

2) То что ищем - нужно сделать индексом: df.set_index('UPCEAN', inplace=True)

3) Обязательно его отсортировать: df.sort_index(inplace=True)

4) Сохранить df в бинарный pickle-формат Python с простым сжатием (zip): df.to_pickle('uhtt_barcode_ref_all.pkl', compression='zip') Сжатие замедлит чтение в ~2 раза, но здорово помогает при скачивании данных по LAN или из Интернет (для Raspberry Pi4 c медленной MicroSD м.б. актуально)

5) При выборке нескольких столбцов по индексу - быстрее всего сработает локатор loc[], а не методы query() или at(). Локатор, кстати, позволяет и изменять исходные данные, не только читать.

В результате по сравнению с итогами поста - имеем ~10-кратное ускорение первичного чтения и ~500-кратное для выборки, и все это - только средствами Python/Pandas. Метрики:

%%time
import pandas as pd
df = pd.read_pickle('uhtt_barcode_ref_all.pkl', compression='zip') # загрузили чистый датасет
Wall time: 5.74 s # было ~1 минута

%%timeit
df.loc[4603726031011, ['Name','CategoryName','BrandName']].to_frame() # выбрали записи
581 µs ± 21.2 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each) # было 0,3 секунды

Отказ от сжатия "консервы" (pkl-файла) или использование pyarrow выведет Pandas в бесспорные победители, но SQLite, безусловно, прекрасна и является самой быстрой SQL БД. К тому же она прекрасно поддерживается в языке Python "из коробки" (стандартная библиотека) и Pandas умеет с ней работать.

Загрузка базы и поиск c pickle, gzip, UPCEAN в index и sort:

то же самое, но без gzip:

Хорошо, но до sql далеко.

Загрузка из pkl - согласен, не быстрая (грузить приходится все данные). А вот у SQLite реализована очень эффективное чтение файла БД по указателям и индексу (частичное чтение). Хотя у себя на Pi4 вижу вдвое более высокую скорость чтения (13 и 7 секунд), возможно из-за более быстрой MicroSD. Также использую разгон CPU до 2 ГГц (Raspberrry OS 64).

Но поиск (выборка) у SQLite c индексами в вашем опыте - самое лучшее 83 миллисекунды, а у Pandas на Pi4 у меня 0.5-2 миллисекунды. Разница в 40 раз и она достаточна для того чтобы признать Pandas самым быстрым решением из рассмотренных, а возможно и из существующих (особенно если нужно найти несколько кодов).

Для чистоты эксперимента можно проверить еще и IN-MEMORY базы данных SQLite, но уверен что результат будет не лучше чем у Pandas, хотя наверняка приблизится к ней вплотную.

Полагаю, справиться легко может любой SQL: MySQL, PostgreSQL. База настолько мала, что может вся в оперативке малинки храниться. Делать этого она конечно не будет, потому что SQL достаточно умные, чтобы не загружать всё в оперативку и не положить сервер. Ну а если лень их ставить, то SQLite тоже подойдёт. Главное индексы сделать для поиска.

А, вообще, у меня веб-сервер работает с базой PostgreSQL в несколько мегабайт с 1 гигом оперативки. Нагрузка небольшая. Оперативки хватает с головой. Страницы напрямую из базы выдаются с учётом времени доставки до моего компьютера за 100-200 мс, если не срабатывет кэш, который, как раз в оперативке, тогда ещё быстрее. Или вам принципиально из csv читать данные?

Из csv читать не принципиально. Требования стандартные: быстро искать, меньше места на диске/в памяти, меньше трудозатрат по развертыванию/обновлению.

Если данные отсортированы, то даже собранный на коленке бинарный поиск по csv файлу (без предварительного считывания всего файла) даст не плохой результат. Лучший результат даёт индекс по баркодам. Индекс в данном случае — это отдельно лежащие данные заточенные под бинарный поиск в формате: код, ссылка_на_полную_запись. Любая база данных при правильном использовании великолепно с этим справится.

Pandas и Dask тоже умеют сооружать индексы.

Да. Можно и свой велосипед сделать. Только зачем? Разве что ради спортивного интереса. В исходном задании об этом ни слова. Ну в а приведённом мною примере даже без ухищрений поиск достаточно быстрый. я вообще не задумывался о скорости поиска, она такая "из коробки".

Я конечно не знаю полной постановки задачи, но если:

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

  2. список помещается в оперативную память

  3. не важно как хранить (поскольку автор несколько раз преобразовывал формат хранения: csv -> сжатый csv с частью колонок, parquet, база SQLight)

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

Компактно на диске, быстрая десериализация, моментальный поиск, минимальный перерасход памяти.

Интересный вариант. Проверим.
Преобразуем базу в словарь с индексом UPCEAN и сохраним в pickle:
df=df.set_index('UPCEAN').T.to_dict('dict')
with open('full_base.pickle', 'wb') as handle:
pickle.dump(a, handle, protocol=pickle.HIGHEST_PROTOCOL)
*без gzip
База на диске 537Мб(было 774 в csv). Это хорошо.

Теперь считаем, найдем штрихкод, замерим время и потребление памяти на raspberry.

from datetime import datetime
import pickle
from memory_profiler import profile

@profile
def load_file():
with open('full_base.pickle', 'rb') as handle:
a = pickle.load(handle)
for k,v in a.items():
if k==71736000619:
return v

if name== "main":
start = datetime.now()
load_file()
print('время работы (h:min:sec:msec): '+str(datetime.now()- start))

Итог:

*профилирование "съедает" 1 сек
Следовательно, если ничего не упустил, pickle работает медленнее "ускоренного варианта" dask и памяти занимает больше.

for k,v in a.items():

вы это серьёзно написали? если да, то, как говорится, просто нет слов, у вас очень серьёзные пробелы в образовании.

понятно. свои плюсы вы собрали, шагайте дальше.
p.s. следующее высказывание, пожалуй, про PEP8 будет.

Это был намёк на полный перебор всех данных. O(N) и O(ln(N)) - видите разницу?

можно выиграть ещё производительности если после запуска перекладывать базу в tmpfs и работать с ней там (я же правильно понял что база меняется не часто?)

а в чем смысл ? та же загрузка в RAM.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории