Про резидентные базы данных

базы данных основы, примеры баз данных для аналитика, резидентные БД примеры, курсы по основам БД, Школа прикладного бизнес-анализа и проектирования информационных систем

Как хранить данные в памяти без развертывания сервера СУБД: ликбез по реляционным и нереляционным In-Memory хранилищам на примере SQLLite, NebulaGraph Lite, DuckDB и Redis.

Зачем нужны резидентные БД и какие они бывают

Хотя главное назначение базы данных – персистентное, т.е. долговременное хранение информации, есть особая категория встраиваемых СУБД, которые хранят данные в оперативной памяти, а не на диске. Они называются резидентные и работают, будучи встроенными в вычислительный процесс, запущенный внутри приложения. Благодаря отсутствию операций дискового ввода-вывода, резидентные БД позволяют работать с данными практически в реальном времени, с минимальной задержкой. Это полезно для для приложений, где важен мгновенный отклик: торговые системы, финансовые платформы, онлайн-игры и мобильные приложения.

Сегодня наиболее популярными примерами резидентных баз данных считаются следующие:

  • SQLLite – компактная встраиваемая однофайловая реляционная СУБД, которая не имеет сервера и хранит всю базу локально на одном устройстве. SQLite не требует отдельного серверного процесса. Все данные хранятся в одном файле на диске, что упрощает развертывание и управление. SQLite обеспечивает ACID-требования к транзакциям, а для работы с данными использует API SQL-запросов. Она не обладает широким набором возможностей, который есть у PostgreSQL и не подходит для высоконагруженных серверных приложений с большим количеством одновременных записей. Тем не менее, библиотека SQLite занимает всего несколько сотен килобайт, что делает эту СУБД идеальной для мобильных и встроенных систем.
  • NebulaGraph Lite — облегченная версия графовой базы данных NebulaGraph, разработанная для упрощения развертывания и использования в небольших проектах или на этапе разработки. Она предоставляет основные функции полнофункциональной версии NebulaGraph, но с упрощенной конфигурацией и меньшими системными требованиями, предоставляя возможности мощной графовой базы данных на малых ресурсах.
  • DuckDB – колоночная аналитическая СУБД с векторизированным механизмом обработки SQL-запросов. Как и SQLLite, она не требует выделенного сервера и работает внутри хост-процесса. Например, благодаря привязкам для интерпретатора Python можно напрямую размещать данных в массивах NumPy. DuckDB предназначена для встраивания в приложения на различных языках программирования, включая Python, R, C++, и другие. Колоночная архитектура позволяет эффективно сжимать данные и ускорять выполнение аналитических запросов, поскольку доступ к данным происходит по столбцам, а не по строкам. DuckDB ориентирована на бессерверные приложения быстрой аналитики больших объемов данных в интерактивном режиме. Она хорошо подходит для мобильных Data Science приложений и систем, которым нужна высокая производительность аналитической обработки больших объемов данных при ограниченных ресурсах.
  • Redis (REmote DIctionary Server) высокопроизводительное хранилище данных в памяти с открытым исходным кодом. Это БД часто используется в качестве внешнего кэша и брокера сообщений. В отличие от многих других key-value хранилищ, Redis поддерживает различные структуры данных: строки, хэши, списки, множества, отсортированные множества, битмапы, гиперлоглоги и геопространственные индексы. Это позволяет выбирать наиболее подходящий тип данных для конкретных задач. Примеры работы с Redis на Python я показывала здесь и здесь.

Чтобы показать резидентный характер перечисленных баз данных, далее я выполню небольшую демонстрацию, запустив их с движком исполнения in-memory в Python-процессе на интерактивной виртуальной машине Google Colab.

SQLLite

В качестве примера напишем Python-скрипт, который создаёт временную базу данных, добавляет в неё случайных клиентов и их заказы. Затем получим агрегированную информацию о том, сколько каждый клиент потратил денег, вычислив LTV (Live Time Value) и сколько заказов у него было.

Сперва установим библиотеки и импортируем модули.

# установка библиотек
!pip install faker

# импорт модулей
import sqlite3
import os
import random
from faker import Faker
from faker.providers.address.ru_RU import Provider
from datetime import datetime

# инициализация Faker
fake = Faker("ru_RU")

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

# подключение к базе данных
conn = sqlite3.connect(':memory:') #БД в памяти

cursor = conn.cursor()

# создаем таблицы в БД
cursor.executescript('''
CREATE TABLE IF NOT EXISTS customers (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    email TEXT UNIQUE NOT NULL
);

CREATE TABLE IF NOT EXISTS orders (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    amount INTEGER NOT NULL,
    moment TIMESTAMP NOT NULL,
    customer INTEGER NOT NULL,
    FOREIGN KEY (customer) REFERENCES customers(id)
);
''')
conn.commit()

Определим функции вставки данных и подсчета агрегаций

# функция добавления клиента
def add_customer(name, email):
    try:
        cursor.execute('''
            INSERT INTO customers (name, email) VALUES (?, ?)
        ''', (name, email))
        conn.commit()
        print(f'Клиент {name} добавлен.')
    except sqlite3.IntegrityError:
        print('Ошибка: Такой email уже существует')

# функция добавления заказа
def add_order(amount, customer_id):
    try:
        current_time = datetime.now()
        cursor.execute('''
            INSERT INTO orders (amount, moment, customer) VALUES (?, ?, ?)
        ''', (amount, current_time, customer_id))
        conn.commit()
    except sqlite3.IntegrityError:
        print('Ошибка: такой клиент НЕ существует')

# функция получения агрегатов заказов по клиентам
def get_customer_orders():
    cursor.execute('''
        SELECT
            customers.name AS "Имя клиента",
            customers.email AS "Email клиента",
            SUM(orders.amount) AS "LTV",
            COUNT(orders.id) AS "Количество заказов"
        FROM orders
        JOIN customers ON customers.id = orders.customer
        GROUP BY customers.id
    ''')
    rows = cursor.fetchall()
    for row in rows:
        print(row)

Вставим данные, сгенерировав значения с помощью библиотеки Faker:

# добавляем клиентов
customers_quantity = random.randint(1, 10)
for _ in range(customers_quantity):
    name = fake.name()
    email = fake.unique.free_email()
    add_customer(name, email)

# получаем список существующих клиентов
cursor.execute('SELECT id FROM customers')
customer_ids = [row[0] for row in cursor.fetchall()]

# добавляем заказы
if customer_ids:
    orders_quantity = random.randint(1, 10) * len(customer_ids)
    for _ in range(orders_quantity):
        amount = random.randint(1, 1000)
        customer_id = random.choice(customer_ids)
        add_order(amount, customer_id)
else:
    print('Нет клиентов для добавления заказов')

Наконец, подсчитаем агрегаты и выведем результат:

# получаем и выводим заказы по клиентам
print('Заказы по клиентам')
get_customer_orders()

# закрываем соединение
conn.close()
Запуск SQLLite в памяти на Google Colab
Запуск SQLLite в памяти на Google Colab

DuckDB

Продолжая пример с ритейл-доменом, в DuckDB создадим таблицы с товарами и поставщиками. Сперва установим библиотеки и импортируем модули

!pip install duckdb
import duckdb
import random
from faker import Faker
from faker.providers.address.ru_RU import Provider

Установим подключение к DuckDB, развернутой в памяти и создадим последовательность для автоинкрементации первичного ключа в каждой таблицы. В отличие от PostgreSQL, MySQL и SQLLite, в DuckDB нет заранее созданных последовательностей, которые можно использовать при объявлении типа данных столбца.

# создание подключения (используем in-memory базу данных)
con = duckdb.connect(':memory:')

# последовательность для автоинкремента ключа
con.execute("""CREATE SEQUENCE id_sequence START 1;""")

# создание таблиц DuckDB
con.execute("""
    CREATE TABLE IF NOT EXISTS providers (
        id INTEGER PRIMARY KEY DEFAULT nextval('id_sequence'),
        name TEXT NOT NULL,
        email TEXT NOT NULL,
        phone TEXT
    );
""")

con.execute("""
    CREATE TABLE IF NOT EXISTS products (
        id INTEGER PRIMARY KEY DEFAULT nextval('id_sequence'),
        name TEXT NOT NULL,
        price DECIMAL NOT NULL,
        provider INTEGER NOT NULL,
        FOREIGN KEY (provider) REFERENCES providers(id)
    );
""")

Определим функции добавления фейковых данных в таблицы и вычисления агрегаций по поставщикам – какое количество товаров поставл каждый из них.

# функция добавления поставщика
def add_provider(name, email, phone):
    try:
        con.execute("INSERT INTO providers (name, email, phone) VALUES (?, ?, ?)", (name, email, phone))
        print(f'Поставщик {name} добавлен.')
    except duckdb.Error as e:
        print(f'Ошибка при добавлении поставщика: {e}')

# функция добавления товара
def add_product(name, price, provider):
    try:
        con.execute("INSERT INTO products (name, price, provider) VALUES (?, ?, ?)", (name, price, provider))
        print(f'Валюта {name} стоимостью {price} добавлена')
    except duckdb.IntegrityError:
        print('Ошибка: такого поставщика НЕ существует')
    except duckdb.Error as e:
        print(f'Ошибка при добавлении товара: {e}')

# добавляем поставщиков
providers_quantity = random.randint(1, 10)
for _ in range(providers_quantity):
    name = fake.company()
    email = fake.unique.email()
    phone = fake.phone_number()
    add_provider(name, email, phone)

# получаем список существующих поставщиков
provider_ids = con.execute('SELECT id FROM providers').fetchall()
provider_ids = [row[0] for row in provider_ids]

# добавляем товары
if provider_ids:
    products_quantity = random.randint(1, 5)
    for _ in range(products_quantity):
        name = fake.currency_name()
        price = random.uniform(10.0, 10000.0)  # float для цены
        provider = random.choice(provider_ids)
        add_product(name, price, provider)
else:
    print('Нет поставщиков для добавления товара')

# функция получения агрегатов товаров по поставщикам
def get_providers_products():
    try:
        result = con.execute('''
            SELECT
                providers.name AS "Provider Name",
                providers.email AS "Provider Email",
                COUNT(products.id) AS "Product Count"
            FROM products
            JOIN providers ON providers.id = products.provider
            GROUP BY products.provider, providers.name, providers.email
            ORDER BY "Product Count" DESC
        ''').fetchall()
        
        for row in result:
            print(row)
    except duckdb.Error as e:
        print(f'Ошибка при получении аналитики: {e}')

Наконец, вычислим результат агрегации и выведем его в консоль.

# получаем и выводим информацию по поставщикам
print('Информация по поставщикам:')
get_providers_products()

# закрываем подключение
con.close()
Работа с DuckDB
Работа с DuckDB

NebulaGraph Lite

Для NebulaGraph Lite я решила не писать собственный набор данных, а использовать демо-граф игроков в командах. Впрочем, чтобы работать с этим легковесным хранилищем, надо сперва установить нужные библиотеки и пакеты. Например, для интерактивной визуализации графа в консоли Colab пришлось установить библиотеку pyvis и NetworkX, а также ng_nx для интеграции между Nebula Graph и NetworkX. БД запускается в Docker-контейнере, чтобы использовать автоматически настроенные сервисы и запустить их в среде Colab. Помимо уже настроенных зависимостей, этот Docker-контейнер с базой содержит сами данные, т.е. граф знаний об игроках.

# Установка библиотеки nebulagraph-lite для работы с графами
!pip install nebulagraph-lite

# Установка библиотек jupyter_nebulagraph и pyvis для визуализации графов в Jupyter Notebook
!pip install jupyter_nebulagraph pyvis

# Установка библиотеки ng_nx для интеграции Nebula Graph с NetworkX
!pip install ng_nx

# Импорт функции nebulagraph_let из пакета nebulagraph_lite
from nebulagraph_lite import nebulagraph_let

# Импорт классов NebulaReader и NebulaWriter из пакета ng_nx для чтения и записи данных в Nebula Graph
from ng_nx import NebulaReader
from ng_nx import NebulaWriter

# Импорт класса NebulaGraphConfig из утилит ng_nx для настройки подключения к Nebula Graph
from ng_nx.utils import NebulaGraphConfig

# Импорт библиотеки NetworkX для создания и управления графами
import networkx as nx

# Инициализация объекта nebulagraph_let с отключенным режимом отладки
n = nebulagraph_let(debug=False)

# Запуск сервиса Nebula Graph
n.start()

# Вывод списка запущенных Docker-контейнеров, связанных с Nebula Graph
n.docker_ps()

# загружаем расширение ngql для выполнения запросов к базе данных Nebula Graph
%load_ext ngql
%ngql --address 127.0.0.1 --port 9669 --user root --password nebula  # установка соединения с сервером Nebula Graph по адресу 127.0.0.1 и порту 9669, используя имя пользователя root и пароль nebula

%ngql SHOW HOSTS;  # отобразим список хостов в кластере Nebula Graph

Сделаем запрос к графу

# ищем все ребра (отношения) между любыми двумя узлами в графе, возвращаем результаты и ограничиваем вывод первыми 10 ребрами
%%ngql
MATCH ()-[e]->() RETURN e LIMIT 10

# визуализация графа
%ng_draw
Запуск NebulaGraph Lite в Google Colab
Запуск NebulaGraph Lite в Google Colab

Визуализацию графа можно сохранить как отдельное изображение.

Визуализация графа
Визуализация графа

Таким образом, если нужно что-то быстро разработать для проверки гипотезы или знакомства с технологией без развертывания отдельного сервера, резидентные базы данных – отличный выбор. При промышленном использовании резидентной БД стоит помнить о том, что вся информация потеряется при остановке вычислительного хост-процесса, в который встроено это хранилище данных. Для долговременного хранения этих данных их следует выгрузить на диск, используя встроенные механизма In-Memory хранилища (моментальные снимки, логирование операций) или внешние инструменты: системы резервного копирования или собственные скрипты.

Подробнее про хранение и обработку данных в информационных системах вы узнаете на моих курсах в Школе прикладного бизнес-анализа на базе нашего лицензированного учебного центра обучения и повышения квалификации системных и бизнес-аналитиков в Москве:

 

Я даю свое согласие на обработку персональных данных и соглашаюсь с политикой конфиденциальности.