Легковесная встраиваемая база данных LiteDB

04.10.2017 at 20:57


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

Авторы рекомендуют базу для небольших десктоп приложений и веб сервисов. При этом нужно учитывать следующие рекоммендации:
— одна база на одного пользователя/аккаунт
— небольшое количество конкурентных операций записи
Вся база данных хранится в одном файле.

LiteDB распостраняется в виде nuget пакета. Доступна в .Net Core 2.0.

Подключение к базе создается так:

using LiteDB;
...
var db = new LiteDatabase(<path_to_database_file>)

При этом LiteDatabase реализует интерфейс IDisposable.

Все объекты в базе хранятся в типизированных именованных коллекциях

var games = db.GetCollection<GameInfo>(GAMES_COLLECTION_NAME)

Для добавления элемента используются методы Insert и InsertBulk, для обновления Update, для поиска Find. Метод EnsureIndex создает индекс если его еще не было.

Тест производительности

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

Тестовый запрос: фильтрация по одному свойству, затем сортировка по другому свойству и получение TOP 5 результатов.

Сначала создаем класс тестовой сущности и заполняем базу

public class TestModel
{
    public string Name { get; set; }
    public int Value { get; set; }
    public int GroupId { get; set; }
}

static void GenerateData()
{
    var faker = new Faker();
    using (var db = new LiteDatabase(@"MyData.db"))
    {
        var models = db.GetCollection<TestModel>("models");
        for (var i = 1; i <= 500; i++)
        {
            var list = new List<TestModel>();
            for (var j = 1; j <= 1000; j++)
            {
                list.Add(new TestModel()
                {
                    GroupId = i,
                    Name = faker.Name.FullName(),
                    Value = faker.Random.Int(10000, 99999)
                });
            }
            models.InsertBulk(list);
        }
    }
}

Для генерации данных использую bogus

Созданная база содержит 500 000 строк и занимает 109 мегабайт.

Теперь замеряем усредненную длительность нужного запроса

static Random random = new Random();

static double TestQuery()
{
    var groupId = random.Next(1, 500);
    using (var db = new LiteDatabase(@"MyData.db"))
    {
        var models = db.GetCollection<TestModel>("models");
        var t1 = DateTime.Now;
        var q = models.Find(x => x.GroupId == groupId).
            OrderByDescending(x => x.Value).
            Take(5).
            ToList();
        return (DateTime.Now - t1).TotalMilliseconds;
    }
}

static double Test()
{ 
    TestQuery();
    double total = 0;
    int count = 30;
    for (var i = 1; i <= count; i++)
    {
        total += TestQuery();
    }
    total = total / count;
    return total;
}

Средняя длительность запроса составила 8.8513 мс. Первый вызов TestQuery() вне цикла нужен для прогрева базы, он на порядки дольше.

Теперь попробуем добавить индекс по GroupId и повторить измерение длительности запроса

static void addIndexByGroup()
{
    using (var db = new LiteDatabase(@"MyData.db"))
    {
        var models = db.GetCollection<TestModel>("models");
        models.EnsureIndex(x => x.GroupId);
    }
}

Размер базы вырос до 136 мегабайт, а длительность запроса уменьшилась до 7.8323 мс.

Меня такая производительность устроила.