Doświadczenie «Database as Code»

Doświadczenie «Database as Code»

SQL, co może być prostszego? Każdy z nas może napisać prosty zapytanie — wpisujemy select, wymieniamy potrzebne kolumny, potem od, nazwę tabeli, trochę warunków w gdzie i to wszystko — przydatne dane mamy w kieszeni, niezależnie od tego, jaka baza danych jest aktualnie używana (a może i wcale nie baza danych). W rezultacie pracę praktycznie z każdym źródłem danych (relacyjnym i nie tylko) można rozważać w kategoriach zwykłego kodu (ze wszystkimi tego konsekwencjami — kontrola wersji, przegląd kodu, analiza statyczna, testy automatyczne i tym podobne). A to dotyczy nie tylko samych danych, schematów i migracji, ale w ogóle całego życia magazynu. W tym artykule porozmawiamy o codziennych zadaniach i problemach związanych z różnymi bazami danych w kontekście "bazy danych jako kod".

Zaczniemy od ORM. Pierwsze starcia typu "SQL vs ORM" zauważono już w przedpotopowej Rosji.

Mapowanie obiektowo-relacyjne

Zwolennicy ORM tradycyjnie cenią szybkość i prostotę tworzenia, niezależność od bazy danych oraz czystość kodu. Dla wielu z nas kod pracy z bazą danych (a często i sama baza danych)

zwykle wygląda mniej więcej tak…

@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;
    }
  ...

Model jest obciążony mądrymi adnotacjami, a gdzieś w tle dzielny ORM generuje i wykonuje tony jakiegoś kodu SQL. Swoją drogą, programiści za wszelką cenę starają się oddzielić swoje bazy danych od siebie, tworząc kilometry abstrakcji, co świadczy o pewnej "nienawiści do SQL".

Po drugiej stronie barykady zwolennicy czystego "ręcznie pisanego" SQL zauważają możliwość wyciskania z bazy danych każdej kropli bez dodatkowych warstw i abstrakcji. W rezultacie pojawiają się projekty "data-centric", w których bazą zajmują się specjalnie przeszkolone osoby (zwane "bazistami", "bazowikami", "baza-derkami" itp.), a programiści tylko "wyciągają" gotowe widoki i procedury składowane, nie zagłębiając się w szczegóły.

A co jeśli weźmiemy to, co najlepsze z dwóch światów? Jak to jest zrealizowane w wspaniałym narzędziu o afirmatywnym tytule Yesql. Przytoczę kilka zdań z ogólnej koncepcji w moim swobodnym tłumaczeniu, a bardziej szczegółowo z nią można się zapoznać tutaj.

Clojure to wspaniały język do tworzenia DSL, ale SQL sam w sobie jest świetnym DSL i nie potrzebujemy kolejnego. Wyrażenia S są piękne, ale tutaj nie wnoszą nic nowego. W efekcie mamy nawiasy dla samych nawiasów. Nie zgadzasz się? Poczekaj do momentu, gdy abstrakcja nad bazą danych zacznie przeciekać, a Ty rozpoczniesz walkę z funkcją. (raw-sql)

I co robić? Pozwólmy, aby SQL pozostał zwykłym SQL — jeden plik na jeden zapytanie:

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

… a następnie przeczytaj ten plik, przekształcając go w zwykłą funkcję Clojure:

(defqueries "some/where/users_by_country.sql"
   {:connection db-spec})

;;; Utworzono funkcję o nazwie `users-by-country`.
;;; Użyjmy jej:
(users-by-country {:country_code "GB"})
;=> ({:name "Kris" :country_code "GB" ...} ...)

Przestrzegając zasady "SQL osobno, Clojure osobno", otrzymujesz:

  • Brak zaskoczeń składniowych. Twoja baza danych (jak każda inna) nie odpowiada standardowi SQL w 100% — ale dla Yesql to nie ma znaczenia. Nigdy nie będziesz tracić czasu na polowanie na funkcje o składni odpowiadającej SQL. Nigdy nie będziesz musiał wracać do funkcji. (raw-sql "some (‘funky’ :: SYNTAX)").
  • Najlepsze wsparcie edytora. Twój edytor ma już doskonałe wsparcie dla SQL. Utrzymując SQL jako SQL, możesz po prostu go używać.
  • Zgodność zespołowa. Twoi DBA mogą czytać i pisać SQL, którego używasz w swoim projekcie Clojure.
  • Łatwiejsza konfiguracja wydajności. Musisz zbudować plan dla problematycznego zapytania? To nie problem, gdy Twoje zapytanie jest zwykłym SQL.
  • Ponowne użycie zapytań. Przenieś te same pliki SQL do innych projektów, ponieważ to po prostu stary dobry SQL — po prostu podziel się nim.

Moim zdaniem pomysł jest bardzo fajny, a przy tym bardzo prosty, dzięki czemu projekt zyskał wiele. naśladowców w różnych językach. A my następnie spróbujemy zastosować podobną filozofię oddzielania kodu SQL od reszty dumnie poza ORM.

IDE & DB-menadżery

Zacznijmy od prostej, codziennej czynności. Często musimy wyszukiwać różne obiekty w bazie danych, na przykład znaleźć tabelę w schemacie i zbadać jej strukturę (jakie kolumny, klucze, indeksy, ograniczenia i inne są używane). Od każdej graficznej IDE lub jakiegokolwiek DB-managera oczekujemy przede wszystkim tych umiejętności. Aby było szybko i nie trzeba było czekać pół godziny, aż pojawi się okno z potrzebnymi informacjami (szczególnie przy wolnym połączeniu z zdalną bazą danych), a jednocześnie, aby uzyskane informacje były świeże i aktualne, a nie przestarzałe w pamięci podręcznej. Co więcej, im bardziej złożona i większa baza danych oraz im więcej ich jest, tym trudniejsze staje się to zadanie.

Ale zazwyczaj odstawiam myszkę na bok i po prostu piszę kod. Załóżmy, że trzeba dowiedzieć się, jakie tabele (i jakie mają właściwości) znajdują się w schemacie 'HR'. W większości systemów baz danych można uzyskać potrzebny wynik za pomocą takiego prostego zapytania z information_schema:

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

Od bazy do bazy zawartość takich tabel referencyjnych różni się w zależności od możliwości każdej bazy danych. Na przykład, dla MySQL z tej samej tablicy można uzyskać specyficzne dla tej bazy dane tabeli:

select table_name
     , storage_engine -- Używany 'silnik' ('MyISAM', 'InnoDB' itd.)
     , row_format     -- Format wiersza ('Fixed', 'Dynamic' itd.)
     , ...
  from information_schema.tables
 where schema = 'HR'

Oracle nie obsługuje information_schema, ale ma metadane Oracle, i nie ma większych problemów:

select table_name
     , pct_free       -- Minimalna ilość wolnego miejsca w bloku danych (%)
     , pct_used       -- Minimalna ilość zajętego miejsca w bloku danych (%)
     , last_analyzed  -- Data ostatniego zbierania statystyk
     , ...
  from all_tables
 where owner = 'HR'

Nie inaczej jest z ClickHouse:

select name
     , engine -- Używany 'silnik' ('MergeTree', 'Dictionary' itd.)
     , ...
  from system.tables
 where database = 'HR'

Podobne zapytanie można wykonać również w Cassandrze (gdzie są columnfamilies zamiast tabel i keyspace zamiast schematów):

select columnfamily_name
     , compaction_strategy_class  -- Strategia kompresji
     , gc_grace_seconds           -- Czas życia danych
     , ...
  from system.schema_columnfamilies
 where keyspace_name = 'HR'

Dla większości innych baz danych również można wymyślić podobne zapytania (nawet w Mongo jest specjalna kolekcja systemowa, która zawiera informacje o wszystkich kolekcjach w systemie).

Oczywiście, w ten sposób można uzyskać informacje nie tylko o tabelach, ale o każdym obiekcie. Co jakiś czas dobrzy ludzie dzielą się takim kodem dla różnych baz danych, jak na przykład w serii artykułów na Habra dotyczących "Funkcje do dokumentowania baz danych PostgreSQL" (ajb, ben, gim). Oczywiście, trzymanie całej tej masy zapytań w głowie i ciągłe ich wpisywanie to "nieduża przyjemność", dlatego w mojej ulubionej IDE/edytorze mam wcześniej przygotowany zestaw snippetów dla często używanych zapytań, wystarczy tylko wpisać nazwy obiektów w szablon.

W rezultacie taki sposób nawigacji i wyszukiwania obiektów jest znacznie bardziej elastyczny, oszczędza dużo czasu, pozwala uzyskać dokładnie te informacje i w takim formacie, w jakim są teraz potrzebne (jak opisano na przykład w poście "Eksport danych z Bazy Danych w dowolnym formacie: co potrafią IDE na platformie IntelliJ").

Operacje z obiektami

Po tym, jak znaleźliśmy i przeanalizowaliśmy potrzebne obiekty, nastał czas na zrobienie z nimi czegoś użytecznego. Oczywiście, również nie odrywając palców od klawiatury.

Nie jest tajemnicą, że proste usunięcie tabeli będzie wyglądać w zasadzie identycznie w niemal wszystkich bazach danych:

drop table hr.persons

Ale tworzenie tabeli jest już bardziej interesujące. Właściwie każda SGBD (w tym wiele NoSQL) w takim czy innym stopniu potrafi używać "create table", a jej główna część będzie się nawet niewiele różnić (nazwa, lista kolumn, typy danych), ale inne szczegóły mogą się znacznie różnić i zależą od wewnętrznej budowy i możliwości konkretnej SGBD. Mój ulubiony przykład — w dokumentacji Oracle same "gołe" BNF’y dla składni "create table" zajmują 31 stron. Inne SGBD mają bardziej skromne możliwości, ale każda z nich także dysponuje wieloma interesującymi i unikalnymi funkcjami przy tworzeniu tabel (postgres, mysql, cockroach, cassandra). Mało prawdopodobne, że jakiś graficzny "wizard" z kolejnej IDE (szczególnie uniwersalnej) będzie w stanie w pełni pokryć wszystkie te możliwości, a jeśli nawet to zrobi, to będzie to widok nie dla ludzi o słabych nerwach. W międzyczasie dobrze i na czas napisany operator create table pozwoli bez trudu skorzystać ze wszystkich z nich, zapewniając, że przechowywanie i dostęp do twoich danych będą niezawodne, optymalne i maksymalnie komfortowe.

W wielu systemach zarządzania bazami danych istnieją specyficzne typy obiektów, które są nieobecne w innych. Możemy wykonywać operacje nie tylko na obiektach bazy danych, ale także na samej bazie, na przykład "zabić" proces, zwolnić określoną przestrzeń pamięci, włączyć śledzenie, przełączyć się w tryb "tylko do odczytu" i wiele innych.

A teraz trochę pokreatywujemy

Jednym z najczęściej spotykanych zadań jest stworzenie diagramu z obiektami bazy danych, aby zobaczyć obiekty i ich wzajemne powiązania na ładnym obrazku. Z tym poradzi sobie praktycznie każda graficzna IDE, oddzielne narzędzia "command line", specjalistyczne narzędzia graficzne i modelery. Które coś narysują "jak potrafią", ale wpływ na ten proces można mieć jedynie poprzez kilka parametrów w pliku konfiguracyjnym lub zaznaczenia w interfejsie.

Ale ten problem można rozwiązać znacznie prościej, elastyczniej i elegancko, oczywiście za pomocą kodu. Do rysowania diagramów o dowolnym poziomie skomplikowania mamy kilka specjalistycznych języków znaczników (DOT, GraphML itd.), a do nich całą gamę aplikacji (GraphViz, PlantUML, Mermaid), które potrafią czytać takie instrukcje i wizualizować w najróżniejszych formatach. A informacje o obiektach i ich powiązaniach już wiemy, jak zdobyć.

Podajmy mały przykład, jak to mogłoby wyglądać, z użyciem PlantUML i demonstracyjnej bazy danych dla PostgreSQL (po lewej SQL zapytanie, które wygeneruje potrzebną instrukcję dla PlantUML, a po prawej rezultat):

Doświadczenie «Database as Code»

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'

A jeśli trochę się postarasz, to na podstawie szablonu ER dla PlantUML można uzyskać coś mocno przypominającego prawdziwy diagram ER:

Zapytanie SQL jest o odrobinę bardziej skomplikowane

-- Nagłówek
select '@startuml
        !define Table(name,desc) class name as "desc" << (T,#FFAAAA) >&gt;
        !define primary_key(x) <b>x</b>
        !define unique(x) <color:green>x</color>
        !define not_null(x) <u>x</u>
        hide methods
        hide stereotypes'
 union all
-- Tabele
select format('Table(%s, "%s n informacje o %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
-- Relacje między tabelami
select distinct ccu.table_name || ' "1" --&gt; "0..N" ' || tc.table_name || format(' : "A %s może mieć wiele %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
-- Stopka
select '@enduml'

Doświadczenie «Database as Code»

Jeśli uważnie się przyjrzysz, to pod maską wiele narzędzi wizualizacyjnych używa podobnych zapytań. Jednak są one zazwyczaj głęboko "wbudowane" w kod samej aplikacji i trudne do zrozumienia, nie mówiąc już o jakiejkolwiek ich modyfikacji.

Metryki i monitoring

Przejdźmy do tradycyjnie trudnego tematu — monitorowanie wydajności baz danych. Przypomnę sobie małą prawdziwą historię, którą opowiedział mi "jeden z moich przyjaciół". Na jednym z projektów żył sobie potężny DBA, a mało kto z programistów znał go osobiście, nawet nie widział go nigdy na oczy (mimo że podobno pracował gdzieś w sąsiednim budynku). W godzinie "X", gdy produkcyjny system dużego detalisty ponownie zaczynał "źle się czuć", bez słowa przesyłał zrzuty ekranowe wykresów z Oracle Enterprise Manager, na których starannie zaznaczał krytyczne miejsca czerwonym markerem dla "lepszej czytelności" (co łagodnie mówiąc, mało pomagało). I po tym "zdjęciu" trzeba było leczyć. Przy tym nikt nie miał dostępu do cennego (w obu znaczeniach tego słowa) Enterprise Manager, ponieważ system był skomplikowany i drogi, żeby nie "wpadli w coś programiści i wszystkiego nie zepsuli". Dlatego programiści "empirycznie" znajdowali miejsce i przyczynę zacięć i wydawali poprawkę. Jeśli groźny list od DBA nie przychodził ponownie w najbliższym czasie, wszyscy z ulgą wypuszczali powietrze i wracali do swoich bieżących zadań (do nowego Pisma).

Jednak proces monitorowania może wyglądać bardziej wesoło i przyjaźnie, a co najważniejsze — być dostępny i przejrzysty dla wszystkich. Przynajmniej jego podstawowa część, jako dodatek do głównych systemów monitorowania (które są niewątpliwie przydatne i w wielu przypadkach niezastąpione). Każda SGBD jest gotowa bezpłatnie podzielić się informacjami o swoim aktualnym stanie i wydajności. W tej samej "krwawej" bazie danych Oracle prawie wszystkie informacje o wydajności można uzyskać z widoków systemowych, zaczynając od procesów i sesji, aż po stan buforowego cache'a (na przykład, Skrypty DBA, sekcja "Monitorowanie"). W PostgreSQL również istnieje cała gama widoków systemowych do monitorowania działania baz danych, w szczególności takie niezbędne w codziennej pracy każdego DBA, jak pg_stat_activity, pg_stat_database, pg_stat_bgwriter. W MySQL z tego powodu jest nawet oddzielny schemat performance_schema. A w MongoDB wbudowany profiler agreguje dane o wydajności w kolekcji systemowej system.profile.

W ten sposób, uzbrojony w dowolny program do zbierania metryk (Telegraf, Metricbeat, Collectd), który potrafi wykonywać niestandardowe zapytania SQL, oraz system przechowywania tych metryk (InfluxDB, Elasticsearch, Timescaledb) i wizualizator (Grafana, Kibana), można stworzyć stosunkowo prosty i elastyczny system monitorowania, który będzie ściśle zintegrowany z innymi metrykami systemowymi (uzyskać je można na przykład z serwera aplikacji, systemu operacyjnego itp.). Jak to jest zrealizowane w pgwatch2, gdzie używana jest kombinacja InfluxDB + Grafana oraz zestaw zapytań do widoków systemowych, do których można również dodać niestandardowe zapytania.

Podsumowując

I to tylko przybliżony wykaz tego, co można zrobić z naszą bazą danych za pomocą standardowego kodu SQL. Jestem pewien, że można znaleźć jeszcze wiele zastosowań, piszcie w komentarzach. A o tym, jak (a co najważniejsze, dlaczego) można to wszystko zautomatyzować i włączyć do swojego pipeline'u CI/CD, porozmawiamy następnym razem.

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster