Денормализованные данные реляционной базы данных (RDBMS) - PullRequest
3 голосов
/ 02 июня 2019

Я полагаю, что этот вопрос не касается конкретно MySQL - базы данных, которую я использую, - и о лучших практиках.

До сих пор мои проблемы можно было решить, создавая таблицы и запрашивая их (иногда присоединяясь здесь и там). Но есть кое-что, что я делаю, и это не правильно, и это вызывает у меня всякий раз, когда мне нужны денормализованные данные вместе с моими "общими" запросами.

Пример варианта использования

Чтобы я мог выразить себя лучше, давайте создадим поверхностный сценарий, где:

  • a user может купить product, генерируя purchase (давайте проигнорируем тот факт, что purchase может иметь только один product);
  • и нам нужно запросить product s с общим количеством раз, которое было purchase d;

Чтобы решить наш сценарий использования, мы могли бы определить простую структуру , созданную следующим образом:

  • product таблица:

    • product_id [INT PK ]
  • user таблица:

    • user_id [INT PK ]
  • purchase таблица:

    • purchase_id [INT PK ]
    • product_id [INT FK NOT NULL]
    • user_id [INT FK NOT NULL]

Вот где это нехорошо : Когда нам нужно получить список product с общим количеством покупок, я бы создал запрос:

# There are probably faster queries than this to reach the same output
SELECT
    product.product_id,
    (SELECT COUNT(*) FROM purchase
      WHERE purchase.product_id = product.product_id)
FROM
    product

Мое беспокойство вызвано тем, что я прочитал, что COUNT выполняет полное сканирование таблицы, и меня пугает выполнение вышеуказанного запроса при масштабировании до тысячи покупаемых продуктов - даже если я создал INDEX с product_id FK на purchase (MySQL делает это по умолчанию).


Возможные решения

Мои знания по реляционным базам данных довольно незначительны, поэтому я немного растерялся, сравнивая альтернативы (вероятные) для подобных проблем. Чтобы не сказать, что я не сделал свою домашнюю работу (поиск, прежде чем спрашивать), я нашел правдоподобным:

Создание транзакций:

Когда INSERT вводит новый purchase, он всегда должен быть внутри транзакции, которая также обновляет таблицу product с purchase.product_id.

Возможные проблемы: человеческая ошибка. Кто-то может вручную вставить purchase без выполнения транзакции и BAM - у нас несоответствие.

Создать триггеры:

Всякий раз, когда я вставляю, удаляю или обновляю какую-либо строку в некоторой конкретной таблице, я обновляю свою таблицу product новым значением (bought_amount). Таким образом, таблица станет:

  • product таблица:
    • product_id [INT PK]
    • bought_amount [INT NOT NULL];

Возможные проблемы: дорогие триггеры? Есть ли способ, которым вставка завершается успешно, но триггер не сработает, оставляя меня в несогласованности?


Вопрос

Обновление определенных таблиц для хранения данных, которые постоянно изменяются, является ли правдоподобным подходом к СУБД? Является ли безопаснее и - в долгосрочной перспективе - выгоднее просто продолжать присоединяться и считать / суммировать другие вхождения?

Я нашел пару полезных вопросов / ответов по этому вопросу, но ни один из них не рассматривал эту тему в широкой перспективе. Пожалуйста, примите во внимание мое незнание о РСУБД, так как я могу предложить бессмыслицу Возможные решения .

Ответы [ 3 ]

2 голосов
/ 02 июня 2019

Обычный способ получить счет за ключ -

SELECT product_id, COUNT(*)
FROM purchase
GROUP BY product_id

Вам не нужно упоминать таблицу product, потому что все, что она содержит, - это ключевой столбец.Теперь, хотя для этого используется COUNT(*), ему не нужно полное сканирование таблицы для каждого product_id, потому что механизм SQL достаточно умен, чтобы увидеть GROUP BY.

Но это приводит к другому результату для вашегоquery: для product s, которые никогда не были куплены, мой запрос просто не показывает их;Ваш запрос покажет product_id с числом ноль.

Затем, прежде чем начать беспокоиться о реализации и эффективности, на какие вопросы вы пытаетесь ответить?Если вы хотите просмотреть все product, независимо от того, куплены они или нет, вы должны отсканировать всю таблицу product и посмотреть на нее до purchase.Я бы сказал

SELECT product_id, count
FROM product
OUTER JOIN (SELECT product_id, COUNT(*) AS count
            FROM purchase
            GROUP BY product_id) AS purch
ON product.product_id = purch.product_id

Что касается ваших более широких вопросов (не уверен, что я их полностью понимаю), в первые дни SQL был довольно неэффективен при такого рода объединении и агрегировании, а схемы часто денормализовали с помощью повторяющихся столбцов.в нескольких таблицах.Механизмы SQL теперь намного умнее, так что в этом нет необходимости.Вы можете увидеть эту старомодную практику в старых учебниках.Я бы проигнорировал это и разработал бы вашу схему максимально нормализованной.

2 голосов
/ 02 июня 2019

Этот запрос:

SELECT p.product_id,
      (SELECT COUNT(*)
       FROM purchase pu
       WHERE pu.product_id = p.product_id
      )
FROM product p;

должен сканировать как product, так и purchase.Я не уверен, почему вы испытываете эмоциональное отношение к одному сканированию таблицы, а не к другому.

Что касается производительности, это может использовать индекс на purchase(product_id).В MySQL это, вероятно, будет быстрее, чем эквивалентная (левая) версия соединения.

Вам не следует беспокоиться о производительности таких запросов, пока это не станет проблемой.Если вам нужно повысить производительность такого запроса, сначала я бы спросил: почему?То есть возвращается много информации - обо всех продуктах за все время.Более типично, я ожидал бы, что кто-то будет заботиться об одном продукте или периоде времени или обоих.И эти проблемы могут привести к разработке datamart.

Если производительность является проблемой, у вас есть много альтернатив, таких как:

  • Определение витрины данных для периодического суммирования данныхв более эффективные структуры для таких запросов.
  • Добавление триггеров в базу данных для суммирования данных, если результаты необходимы в режиме реального времени.
  • Разработка методологии для обслуживания данных, которая также поддерживаетсводки, либо на уровне приложения, либо с использованием хранимых процедур.

То, что вам «не кажется правильным», на самом деле является огромной силой реляционной базы данных (с разумной моделью данных).Вы можете держать это в актуальном состоянии.И вы можете запросить его, используя довольно лаконичный язык, который отвечает потребностям бизнеса.

0 голосов
/ 03 июня 2019

Возможные проблемы: человеческая ошибка. Кто-то может вручную вставить покупку без выполнения транзакции и BAM - у нас несоответствие.

-> Создайте хранимую процедуру, которая выполняет оба шага транзакции, а затем заставляет пользователей проходить через нее.

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

Триггеры не так уж плохи. Но, опять же, я бы порекомендовал принуждать пользователей к хранимой процедуре, которая выполняет все необходимые действия.

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

База данных является «источником правды» в данных. Это «постоянный» репозиторий для таких. Не следует рассматривать весь движок для создания приложения.

Что касается производительности:

  • Суммирование более миллиона строк может занять заметное количество времени.
  • Вы можете легко выполнить сотню однострочных запросов (выбрать / вставить / обновить) в секунду.
  • Пожалуйста, продумайте цифры вот так.
...