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

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

Проектирование

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

    Наиболее частый анти-паттерн в схемах это широкая таблица.

    Приведу парочку таблиц, с которыми мне приходилось сталкиваться в проектах.

    Table: Usl, Cols: Usl1..Usl88, далее в приложениях ну очень интересный код типа Usl1+..+Usl88

    Table: DDUContracts, Cols: FirstPay,FirsPayDate, SecondPay, SecondPayDate - ну вы поняли, первой задачей в данном проекте мне было добавить третий платеж :)

    Table: Contragents, Cols: Addr1, Addr2, Phone, MobilePhone - тоже реальный пример, попробуйте добавить третий адрес или новый тип телефона.

    Table: GoodTable, Cols: Fld1,Fld2....Fld300

  2. При проектировании схемы хранения, если сложно определиться с уровнем нормализации, лучше использовать третью нормальную форму.

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

  4. Все объекты (таблицы, колонки, представления и т.д.) необходимо тщательно комментировать. Если в используемой базе данных предусмотрены комментарии, желательно использовать данные механизмы;  если база данных не поддерживает комментарий, можно разработать собственный механизм хранения комментариев.

  5. Старайтесь минимизировать использование вызовы функций в условиях WHERE.  Вызовы функций могут мешать использованию индексов и сильно замедлять получение данных.

  6. При написании SQL запросов особое внимание стоит обращать на количество логических чтений. Большое количество логических чтений обычно указывает на недостаточное покрытие запроса индексами (Sargable).

  7. В процессе проектирования и разработки базы данных старайтесь полностью изучить механизмы предоставляемые поставщиком базы данных, например, в Microsoft SQL Server последних версии существуют таблицы хранимые в памяти и хранимые вычисляемые колонки.

  8. Если проект предусматривает поддержку нескольких баз данных, старайтесь применять ANSI SQL и не используйте слишком специфичные инструменты без сильной необходимости.

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

  10. Старайтесь изучать и избегать узкие места, заложенные проектировщиками используемой базы данных, например, функция NVL(a,b) в Oracle интерпретирует оба аргумента. Если аргумент b является подзапросом или вызовом функции, лучше использовать функцию COALESCE.

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

  12. Весь код исполняемых объектов  (функции, процедуры, триггеры и т.д.) должен содержать подробное описание, желательно с работающими примерами вызовов и результатами.

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

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

Рефакторинг и сопровождение баз данных

  1. Настройте и протестируйте механизмы бекапов.

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

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

  4. Запланируйте регулярный аудит схем БД, например, удаляйте неиспользуемые колонки.

  5. Поскольку SQL код бывает достаточно плотным и не всегда хорошо читабельным, старайтесь оставлять подробные комментарии в данных случаях.

  6. Старайтесь комментировать или давать описательные имена всем магическим константам, также иногда можно выносить константы в простые SQL функции.

  7. Для улучшения читабельности рассмотрите возможность использования CTE.

  8. В определенных условиях не всегда удается стандартными SQL средствами  получить требуемую производительность. В данном случае можно использовать кэширующие таблицы, вычисляемые хранимые колонки, материализованные представления и прочие механизмы и методы для достижения требуемых параметров производительности.

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

  10. Если при проектировании таблиц заранее известно про очень большую размерность таблиц, необходимо предусмотреть механизм секционирования таблиц.

  11. Все изменения в объектах БД также необходимо хранить в GIT или любой другой используемой на проекте системе контроля версий.

  12. Настройте логирование изменений объектов БД, обычно это триггер уровня БД и таблица, в которую триггер записывает все изменения в объектах БД.

Разработка клиентских приложений и сервисов с использованием баз данных

  1. Следите за размером строк в байтах, генерируемые запросами на чтение данных. Неоправданно большие размеры строк могут негативно сказаться на производительности, как БД, так и клиентских приложений. В исключительных случаях можно увеличить параметр FetchSize в провайдере данных для ускорения получения данных клиентским приложением.

  2. Некоторые базы данных и провайдеры обеспечивают возможности для эффективной отмены SQL запросов, в данном случае в интерфейс можно добавить кнопку для отмены запросов.

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

  4. В проектах с большой нагрузкой постарайтесь изучить и правильно использовать ConnectionPool.

  5. Для повышения производительности приложений рассмотрите возможность использования асинхронных запросов.

  6. Для повышения производительности приложений старайтесь по возможности кешировать данные полученные из базы данных на клиенте.

  7. С некоторыми операциями базы данных справляются достаточно плохо, в данном случае рассмотрите вопрос переноса операции на сторону клиента, например, склеивание blob файлов в MS-SQL может быть достаточно медленным.

Комментарии (14)


  1. Tzimie
    17.11.2023 15:22
    +8

    1. Делайте хорошо и не делайте плохо


  1. zubrbonasus
    17.11.2023 15:22

    Чем плоха таблица с col1487 находящаяся во второй нф? Например есть котельная и в ней 5000 приборов и часть метрик принадлежат одной группе, и сохраняются в одну таблицу.

    Для рефакторинга и сопровождения (называется оптимизация и нормализация бд) я бы посоветовал иметь в наличии тестовые скрипты, которые помогут протестировать любой форк базы и показать 95% процентиль, время отклика, и прочие метрики бд.


    1. kAIST
      17.11.2023 15:22

      Может проще использовать другой тип баз данных, который больше подходит под хранение таких данных?


      1. ptr128
        17.11.2023 15:22

        Зачем? Подобная информация замечательно может храниться в массивах. У меня такие массивы и по 100 тыс. элементов встречаются.


      1. savostin
        17.11.2023 15:22

        А индексы как же?


        1. ptr128
          17.11.2023 15:22

          Так речь идёт о данных, которые не надо индексировать. Если Вам нужно индексировать меры, а не измерения и атрибуты, то Вам в columnstore.


  1. ptr128
    17.11.2023 15:22
    +2

    широкая таблица

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

    Общее количество полей в таблице существенного значения не имеет. Горизонтальное разделение таблицы на две с отношением один-к-одному имеет смысл только в редких сценариях, когда часть полей в запросах используется на 2 и более порядка реже, чем другая часть.

    лучше использовать третью нормальную форму

    Лучше стремиться к максимально нормализованной БД, но до тех пор, пока это не приводит к деградации производительности. А без нарушений третей нормальной формы, например, для хранения агрегатов, обойтись очень сложно.

    минимизировать использование вызовы функций в условиях WHERE

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

    При написании SQL запросов, особое внимание стоит обращать на количество логических чтений

    План запроса надо смотреть в первую очередь, а не количество логических чтений.

    не рекомендуется использовать такой механизм баз данных как хранимые процедуры

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

    С некоторыми операциями базы данных справляются достаточно плохо

    Лучше решать эти проблемы функциями. На PostgreSQL, после C, наилучший результат дает Rust, хотя Java или Python часто тоже достаточно. На MS SQL - С#.


    1. alixa Автор
      17.11.2023 15:22

      Если полностью раскрывать все вопросы, то это уже целая книга будет :) На это я пойти не готов (пока).


    1. alixa Автор
      17.11.2023 15:22

      Contragents, Cols: Addr1, Addr2, Phone, MobilePhone - ну это уж точно широкая таблица.

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


      1. ptr128
        17.11.2023 15:22

        Contragents, Cols: Addr1, Addr2, Phone, MobilePhone - ну это уж точно широкая таблица.

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

        Ширина таблицы - это количество полей в ней, а не их семантика.


  1. Akina
    17.11.2023 15:22

    В дополнение к пункту 3.

    Настоятельно рекомендую использовать одинаковые имена для референсных полей в обеих таблицах. В остальном - использовать имена полей (или пар полей для референсных), уникальные в рамках базы данных. NATURAL JOIN и JOIN USING - весьма удобные и логичные конструкции.

    Также рекомендую формировать имена вложенных объектов (констрейнты, индексы, триггеры и пр.) таким образом, чтобы они однозначно определяли базовый объект. Следствие - формировать эти имена явно, а не полагаться на автоматически создаваемые, когда это возможно.

    По пункту 6.

    Использованный термин SARGable (пишется именно так) надо было описывать в пункте 5. А сам пункт 6 после этого - вообще выбросить.

    По пункту 9.

    Это в основном болезнь (причём хроническая) всех фреймворков - либо в них вообще отсутствует поддержка хранимых объектов, либо она куцая и выполнена через универсальный интерфейс. И если на отсутствие поддержки ХП ещё можно наплевать и забыть, то вот то же для триггеров - это пичалька.

    По пункту 17.

    Вы свалили в одну кучу набор БД в рамках одного инстанса СУБД, и набор инстансов СУБД.

    По пункту 21.

    Применимость зависит от того, как СУБД обрабатывает CTE. Например, старые версии Постгресса однозначно материализуют результат CTE... А потому совет запросто может оказаться вредным.

    По пункту 23.

    Вообще неясно, о чём именно речь. Не используйте TEXT, если нужно хранить CHAR(1), что ли?

    По пункту 24.

    О чём речь? партиционирование? горизонтальный шардинг? вертикальный? что-то ещё?


    1. savostin
      17.11.2023 15:22
      +1

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

      А ну-ка покажите Ваши join'ы


      1. Akina
        17.11.2023 15:22

        ну-ка покажите Ваши join'ы

        SELECT * FROM t1 NATURAL JOIN t2;

        SELECT * FROM t3 JOIN t4 USING (t3_id)


  1. savostin
    17.11.2023 15:22

    ...