Как хранить данные в памяти без развертывания сервера СУБД: ликбез по реляционным и нереляционным 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()
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()
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
Визуализацию графа можно сохранить как отдельное изображение.
Таким образом, если нужно что-то быстро разработать для проверки гипотезы или знакомства с технологией без развертывания отдельного сервера, резидентные базы данных – отличный выбор. При промышленном использовании резидентной БД стоит помнить о том, что вся информация потеряется при остановке вычислительного хост-процесса, в который встроено это хранилище данных. Для долговременного хранения этих данных их следует выгрузить на диск, используя встроенные механизма In-Memory хранилища (моментальные снимки, логирование операций) или внешние инструменты: системы резервного копирования или собственные скрипты.
Подробнее про хранение и обработку данных в информационных системах вы узнаете на моих курсах в Школе прикладного бизнес-анализа на базе нашего лицензированного учебного центра обучения и повышения квалификации системных и бизнес-аналитиков в Москве: