БД месенджера (ч.1): проектуємо каркас бази

Як можна перевести бізнес-вимоги до конкретних структур даних на прикладі проектування «з нуля» бази для месенджера.

БД месенджера (ч.1): проектуємо каркас бази
Наша база буде не такою масштабною та розподіленою, як у ВКонтакті або Badoo, а «щоб було», але було добре – функціонально, швидко та уміщалося на одному сервері PostgreSQL — щоб можна було розгорнути окремий екземпляр сервісу десь на боці, наприклад.

Тому не зачіпатимемо питання шардингу, реплікації та георозподілених систем, а зосередимося на схемних рішеннях усередині БД.

Крок 1: Небагато бізнес-специфіки

Наш обмін повідомленнями ми проектуватимемо не абстрактно, а вбудовуватимемо в оточення корпоративної соцмережі. Тобто люди у нас не просто переписуються, а спілкуються між собою в контексті вирішення певних бізнес-завдань.

А які бувають завдання у бізнесу? Подивимося на прикладі Василя — керівника відділу розробки.

  • «Микола, ось із цього завдання патч потрібен вже сьогодні!»
    Значить, листування може вестись у контексті якогось документа.
  • «Коля, го ввечері в доту?»
    Тобто навіть у однієї пари співрозмовників спілкування одночасно може вестись з різних тем.
  • «Петро, ​​Миколо, подивіться в атачі прайс на новий сервер.»
    Так, в одного повідомлення може бути кілька адресатів. При цьому повідомлення може містити Прикріплені файли.
  • "Семен, і ти теж поглянь."
    І має бути можливість у вже існуюче листування запросити нового учасника.

Зупинимося поки що на цьому переліку «очевидних» потреб.

Без розуміння прикладної специфіки завдання та обмежень, що задаються їй, спроектувати ефективну схему БД на її вирішення практично неможливо.

Крок 2: Мінімальна логічна схема

Схемно поки що все виходить дуже схоже на email-листування — традиційний інструмент ведення бізнесу. Такі «алгоритмічно» багато завдань бізнесу схожі один на одного, тому й інструменти для їх вирішення будуть структурно подібні.

Давайте зафіксуємо логічну схему відносин сутностей, що вже вийшла. Для простоти розуміння нашої моделі скористаємося найпримітивнішим варіантом відображення ER-моделі без ускладнень UML або IDEF-нотацій:

БД месенджера (ч.1): проектуємо каркас бази

У прикладі персона, документ і бінарне «тіло» файлу — це «зовнішні» сутності, які самостійно існують і без нашого сервісу. Тому просто сприйматимемо їх надалі як деякі посилання «кудись» по UUID.

Малюйте схеми якомога простіше — більшість тих, кому ви їх показуватимете, не є експертами в читанні UML/IDEF. Але малюйте обов'язково.

Крок 3: Накидаємо структуру таблиць

Про імена таблиць та полівДо «російських» назв полів та таблиць можна ставитися по-різному, але це справа смаку. Оскільки у нас у «Тензорі» немає розробників-іноземців, а PostgreSQL дозволяє нам давати назви хоч ієрогліфами, якщо вони укладені в лапки, то ми вважаємо за краще називати об'єкти однозначно-зрозуміло, щоб не виникало різночитань.
Оскільки повідомлення у нас пишуть багато людей одразу, частина з них взагалі може робити це в офлайн-режимі, то найпростіший варіант - використовувати UUID як ідентифікатори не тільки для зовнішніх сутностей, а й для всіх об'єктів усередині нашого сервісу. Причому генерувати їх можна навіть на стороні клієнта — це допоможе нам підтримати відправку повідомлень при короткочасній недоступності БД, а ймовірність колізії вкрай мала.

Чорнова структура таблиць у нашій базі набуде такого вигляду:
Таблиці: RU

CREATE TABLE "Тема"(
  "Тема"
    uuid
      PRIMARY KEY
, "Документ"
    uuid
, "Название"
    text
);

CREATE TABLE "Сообщение"(
  "Сообщение"
    uuid
      PRIMARY KEY
, "Тема"
    uuid
, "Автор"
    uuid
, "ДатаВремя"
    timestamp
, "Текст"
    text
);

CREATE TABLE "Адресат"(
  "Сообщение"
    uuid
, "Персона"
    uuid
, PRIMARY KEY("Сообщение", "Персона")
);

CREATE TABLE "Файл"(
  "Файл"
    uuid
      PRIMARY KEY
, "Сообщение"
    uuid
, "BLOB"
    uuid
, "Имя"
    text
);

Таблиці: EN

CREATE TABLE theme(
  theme
    uuid
      PRIMARY KEY
, document
    uuid
, title
    text
);

CREATE TABLE message(
  message
    uuid
      PRIMARY KEY
, theme
    uuid
, author
    uuid
, dt
    timestamp
, body
    text
);

CREATE TABLE message_addressee(
  message
    uuid
, person
    uuid
, PRIMARY KEY(message, person)
);

CREATE TABLE message_file(
  file
    uuid
      PRIMARY KEY
, message
    uuid
, content
    uuid
, filename
    text
);

Найпростіше при описі формату - починати "розкручувати" граф зв'язків від таблиць, які не посилаються самі ні на кого.

Крок 4: З'ясовуємо неочевидні потреби

Все, ми спроектували базу, в яку можна добре писати і якось читати.

Давайте поставимо себе на місце користувача нашого сервісу - що ми захочемо робити за його допомогою?

  • Останні повідомлення
    Це хронологічно відсортований за різними ознаками реєстр «моїх» повідомлень. Де я один із адресатів, де я автор, де мені написали, а я не відповів, де не відповіли мені, …
  • Учасники листування
    Хто взагалі бере участь у цьому довгому-довгому чаті?

Наша структура дозволяє вирішити обидві ці завдання «взагалі», але швидко ні. Проблема в тому, що для сортування в рамках першого завдання неможливо створити індекс, що підходить для кожного з учасників (і доведеться витягувати всі записи), а для вирішення другої необхідно витягувати всі повідомлення по темі.

Непередбачені завдання користувача можуть поставити жирний хрест на продуктивності.

Крок 5: Розумна денормалізація

Обидві наші проблеми допоможуть вирішити додаткові таблиці, у яких ми будемо дублювати частину даних, необхідні формування на них відповідних до наших завдань індексів.
БД месенджера (ч.1): проектуємо каркас бази

Таблиці: RU

CREATE TABLE "РеестрСообщений"(
  "Владелец"
    uuid
, "ТипРеестра"
    smallint
, "ДатаВремя"
    timestamp
, "Сообщение"
    uuid
, PRIMARY KEY("Владелец", "ТипРеестра", "Сообщение")
);
CREATE INDEX ON "РеестрСообщений"("Владелец", "ТипРеестра", "ДатаВремя" DESC);

CREATE TABLE "УчастникТемы"(
  "Тема"
    uuid
, "Персона"
    uuid
, PRIMARY KEY("Тема", "Персона")
);

Таблиці: EN

CREATE TABLE message_registry(
  owner
    uuid
, registry
    smallint
, dt
    timestamp
, message
    uuid
, PRIMARY KEY(owner, registry, message)
);
CREATE INDEX ON message_registry(owner, registry, dt DESC);

CREATE TABLE theme_participant(
  theme
    uuid
, person
    uuid
, PRIMARY KEY(theme, person)
);

Тут ми застосували два типові підходи, які застосовуються при створенні допоміжних таблиць:

  • Розмноження записів
    Формуємо по одному вихідного запису повідомлення відразу кілька записів-наслідків у різні види реєстрів для різних власників - як відправника, так одержувача. Проте кожен із реєстрів тепер лягає на індекс — адже в типовому випадку ми захочемо бачити лише першу сторінку.
  • Унікалізація записів
    При кожній відправці повідомлення всередині конкретної теми достатньо перевірити, чи існує такий запис. Якщо ні — додаємо її до нашого «словника».

У наступній частині статті мова піде про використання секціонування у структуру нашої бази.

Джерело: habr.com

Додати коментар або відгук