«Database as Сode» Experience

pduuyeiseyxkeubge4k8exyb7gq.png

SQL, что может быть проще? Каждый из нас может написать простенький запрос — набираем select, перечисляем необходимые колонки, затем from, имя таблицы, немного условий в where и все — полезные данные у нас в кармане, причем (почти) независимо от того какая СУБД в это время находится под капотом (а может и не СУБД вовсе). В результате работу практически с любым источником данных (реляционным и не очень) можно рассматривать с точки зрения обычного кода (со всеми вытекающими — version control, code review, статический анализ, автотесты и вот это все). И это касается не только самих данных, схем и миграций, а вообще всей жизнедеятельности хранилища. В этой статье поговорим о повседневных задачах и проблемах работы с различными БД под прицелом «database as code».

И начнем прямо с ORM. Первые батлы вида «SQL vs ORM» были замечены еще в допетровской Руси.


Объектно-реляционный мапинг

Сторонники ORM традиционно ценят скорость и простоту разработки, независимость от СУБД и чистоту кода. Для многих из нас код работы с БД (а зачастую и сама БД)


обычно выглядит примерно так…
@Entity
@Table(name = "stock", catalog = "maindb", uniqueConstraints = {
        @UniqueConstraint(columnNames = "STOCK_NAME"),
        @UniqueConstraint(columnNames = "STOCK_CODE") })
public class Stock implements java.io.Serializable {

    @Id
    @GeneratedValue(strategy = IDENTITY)
    @Column(name = "STOCK_ID", unique = true, nullable = false)
    public Integer getStockId() {
        return this.stockId;
    }
  ...

Модель обвешена умными аннотациями, а где-то за кулисами доблестный ORM генерирует и выполняет тонны какого-то SQL-кода. К слову сказать, разработчики всеми силами пытаются отгородиться от своей БД километрами абстракций, что говорит о некоторой «SQL ненависти».

По другую сторону баррикад приверженцы чистого «handmade»-SQL отмечают возможность выжимать все соки из своей СУБД без дополнительных прослоек и абстракций. В результате чего появляются «data-centric» проекты, где базой занимаются специально обученные люди (они же «базисты», они же «базовики», они же «базденьщики» и т.д.), а разработчикам только остается «дергать» готовые вьюхи и хранимые процедурки, не вдаваясь в подробности.

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


Clojure это крутой язык для создания DSL’ей, но SQL уже сам по себе является крутым DSL, и нам не нужен еще один. S-выражения прекрасны, но здесь они не добавляют ничего нового. В итоге получаем скобки ради скобок. Не согласны? Тогда дождитесь того момента когда абстракция над БД даст течь, и вы начнете борьбу с функцией (raw-sql)

И что делать? Давайте оставим SQL обычным SQL’ем — один файл на один запрос:

-- name: users-by-country
select *
  from users
 where country_code = :country_code


…, а затем прочитайте этот файл, превратив его в обычную Clojure функцию:
(defqueries "some/where/users_by_country.sql"
   {:connection db-spec})

;;; A function with the name `users-by-country` has been created.
;;; Let's use it:
(users-by-country {:country_code "GB"})
;=> ({:name "Kris" :country_code "GB" ...} ...)


Придерживаясь принципа «SQL отдельно, Clojure отдельно», вы получаете:
  • Никаких синтаксических сюрпризов. Ваша база данных (как и любая другая) не соответствует SQL стандарту на 100% —, но для Yesql это не важно. Вы никогда не будете тратить время на охоту за функциями с синтаксисом эквивалентным SQL. Вам никогда не придется возвращаться к функции (raw-sql «some ('funky' :: SYNTAX)»)).
  • Лучшая поддержка редактора. Ваш редактор уже имеет отличную поддержку SQL. Сохраняя SQL как SQL, вы можете просто использовать его.
  • Командная совместимость. Ваши DBA могут читать и писать SQL, который вы используете в своем Clojure проекте.
  • Более простая настройка производительности. Нужно построить план для проблемного запроса? Это не проблема, когда ваш запрос является обычным SQL.
  • Повторное использование запросов. Перетащите эти же SQL-файлы в другой проекты, потому что это просто старый добрый SQL — просто поделитесь им.

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


IDE & DB-менеджеры

Начнем с простой повседневной задачи. Часто нам приходится искать какие-либо объекты в БД, например, найти таблицу в схеме и изучить ее структуру (какие используются колонки, ключи, индексы, констрейнты и прочее). И от любой графической IDE или маломальского DB-manager’а, в первую очередь, мы ждем именно этих способностей. Чтобы было быстро и не пришлось ждать по полчаса, пока нарисуется окошко с нужной информацией (особенно при медленном соединении с удаленной БД), и при этом чтобы полученная информация была свежей и актуальной, а не закешированное старье. Причем чем сложнее и крупнее БД и больше их количество, тем сложнее это сделать.

Но обычно я забрасываю мышь куда подальше и просто пишу код. Допустим, необходимо узнать, какие таблицы (и с какими свойствами) содержаться в схеме «HR». В большинстве СУБД нужного результата можно добиться вот таким нехитрым запросом из information_schema:

select table_name
     , ...
  from information_schema.tables
 where schema = 'HR'

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

select table_name
     , storage_engine -- Используемый "движок" ("MyISAM", "InnoDB" etc)
     , row_format     -- Формат строки ("Fixed", "Dynamic" etc)
     , ...
  from information_schema.tables
 where schema = 'HR'

Oracle не умеет information_schema, зато у него есть Oracle metadata, и больших проблем не возникает:

select table_name
     , pct_free       -- Минимум свободного места в блоке данных (%)
     , pct_used       -- Минимум используемого места в блоке данных (%)
     , last_analyzed  -- Дата последнего сбора статистики
     , ...
  from all_tables
 where owner = 'HR'

Не исключение и ClickHouse:

select name
     , engine -- Используемый "движок" ("MergeTree", "Dictionary" etc)
     , ...
  from system.tables
 where database = 'HR'

Что-то похожее можно сделать и в Cassandra (где есть columnfamilies вместо tables и keyspace’ы вместо схем):

select columnfamily_name
     , compaction_strategy_class  -- Стратегия сборки мусора
     , gc_grace_seconds           -- Время жизни мусора
     , ...
  from system.schema_columnfamilies
 where keyspace_name = 'HR'

Для большинства остальных БД также можно придумать похожие запросы (даже в Mongo есть специальная системная коллекция, которая содержит в себе информацию о всех коллекциях в системе).

Само собой, таким способом можно получить информацию не только о таблицах, а вообще о любом объекте. Периодически добрые люди делятся таким кодом для разных БД, как, например, в серии хабра-статей «Функции для документирования баз данных PostgreSQL» (айб, бен, гим). Само собой, держать всю эту гору запросов в голове и постоянно набирать их — это «такое себе» удовольствие, поэтому в любимой IDE/редакторе у меня есть заранее заготовленный набор сниппетов для часто используемых запросов, и остается только впечатать имена объектов в шаблон.

В итоге такой способ навигации и поиска объектов гораздо более гибок, экономит много времени, позволяет получить именно ту информацию и в том виде, в котором сейчас необходимо (как, например, описано в посте «Экспорт данных из БД в любом формате: что умеют IDE на платформе IntelliJ»).


Операции с объектами

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

Не секрет, что простое удаление таблицы будет выглядеть одинаково почти во всех БД:

drop table hr.persons

А вот с созданием таблицы уже поинтереснее. Практически любая СУБД (в том числе и многие NoSQL) в том или ином виде умеет «create table», и основная его часть даже мало будет отличаться (имя, список колонок, типы данных), но остальные детали могут разительно отличаться и зависят от внутреннего устройства и возможностей конкретной СУБД. Мой любимый пример — в документации Oracle только одни «голые» БНФ'ы для синтаксиса «create table» занимают 31 страницу. Другие СУБД обладают более скромными возможностями, но каждая из них также обладает множеством интересных и уникальных фич по созданию таблиц (postgres, mysql, cockroach, cassandra). Вряд ли какой-нибудь графический «wizard» из очередной IDE (особенно универсальной) сможет полностью покрыть все эти способности, а если и сможет, то это будет зрелище не для слабонервных. В то же время правильно и вовремя написанный оператор create table позволит без труда воспользоваться всеми из них, сделать хранение и доступ к вашим данным надежным, оптимальным и максимально комфортным.

Также во многих СУБД есть свои специфичные типы объектов, которые отсутствуют в других СУБД. Причем мы можем выполнять операции не только над объектами БД, но и над самой СУБД, например «убить» процесс, освободить какую-либо область памяти, включить трассировку, перейти в режим «read only» и многое другое.


А теперь немного порисуем

Одна из самых распространенных задач — построить диаграмму с объектами БД, на красивой картинке увидеть объекты и связи между ними. Это умеет практически любая графическая IDE, отдельные «command line»-утилиты, специализированные графические тулы и моделлеры. Которые вам что-то нарисуют «как умеют», а немного повлиять на этот процесс можно только с помощью нескольких параметров в конфигурационном файле или галочек в интерфейсе.

Но эту проблему можно решить гораздо проще, гибче и элегантнее, и конечно же с помощью кода. Для построения диаграмм любой сложности у нас есть сразу несколько специализированных языков разметки (DOT, GraphML etc), а к ним — целая россыпь приложений (GraphViz, PlantUML, Mermaid), которые умеют читать такие инструкции и визуализировать в самых разных форматах. Ну, а информацию об объектах и связях между ними мы уже знаем как получить.

Приведем небольшой пример того, как это могло бы выглядеть, с использованием PlantUML и демонстрационной база данных для PostgreSQL (слева SQL-запрос, который сгенерирует нужную инструкцию для PlantUML, а справа результат):


8vemroactkr89t2prtqap-2i1yg.png

select '@startuml'||chr(10)||'hide methods'||chr(10)||'hide stereotypes' union all
select distinct ccu.table_name || ' --|> ' ||
       tc.table_name as val
  from table_constraints as tc
  join key_column_usage as kcu
    on tc.constraint_name = kcu.constraint_name
  join constraint_column_usage as ccu
    on ccu.constraint_name = tc.constraint_name
 where tc.constraint_type = 'FOREIGN KEY'
   and tc.table_name ~ '.*' union all
select '@enduml'

А если немного постараться, то на основе ER-шаблона для PlantUML можно получить что-то сильно похожее на настоящую ER-диаграмму:


SQL-запрос чуууть посложнее
-- Шапка
select '@startuml
        !define Table(name,desc) class name as "desc" << (T,#FFAAAA) >>
        !define primary_key(x) x
        !define unique(x) x
        !define not_null(x) x
        hide methods
        hide stereotypes'
 union all
-- Таблицы
select format('Table(%s, "%s \n information about %s") {'||chr(10), table_name, table_name, table_name) ||
       (select string_agg(column_name || ' ' || upper(udt_name), chr(10))
          from information_schema.columns
         where table_schema = 'public'
           and table_name = t.table_name) || chr(10) || '}'
  from information_schema.tables t
 where table_schema = 'public'
 union all
-- Связи между таблицами
select distinct ccu.table_name || ' "1" --> "0..N" ' || tc.table_name || format(' : "A %s may have\n many %s"', ccu.table_name, tc.table_name)
  from information_schema.table_constraints as tc
  join information_schema.key_column_usage as kcu on tc.constraint_name = kcu.constraint_name
  join information_schema.constraint_column_usage as ccu on ccu.constraint_name = tc.constraint_name
 where tc.constraint_type = 'FOREIGN KEY'
   and ccu.constraint_schema = 'public'
   and tc.table_name ~ '.*'
 union all
-- Подвал
select '@enduml'


szz_7zmn56d3858gqg60d4f00be.png

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


Метрики и мониторинг

Перейдем к традиционно сложной теме — мониторинг производительности БД. Вспомню небольшую true story, рассказанную мне «одним моим другом». На очередном проекте жил-был некий могущественный DBA, и мало кто из разработчиков был с ним знаком лично, да и вообще видел когда-нибудь его в глаза (несмотря на то, что трудился он, по слухам, где-то в соседнем корпусе). В час «X», когда poduction-система крупного ретейлера начинала в очередной раз «плохо себя чувствовать», он молча присылал скриншоты графиков из ораклового Enterprise Manager, на которых бережно выделял критичные места красным маркером для «понятности» (это, мягко говоря, мало помогало). И вот по этой «фотокарточке» приходилось лечить. При этом доступа к драгоценному (в обоих смыслах этого слова) Enterprise Manager ни у кого не было, т.к. система сложная и дорогая, вдруг «разрабы чего-нить натыкают и все поломают». Поэтому разработчики «эмпирическим» путем находили место и причину тормозов и выпускали патч. Если грозное письмо от DBA не приходило повторно в ближайшее время, то все с облегчением выдыхали и возвращались к своим текущим задачам (до нового Письма).

Но процесс мониторинга может выглядеть более весело и дружелюбно, а самое главное — доступно и прозрачно для всех. Хотя бы базовая его часть, как дополнение к основным системам мониторинга (которые безусловно полезны и во многих случаях незаменимы). Любая СУБД свободно и абсолютно безвозмездно готова поделиться информацией о своем текущем состоянии и производительности. В той же самой «кровавой» Oracle DB практически любую информацию о производительности можно получить из системных представлений, начиная от процессов и сессий и заканчивая состоянием буферного кеша (например, DBA Scripts, раздел «Monitoring»). В Postgresql также есть целая россыпь системных представлений для мониторинга работы БД, в частности такие незаменимые в повседневной жизни любого DBA, как pg_stat_activity, pg_stat_database, pg_stat_bgwriter. В MySQL для этого предназначена даже отдельная схема performance_schema. А В Mongo встроенный профайлер агрегирует данные о производительности в системную коллекцию system.profile.

Т.о., вооружившись каким-либо сборщиком метрик (Telegraf, Metricbeat, Collectd), который умеет выполнять кастомные sql-запросы, хранилищем этих метрик (InfluxDB, Elasticsearch, Timescaledb) и визуализатором (Grafana, Kibana), можно получить достаточно легкую и гибкую систему мониторинга, которая будет тесно интегрирована с другими общесистемными метриками (получаемыми, например, от сервера приложений, от ОС и пр.). Как, например, это сделано в pgwatch2, где используется связка InfluxDB + Grafana и набор запросов к системным представлениям, к которым также можно добавить кастомные запросы.


Итого

И это только приблизительный перечень того, что можно сделать с нашей БД посредством обычного SQL-кода. Уверен, можно найти еще множество применений, пишите в комментариях. А о том, как (и самое главное зачем) это все заавтоматизировать и включить в свой CI/CD pipeline мы поговорим в следующий раз.

© Habrahabr.ru