понедельник, 28 июня 2010 г.

dbdict - база данных ключ/значение на python

Некоторое время назад я писал о небольших наработках использования баз данных ключ/значение: kvlite или KeyValue datastore на базе sqlite3, методы хранения данных. Сегодня я хочу продолжить эту тематику и рассказать о маленькой python библиотеке для работы с ключ/значения базой данных использующей sqlite3 как ядро.

Библиотека называется dbdict и доступна на bitbucket.org http://www.bitbucket.org/nephics/dbdict. По размеру составляет всего 400 строк. Интерфейс работы с библиотекой достаточно прост и легко может быть продемонстрирован следующим кодом:
import dbdict
d = dbdict.open('tempdict')
d['foo'] = 'bar'
# В этой точке пара ключ-значение 'foo': 'bar' будет сохранена на диск.
d['John'] = 'doh!'
d['pi'] = 3.999
d['pi'] = 3.14159  # замещение предыдущей версии pi
d['pi'] += 1
d['age'] = '21'
del d['age']    # пара 'age': 21 будет удалена из базы
d.close()    # закрытие файла база данных

Для более эффективного получения значений из базы данных может быть использован метод get(), который в качестве аргумента получает список ключей. Для эффективного добавления/обновления пар ключ-значение используется метод update(). В качестве аргумента этот метод получает список пар ключ-значение. Если пара не существует в базе она будет добавлена, если существует, то значение для указанного ключа будет изменено. Для удаления используется метод remove, который также удобно использовать над множеством ключей.

Также есть ряд служебных методов: clear - удаление всех записей и освобождение неиспользуемого дискового пространства, reindex - пересоздание индекса ключей, vacuum - стандартная функция sqlite3

К сожалению у меня не получилось воспроизвести результаты тестов, проводимых Якобом и достичь записи 1 млн. пар за 12 секунд, и чтения 1 млн. записей за 32 секунд и 10 секунд при использовании метода get(). Думаю это можно списать на мой лептоп (Lenovo X61s), который на лету шифрует данные при записи на диск. Да и процессор у меня не из самых мощных. Даже несмотря на эти ограничения системы, результаты получились на такие уж и плохие:

Время выполнения вставки/обновления, выборки ключей (база данных на диске)



100 записей1000 записей10000 записей100000 записей1000000 записей
INSERT/REPLACE0.33 сек0.61 сек0.73 сек5 сек53.16 сек
SELECT (по одному значению)0.03 сек0.25 сек2.5 сек25.69 сек261.37 сек
SELECT (методом get)0 сек0.03 сек0.281 сек3.20 сек42.95 сек

Расчетное значение количества записей в секунду (база данных на диске)



100 записей1000 записей10000 записей100000 записей1000000 записей
INSERT/REPLACE304.881642.0413623.982000018812.2
SELECT (по одному значению)3225.81400040003893.023825.92
SELECT (методом get)-32258.0635587.1931210.9923281.26


Время выполнения вставки/обновления, выборки ключей (база данных в памяти)



100 записей1000 записей10000 записей100000 записей1000000 записей
INSERT/REPLACE0 сек0.03 сек0.37 сек4.19 сек46.812 сек
SELECT (по одному значению)0 сек0.05 сек0.5 сек4.9 сек54.7 сек
SELECT (методом get)0 сек0.03 сек0.25 сек2.83 сек34.47 сек


Расчетное значение количества записей в секунду (база данных в памяти)





100 записей1000 записей10000 записей100000 записей1000000 записей
INSERT/REPLACE-32258.06 26666.6723883.45 21362.04
SELECT (по одному значению)-21276.5920661.1620383.2018280.2
SELECT (методом get)-32258.064000035348.1829012.42


Код на основании которого проводились тесты:
d = DbDict2(':memory:') # or d = DbDict2('dbdict2.sqlite')

data = []
keys = []
time_start = datetime.datetime.now()
for i in range(10**6): 
    key = unicode("key-%d" % i,'utf-8')
    value = unicode("value-%d" % i,'utf-8')
    data.append((key, value))
    keys.append(key)
print 'Data generation time: %s' % (datetime.datetime.now() - time_start)

time_start = datetime.datetime.now()
d.update(data)
print 'Data insert/replace time: %s' % (datetime.datetime.now() - time_start)

time_start = datetime.datetime.now()
for k in keys: v = d[k]
print 'Data select time: %s' % (datetime.datetime.now() - time_start)

time_start = datetime.datetime.now()
d.get(keys)
print 'Data select time (method get()): %s' % (datetime.datetime.now() - time_start)
d.close()
Так же следует отметить, что исходный код bddict не позволял делать выборку методом get при передаче в качестве аргумента миллиона записей, пришлось его несколько доработать.
def get(self, keys):
    '''Get item(s) for the specified key or list of keys.
    
    Items will be returned only for those keys that are defined. The
    function will pass silently (i.e. not raise an error) if one or more of
    the keys is not defined.
    
    Support long list of keys'''

    ssize = 200  # slice size
    try:
        keys = tuple(keys)
    except TypeError:
        # probably a single key (ie not an iterable)
        keys = (keys,)
    SQL = u'SELECT key, value FROM data WHERE key in (%s);'
    kv = []
    for i in xrange(len(keys)/ssize + 1):
        parted_keys = keys[i*ssize:i*ssize+ssize]
        kv.extend(self.con.execute(SQL % ','.join('?' for k in parted_keys), parted_keys).fetchall())
    return kv
Как резюме, в целом библиотека достаточно интересная, простая и удобная в применении. В будущем постараюсь поделиться опытом использования ее в реальных приложениях.

1 комментариев:

  1. похожую логику (см. метод get) необходимо применить и для метода remove

    ОтветитьУдалить