Experiencia "Base de datos como código"

Experiencia "Base de datos como código"

SQL, ¿qué podría ser más sencillo? Cada uno de nosotros puede escribir una solicitud simple: escribimos selecciona, enumere las columnas requeridas, luego Desde, nombre de la tabla, algunas condiciones en donde y eso es todo: los datos útiles están en nuestro bolsillo, y (casi) independientemente de qué DBMS esté bajo el capó en ese momento (o tal vez no es un DBMS en absoluto). Como resultado, trabajar con casi cualquier fuente de datos (relacional y no tan) se puede considerar desde el punto de vista del código ordinario (con todo lo que implica: control de versiones, revisión de código, análisis estático, pruebas automáticas y eso es todo). Y esto se aplica no sólo a los datos en sí, los esquemas y las migraciones, sino en general a toda la vida útil del almacenamiento. En este artículo hablaremos sobre las tareas y problemas cotidianos de trabajar con varias bases de datos bajo la lente de "base de datos como código".

Y comencemos desde ORM. Las primeras batallas del tipo "SQL vs ORM" se notaron allá por Rusia prepetrina.

Mapeo relacional de objetos

Los partidarios de ORM tradicionalmente valoran la velocidad y la facilidad de desarrollo, la independencia del DBMS y el código limpio. Para muchos de nosotros, el código para trabajar con la base de datos (y a menudo con la propia base de datos)

normalmente se parece a esto...

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

El modelo está adornado con anotaciones inteligentes y, en algún lugar detrás de escena, un valiente ORM genera y ejecuta toneladas de código SQL. Por cierto, los desarrolladores están haciendo todo lo posible para aislarse de su base de datos con kilómetros de abstracciones, lo que indica algunas "Odio SQL".

Del otro lado de las barricadas, los partidarios del SQL puro "hecho a mano" notan la capacidad de exprimir todo el jugo de su DBMS sin capas ni abstracciones adicionales. Como resultado, aparecen proyectos "centrados en datos", en los que personas especialmente capacitadas participan en la base de datos (también son "básicos", también son "básicos", también son "basdeners", etc.), y los desarrolladores Sólo hay que “tirar” de las vistas y procedimientos almacenados ya preparados, sin entrar en detalles.

¿Y si tuviéramos lo mejor de ambos mundos? Cómo se hace esto en una herramienta maravillosa con un nombre que afirma la vida ysql. Daré un par de líneas del concepto general en mi traducción gratuita y podrás familiarizarte con él con más detalle. aquí.

Clojure es un lenguaje genial para crear DSL, pero SQL en sí es un DSL genial y no necesitamos otro. Las expresiones S son geniales, pero no añaden nada nuevo aquí. Como resultado, obtenemos paréntesis por el bien de los paréntesis. ¿No estoy de acuerdo? Luego espera el momento en que la abstracción de la base de datos comience a filtrarse y comiences a pelear con la función. (sql sin formato)

¿Entonces qué debo hacer? Dejemos SQL como SQL normal: un archivo por solicitud:

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

... y luego lea este archivo, convirtiéndolo en una función normal de 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" ...} ...)

Al seguir el principio "SQL por sí solo, Clojure por sí mismo", se obtiene:

  • Sin sorpresas sintácticas. Su base de datos (como cualquier otra) no cumple al 100% con el estándar SQL, pero esto no importa para Yesql. Nunca perderá el tiempo buscando funciones con sintaxis equivalente a SQL. Nunca tendrás que volver a una función. (raw-sql "algunos('funky'::SINTAXIS)")).
  • Mejor soporte de editor. Su editor ya cuenta con un excelente soporte SQL. Al guardar SQL como SQL, simplemente puede usarlo.
  • Compatibilidad del equipo. Sus administradores de bases de datos pueden leer y escribir el SQL que utiliza en su proyecto Clojure.
  • Ajuste de rendimiento más sencillo. ¿Necesita crear un plan para una consulta problemática? Esto no es un problema cuando su consulta es SQL normal.
  • Reutilización de consultas. Arrastre y suelte esos mismos archivos SQL en otros proyectos porque es simplemente SQL antiguo: simplemente compártalo.

En mi opinión, la idea es genial y al mismo tiempo muy sencilla, gracias a la cual el proyecto ha ganado muchos seguidores en una variedad de idiomas. Y a continuación intentaremos aplicar una filosofía similar de separar el código SQL de todo lo demás más allá del ORM.

Administradores de IDE y bases de datos

Comencemos con una sencilla tarea cotidiana. A menudo tenemos que buscar algunos objetos en la base de datos, por ejemplo, encontrar una tabla en el esquema y estudiar su estructura (qué columnas, claves, índices, restricciones, etc. se utilizan). Y de cualquier IDE gráfico o de un pequeño administrador de bases de datos, en primer lugar, esperamos exactamente estas capacidades. Para que sea rápido y no tenga que esperar media hora hasta que se abra una ventana con la información necesaria (especialmente con una conexión lenta a una base de datos remota), y al mismo tiempo la información recibida sea fresca y relevante, y no basura almacenada en caché. Además, cuanto más compleja y grande sea la base de datos y mayor sea su número, más difícil será hacerlo.

Pero normalmente tiro el mouse y simplemente escribo código. Digamos que necesita saber qué tablas (y con qué propiedades) están contenidas en el esquema "HR". En la mayoría de los DBMS, el resultado deseado se puede lograr con esta simple consulta desde information_schema:

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

De una base de datos a otra, el contenido de dichas tablas de referencia varía según las capacidades de cada DBMS. Y, por ejemplo, para MySQL, del mismo libro de referencia puede obtener parámetros de tabla específicos para este DBMS:

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

Oracle no conoce el esquema_información, pero sí lo tiene. Metadatos de Oracle, y no surgen grandes problemas:

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

ClickHouse no es una excepción:

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

Se puede hacer algo similar en Cassandra (que tiene familias de columnas en lugar de tablas y espacios de claves en lugar de esquemas):

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

Para la mayoría de las otras bases de datos, también puede realizar consultas similares (incluso Mongo tiene colección de sistema especial, que contiene información sobre todas las colecciones del sistema).

Por supuesto, de esta forma podrás obtener información no solo sobre tablas, sino sobre cualquier objeto en general. De vez en cuando, personas amables comparten dicho código para diferentes bases de datos, como, por ejemplo, en la serie de artículos de habra "Funciones para documentar bases de datos PostgreSQL" (Ayb, бен, Gimnasio). Por supuesto, mantener toda esta montaña de consultas en mi cabeza y escribirlas constantemente es un gran placer, por lo que en mi IDE/editor favorito tengo un conjunto de fragmentos preparados previamente para consultas de uso frecuente, y todo lo que queda es escribir el nombres de objetos en la plantilla.

Como resultado, este método de navegación y búsqueda de objetos es mucho más flexible, ahorra mucho tiempo y le permite obtener exactamente la información en la forma en que ahora es necesaria (como, por ejemplo, se describe en la publicación "Exportar datos desde una base de datos en cualquier formato: qué pueden hacer los IDE en la plataforma IntelliJ").

Operaciones con objetos

Una vez que hayamos encontrado y estudiado los objetos necesarios, es hora de hacer algo útil con ellos. Por supuesto, también sin quitar los dedos del teclado.

No es ningún secreto que simplemente eliminar una tabla tendrá el mismo aspecto en casi todas las bases de datos:

drop table hr.persons

Pero con la creación de la mesa se vuelve más interesante. Casi cualquier DBMS (incluidos muchos NoSQL) puede "crear una tabla" de una forma u otra, y la parte principal incluso diferirá ligeramente (nombre, lista de columnas, tipos de datos), pero otros detalles pueden diferir dramáticamente y depender del dispositivo interno y capacidades de un DBMS específico. Mi ejemplo favorito es que en la documentación de Oracle solo hay BNF "desnudos" para la sintaxis de "crear tabla". ocupa 31 páginas. Otros DBMS tienen capacidades más modestas, pero cada uno de ellos también tiene muchas características interesantes y únicas para crear tablas (Postgres, mysql, cucaracha, cassandra). Es poco probable que cualquier "asistente" gráfico de otro IDE (especialmente uno universal) pueda cubrir completamente todas estas capacidades, e incluso si pudiera, no será un espectáculo para los débiles de corazón. Al mismo tiempo, una declaración escrita correcta y oportunamente. crear mesa te permitirá utilizarlos todos fácilmente, hacer que el almacenamiento y acceso a tus datos sea fiable, óptimo y lo más cómodo posible.

Además, muchos DBMS tienen sus propios tipos específicos de objetos que no están disponibles en otros DBMS. Además, podemos realizar operaciones no solo en los objetos de la base de datos, sino también en el propio DBMS, por ejemplo, "matar" un proceso, liberar un área de memoria, habilitar el seguimiento, cambiar al modo "solo lectura" y mucho más.

Ahora dibujemos un poco.

Una de las tareas más comunes es construir un diagrama con los objetos de la base de datos y ver los objetos y las conexiones entre ellos en una hermosa imagen. Casi cualquier IDE gráfico, utilidades independientes de “línea de comandos”, herramientas gráficas especializadas y modeladores pueden hacer esto. Ellos dibujarán algo para usted "lo mejor que puedan", y usted puede influir un poco en este proceso solo con la ayuda de algunos parámetros en el archivo de configuración o casillas de verificación en la interfaz.

Pero este problema se puede resolver de forma mucho más sencilla, flexible y elegante y, por supuesto, con la ayuda de código. Para crear diagramas de cualquier complejidad, disponemos de varios lenguajes de marcado especializados (DOT, GraphML, etc.) y, para ellos, una gran variedad de aplicaciones (GraphViz, PlantUML, Mermaid) que pueden leer dichas instrucciones y visualizarlas en una variedad de formatos. . Bueno, ya sabemos cómo obtener información sobre objetos y conexiones entre ellos.

Aquí hay un pequeño ejemplo de cómo podría verse esto, usando PlantUML y base de datos de demostración para PostgreSQL (a la izquierda hay una consulta SQL que generará la instrucción requerida para PlantUML, y a la derecha está el resultado):

Experiencia "Base de datos como código"

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'

Y si lo intentas un poco, entonces basado en Plantilla ER para PlantUML puedes obtener algo muy similar a un diagrama ER real:

La consulta SQL es un poco más complicada.

-- Шапка
select '@startuml
        !define Table(name,desc) class name as "desc" << (T,#FFAAAA) >>
        !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
-- Таблицы
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 haven 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'

Experiencia "Base de datos como código"

Si miras de cerca, muchas herramientas de visualización también utilizan consultas similares. Es cierto que estas solicitudes suelen ser profundamente "integrados" en el código de la propia aplicación y son difíciles de entender, por no hablar de cualquier modificación de los mismos.

Métricas y seguimiento

Pasemos a un tema tradicionalmente complejo: la supervisión del rendimiento de la base de datos. Recuerdo una pequeña historia real que me contó “uno de mis amigos”. En otro proyecto vivía un poderoso administrador de bases de datos, y pocos de los desarrolladores lo conocían personalmente o lo habían visto en persona (a pesar de que, según los rumores, trabajaba en algún lugar del edificio contiguo) . A la hora "X", cuando el sistema de producción de un gran minorista empezó a "sentirse mal" una vez más, envió silenciosamente capturas de pantalla de gráficos de Oracle Enterprise Manager, en los que resaltó cuidadosamente los lugares críticos con un marcador rojo para "comprensibilidad" ( esto, por decirlo suavemente, no ayudó mucho). Y en base a esta “tarjeta fotográfica” tuve que tratar. Al mismo tiempo, nadie tenía acceso al precioso (en ambos sentidos de la palabra) Enterprise Manager, porque el sistema es complejo y caro, de repente "los desarrolladores tropiezan con algo y lo rompen todo". Por lo tanto, los desarrolladores encontraron "empíricamente" la ubicación y la causa de los frenos y lanzaron un parche. Si la amenazadora carta del DBA no volviera a llegar en un futuro próximo, entonces todos darían un suspiro de alivio y volverían a sus tareas actuales (hasta la nueva Carta).

Pero el proceso de seguimiento puede parecer más divertido y amigable y, lo más importante, accesible y transparente para todos. Al menos su parte básica, como complemento a los principales sistemas de seguimiento (que sin duda son útiles y en muchos casos irremplazables). Cualquier DBMS es libre y absolutamente gratuito para compartir información sobre su estado actual y rendimiento. En la misma base de datos Oracle "maldita", casi cualquier información sobre el rendimiento se puede obtener de las vistas del sistema, desde procesos y sesiones hasta el estado de la memoria caché del búfer (por ejemplo, Guiones de bases de datos, apartado "Seguimiento"). Postgresql también tiene un montón de vistas del sistema para monitoreo de base de datos, en particular aquellos que son indispensables en la vida diaria de cualquier DBA, como pg_stat_actividad, pg_stat_base de datos, pg_stat_bgwriter. MySQL incluso tiene un esquema separado para esto. esquema_de_rendimiento. A En Mongo incorporado perfilador agrega datos de rendimiento en una colección del sistema sistema.perfil.

Por lo tanto, armado con algún tipo de recopilador de métricas (Telegraf, Metricbeat, Collectd) que pueda realizar consultas SQL personalizadas, un almacenamiento de estas métricas (InfluxDB, Elasticsearch, Timescaledb) y un visualizador (Grafana, Kibana), puede obtener una solución bastante sencilla. y un sistema de monitoreo flexible que se integrará estrechamente con otras métricas de todo el sistema (obtenidas, por ejemplo, del servidor de aplicaciones, del sistema operativo, etc.). Como, por ejemplo, esto se hace en pgwatch2, que utiliza la combinación InfluxDB + Grafana y un conjunto de consultas a las vistas del sistema, a las que también se puede acceder agregar consultas personalizadas.

En total

Y esto es sólo una lista aproximada de lo que se puede hacer con nuestra base de datos utilizando código SQL normal. Seguro que puedes encontrar muchos más usos, escribe en los comentarios. Y hablaremos sobre cómo (y lo más importante, por qué) automatizar todo esto e incluirlo en su canal de CI/CD la próxima vez.

Fuente: habr.com

Añadir un comentario