Объектно-реляционное сопоставление (ORM) оказывается неоценимым для разработчиков, оптимизируя взаимодействие между реляционными базами данных и кодом их приложений. Абстрагируя взаимодействие с базой данных в высокоуровневые программные конструкции, системы ORM, такие как Sequelize, избавляют разработчиков от необходимости писать сложные и подверженные ошибкам SQL-запросы. Такое упрощение ускоряет циклы разработки и снижает вероятность уязвимостей SQL-инъекций. Более того, ORM способствует независимости платформы, позволяя разработчикам беспрепятственно переключаться между различными системами управления базами данных без переписывания обширного кода.

Sequelize предлагает разработчикам удобные и эффективные средства взаимодействия с базами данных. Он написан на TypeScript и предоставляет простой в использовании API для выполнения запросов. Хотя его API больше похож на построитель запросов, он все же может помочь разработчикам писать запросы с полной типобезопасностью. Он поддерживает несколько механизмов баз данных, дает возможность избегать запросов, написанных вручную, обрабатывает параметры и хорошо интегрируется с остальной частью экосистемы.

В этом сообщении блога мы собираемся использовать Sequelize для изучения набора данных IMDb. Мы собираемся создать правильный Мониторинг базы данных, чтобы видеть изменения производительности и настраивать запросы. Напишем приложение на JavaScript с Экспрессом и базой данных PostgreSQL. Весь код находится на GitHub. Давай начнем.

Структура приложения

Я собираюсь реализовать приложение на Amazon Linux 2, но вы сможете запускать его в любой операционной системе.

Начнем с загрузки исходного кода с GitHub. Создайте форк репозитория и тега оформления заказа Step_1:

git checkout Step_1

Бизнес-логика

Давайте рассмотрим ревизию. Точка входа находится в файле main.js. Он загружает переменные среды, запускает приложение и открывает доступ к серверу на localhost через порт 3000:

require(‘dotenv’).config();
const app = require(‘./app’);
async function bootstrap() {
  app.listen(process.env.PORT || 3000, ‘127.0.0.1’);
}

bootstrap();

Сервер настроен в app.js. Настраиваем логгер, CORS, контроллеры нагрузки и обрабатываем ошибки.

const express = require('express');
const cookieParser = require('cookie-parser');
const logger = require('morgan');
const cors = require('cors');
const controllers = require('./controllers');

function bootstrap(){
  const router = express.Router();
  controllers.initialize(router);

  const app = express();
  app.use(logger('dev'));
  app.use(express.json());
  app.use(express.urlencoded({ extended: false }));
  app.use(cookieParser());
  app.use(cors())

  app.use('/', router);  

  app.use(function(req, res, next) {
    res.status(404).send({ error: 'Not found' })
  });
  
  app.use(function(err, req, res, next) {
    console.log("Error: " + err);
    res.locals.message = err.message;
    res.locals.error = err;
    res.status(err.status || 500).send({ error: err })
  });

  return app;
}

module.exports = bootstrap();

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

module.exports = {
  initialize(router) {
    router.get('/titles/ratings/best', this.handleResult(this.getBestMovies));
  },

  getBestMovies(req, res) {
    return titleRatingService.getBestMovies();
  },

  handleResult(lambda) {
    return (req, res) => Promise.resolve(lambda(req, res))
      .then((results) => res.status(200).send(results))
      .catch((error) => { console.log(error); res.status(400).send(error); });
  }
};

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

Оба контроллера поддерживаются службами, которые пока не возвращают никаких данных. Посмотреть их можно здесь и здесь. Мы также определяем набор тестов, которые проверяют, работает ли приложение от начала до конца. Набор тестов перебирает все конечные точки, вызывает их одну за другой и проверяет, получил ли HTTP-код 200, указывающий на успех:

require('dotenv').config();
const request = require('supertest');

describe('AppController (e2e)', function (){
  this.timeout(0);

  const endpoints = [
    '/titles/ratings/best',
    '/titles?title=Test',
    '/titlesForAnActor?nconst=nm1588970',
    '/highestRatedMoviesForAnActor?nconst=nm1588970',
    '/highestRatedMovies?numvotes=10000',
    '/commonMoviesForTwoActors?actor1=nm0302368&actor2=nm0001908',
    '/crewOfGivenMovie?tconst=tt0000439',
    '/mostProlificActorInPeriod?startYear=1900&endYear=1912',
    '/mostProlificActorInGenre?genre=Action',
    '/mostCommonTeammates?nconst=nm0000428',
  ];

  let app;

  before(async function() {
    this.timeout(0);
    
    app = require('../app');
  });

  endpoints.map(url => it(`${url} (GET)`, async function() {
    await request(app)
      .get(url)
      .expect(200);
  }));
});

Давайте теперь посмотрим на источник данных.

База данных IMDb

Мы собираемся использовать набор данных IMDb. Вы можете скачать файлы и преобразовать их в базу данных, но самый простой способ — взять Docker-контейнер, который уже это делает. Клонируйте этот репозиторий и запустите следующий скрипт:

./start-docker-database.sh

Это должно запустить базу данных локально и открыть порт 5432 для локального хоста.

Запустив его и подключившись к базе данных, вы увидите, что некоторые столбцы не нормализованы. Например, title_crew имеет следующую строку:

При объединении данных нам нужно будет разделить эти значения запятой и выполнить дополнительную обработку.

В наборе данных IMDb есть два важных идентификатора: tconst и nconst. tconst используется для указания заголовка. nconst — имя (актера, режиссера, писателя и т. д.). Мы собираемся использовать эти идентификаторы для эффективного объединения таблиц.

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

Запуск приложения

В корневом каталоге пакета есть несколько сценариев, которые позволяют запускать все локально. Приложение можно запускать как с локальной установкой Node, так и внутри контейнера Docker, поэтому вам не нужно ничего устанавливать локально.

Давайте запустим приложение локально. build-and-run.sh скомпилирует приложение локально и предоставит его через порт по умолчанию 3000:

Запустив скрипт, вы можете убедиться, что он работает правильно:

curl https://localhost:3000/titles/ratings/best

Это должно вернуть пустой массив:

[]

Вы также можете запустить test.sh, который будет запускать все тесты конечных точек локально с помощью mocha:

Вы также можете запустить приложение в Docker с помощью start-service.sh или start-service.ps1. Вы можете протестировать это с помощью Curl, как и раньше. Впоследствии вы можете удалить докер с помощью remove-container.sh и remove-container.ps1.

На данный момент наше приложение работает. Давайте теперь интегрируем его с Sequelize.

Интеграция с Sequelize

Теперь мы собираемся установить Sequelize. Вы можете запускать все команды локально (при условии, что у вас настроен Node) или просто извлечь тег Step_2 и посмотреть, как он работает (особенно если у вас нет Node и хочу запускать все только в Docker).

Давайте установим Sequelize, как описано в Руководстве по началу работы:

npm install --save sequelize
npm instalsave pg pg-hstore

Эти команды добавляют пакеты в файл package.json. Теперь мы можем использовать библиотеку Sequelize для доступа к базе данных.

Начнем с определения сущностей. Давайте создадим src/names/entities/name_basic.entity.js со следующим содержимым:

'use strict';
const { Model } = require('sequelize');
module.exports = (sequelize, DataTypes) => {
  class NameBasic extends Model {
    static associate(models) {
    }
  }
  NameBasic.init(
    {
      nconst: { type: DataTypes.STRING, primaryKey: true, allowNull: false },
      primaryname: { type: DataTypes.TEXT },
      birthyear: { type: DataTypes.INTEGER },
      deathyear: { type: DataTypes.INTEGER },
      primaryprofession: { type: DataTypes.TEXT },
      knownfortitles: { type: DataTypes.TEXT }
    },
    {
      sequelize,
      modelName: 'NameBasic',
      tableName: 'name_basics',
      schema: 'imdb',
      timestamps: false,
    }
  );
  return NameBasic;
};

Мы импортируем Sequelize, а затем создаем класс NameBasic, расширяющий Model. Мы определяем такие поля, как nconst или primaryname, а имя модели определяем как NameBasic. . Мы можем продолжать определять другие сущности таким же образом.

Далее нам нужно инициализировать Sequelize при загрузке модуля. Создадим src/models/index.js:

'use strict';

const Sequelize = require('sequelize');
const process = require('process');
const db = {};
const fsPromises = require('fs').promises;

let sequelize = new Sequelize(process.env['DATABASE_URL'], { dialect: 'postgres' });

let models = [
  '../titles/entities/title_rating.entity',
  '../titles/entities/title_basic.entity',
  '../titles/entities/title_principal.entity',
  '../titles/entities/title_crew.entity',
  '../names/entities/name_basic.entity',
];

(function createModels() {
  models.forEach((file) => {
    const model = require(file)(sequelize, Sequelize.DataTypes);
    db[model.name] = model;
  });

  Object.keys(db).forEach((modelName) => {
    if (db[modelName].associate) {
      db[modelName].associate(db);
    }
  });
})();

(function createRelations() {
  (function joinTitleBasiclAndTitlePrincipal(){
    db.TitleBasic.hasMany(db.TitlePrincipal, {
      foreignKey: 'tconst',
      targetKey: 'tconst',
      as: 'titleBasicTitlePrincipal',
    });
    
    // Another association to allow for two joins when taking movies for two actors
    db.TitleBasic.hasMany(db.TitlePrincipal, {
      foreignKey: 'tconst',
      targetKey: 'tconst',
      as: 'titleBasicTitlePrincipal2',
    });

    db.TitlePrincipal.belongsTo(db.TitleBasic, {
      foreignKey: 'tconst',
      targetKey: 'tconst'
    });
  })();

  (function joinTitlePrincipalAndNameBasic() {
    db.TitlePrincipal.hasOne(db.NameBasic, {
      foreignKey: 'nconst',
      targetKey: 'nconst',
      sourceKey: 'nconst'
    });

    db.NameBasic.belongsTo(db.TitlePrincipal, {
      foreignKey: 'nconst',
      targetKey: 'nconst',
      sourceKey: 'nconst'
    });
  })();

  (function joinTitleRatingAndTitleBasic() {
    db.TitleBasic.hasOne(db.TitleRating, {
      foreignKey: 'tconst',
      targetKey: 'tconst'
    });

    db.TitleRating.belongsTo(db.TitleBasic, {
      foreignKey: 'tconst',
      targetKey: 'tconst'
    });
  })();

  (function joinTitleBasicAndTitleCrew(){
    db.TitleBasic.hasOne(db.TitleCrew, {
      foreignKey: 'tconst',
      targetKey: 'tconst',
    });

    db.TitleCrew.belongsTo(db.TitleBasic, {
      foreignKey: 'tconst',
      targetKey: 'tconst'
    });
  })();
})();

db.sequelize = sequelize;
db.Sequelize = Sequelize;

module.exports = db;

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

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

Теперь мы можем начать использовать Sequelize. Давайте изменим title_ratings.service.js и выделим несколько лучших фильмов:

const titleRating = require('../models').TitleRating;

module.exports = {
  getBestMovies() {
    return titleRating
      .findAll({
        where: {
          averagerating: 10.0
        }
      });
  }
};

Вы можете видеть, что мы импортируем класс TitleRating, а затем используем метод findAll для фильтрации фильмов с 10 звездами. . Теперь вы можете запустить приложение и использовать Curl, чтобы убедиться, что конечная точка сейчас возвращает данные. Вы также можете повторно запустить тесты, чтобы убедиться, что все работает и что конечная точка для получения лучших заголовков отправляет SQL-запрос:

Написание запросов с помощью Sequelize

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

Перейдите в src/titles/titles.service.js и добавьте следующий импорт в начало файла:

const { Op } = require("sequelize");
const sequelize = require('../models').sequelize;
const titleBasic = require('../models').TitleBasic;
const titlePrincipal = require('../models').TitlePrincipal;
const titleRating = require('../models').TitleRating;
const titleCrew = require('../models').TitleCrew;
const nameBasic = require('../models').NameBasic;

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

 getTitles(title) {
    return titleBasic 
      .findAll({
        where: {
          primarytitle: { [Op.like]: '%' + title + '%' }
        }
      });
  },

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

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

 titlesForAnActor(nconst) {
    function titlesForAnActorNaive() {
      return titleBasic 
        .findAll({
          include: [{
            model: titlePrincipal,
            required: true,
            as: 'titleBasicTitlePrincipal',
            where: {
              'nconst': nconst
            },
          }],
          order: [
            ['startyear', 'DESC']
          ],
          limit: 10
        });
    }

    return titlesForAnActorNaive();
  },

Мы берем таблицу title_basic и затем соединяем ее с title_principal, используя tconst столбец. Затем мы фильтруем на основе идентификатора nconst и, наконец, сортируем результат по году начала, чтобы первыми получить самые последние фильмы. Возвращаем десять из них.

Аналогичным образом мы можем получить фильмы с самым высоким рейтингом для актера:

 highestRatedMovies(numvotes) {
    function highestRatedMoviesNaive() {
      return titleBasic 
        .findAll({
          include: [
            {
              model: titleRating,
              required: true,
              duplicating: false,
              where: {
                'numvotes': { [Op.gte]: numvotes }
              }
            },
          ],
          order: [
            [ titleRating, 'averagerating', 'DESC'], 
          ]
        });
    }

    return highestRatedMoviesNaive();
  },

Мы присоединяемся к title_basic с title_rating, выполняем фильтрацию и, наконец, упорядочиваем результат на основе звездного рейтинга фильм.

Sequelize очень мощный инструмент, однако иногда проще реализовать SQL-запрос напрямую. Чтобы получить съемочную группу фильма, мы можем сделать это следующим образом:

 crewOfGivenMovie(tconst) {
    function crewOfGivenMovieManualSlow(){
        return sequelize.query(`
          SELECT DISTINCT NB.*
          FROM imdb.title_basics AS TB
          LEFT JOIN imdb.title_principals AS TP ON TP.tconst = TB.tconst
          LEFT JOIN imdb.title_crew AS TC ON TC.tconst = TB.tconst
          LEFT JOIN imdb.name_basics AS NB ON 
                  NB.nconst = TP.nconst 
                  OR TC.directors = NB.nconst
                  OR TC.directors LIKE NB.nconst || ',%'::text
                  OR TC.directors LIKE '%,'::text || NB.nconst || ',%'::text
                  OR TC.directors LIKE '%,'::text || NB.nconst
                  OR TC.writers = NB.nconst
                  OR TC.writers LIKE NB.nconst || ',%'::text
                  OR TC.writers LIKE '%,'::text || NB.nconst || ',%'::text
                  OR TC.writers LIKE '%,'::text || NB.nconst
          WHERE TB.tconst = :tconst
        `, {
          model: nameBasic,
          mapToModel: true,
          replacements: {
            tconst: tconst
          }
      });
    }

    return crewOfGivenMovieManualSlow();
  },

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

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

Представляем мониторинг и наблюдаемость базы данных

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

Мы собираемся интегрировать приложение с Metis для создания мониторинга базы данных. Таким образом, мы сможем увидеть проблемы с производительностью и оптимизировать наши запросы перед запуском в производство.

Прежде чем двигаться дальше, зайдите в Метис и создайте там проект. Для интеграции приложения вам понадобится ключ API.

Все изменения вы можете увидеть в теге Шаг_4. Мы собираемся реализовать инструментарий так, как он описан в документации. Начнем с установки зависимостей. Metis использует OpenTelemetry для извлечения подробностей вызовов REST и запросов SQL. Никакие конфиденциальные данные не отправляются на платформу, поскольку Metis фиксирует только планы выполнения.

npm install --save pg @metis-data/pg-interceptor

npm install --save @opentelemetry/api \
                @opentelemetry/context-async-hooks \
                @opentelemetry/instrumentation \
                @opentelemetry/instrumentation-http \
                @opentelemetry/resources \
                @opentelemetry/sdk-trace-base \
                @opentelemetry/semantic-conventions

Как только это будет сделано, нам нужно добавить конфигурацию трассировки. Добавьте файл src/tracer.js:

let opentelemetry = require('@opentelemetry/api');
let { registerInstrumentations } = require('@opentelemetry/instrumentation');
let {
  BasicTracerProvider,
  BatchSpanProcessor,
  ConsoleSpanExporter,
  SimpleSpanProcessor,
} = require('@opentelemetry/sdk-trace-base');
let { getMetisExporter, getResource, MetisHttpInstrumentation, MetisPgInstrumentation } = require('@metis-data/pg-interceptor');
let { AsyncHooksContextManager } = require('@opentelemetry/context-async-hooks');

let tracerProvider;
let metisExporter;

const shudownhook = async () => {
  console.log('Shutting down tracer provider and exporter...');
  await tracerProvider?.shutdown();
  await metisExporter?.shutdown();
  console.log('Tracer provider and exporter were shut down.');
}

process.on('SIGINT', () => {
  shudownhook().finally(() => process.exit(0));
});
process.on('SIGTERM', () => {
  shudownhook().finally(() => process.exit(0));
});

const connectionString = process.env.DATABASE_URL;

const startMetisInstrumentation = () => {
  tracerProvider = new BasicTracerProvider({
    resource: getResource(process.env.METIS_SERVICE_NAME, process.env.METIS_SERVICE_VERSION),
  });

  metisExporter = getMetisExporter(process.env.METIS_API_KEY);

  tracerProvider.addSpanProcessor(new BatchSpanProcessor(metisExporter));
  
  if (process.env.OTEL_DEBUG === "true") {
    tracerProvider.addSpanProcessor(new SimpleSpanProcessor(new ConsoleSpanExporter()));
  }

  const contextManager = new AsyncHooksContextManager();

  contextManager.enable();
  opentelemetry.context.setGlobalContextManager(contextManager);

  tracerProvider.register();

  const excludeUrls = [/favicon.ico/];
  registerInstrumentations({
    instrumentations: [new MetisPgInstrumentation({ connectionString }), new MetisHttpInstrumentation(excludeUrls)],
  });
};

module.exports = {
  startMetisInstrumentation,
  shudownhook,
};

Мы извлекаем ключ API Metis, добавляем процессор для диапазонов OpenTelemtry и регистрируем инструменты для Pg (драйвер SQL) и HTTP (сетевые вызовы). Наконец, нам нужно добавить переменные среды в файл .env:

DATABASE_URL=postgres://postgres:[email protected]:5432/demo?schema=imdb
METIS_API_KEY=YOUR_API_KEY
METIS_SERVICE_NAME=sequelize
METIS_SERVICE_VERSION=1

METIS_API_KEY — это ключ вашего проекта. METIS_SERVICE_NAME и METIS_SERVICE_VERSION могут быть любыми по вашему желанию. Эти два значения используются только для указания того, какое приложение отправило трассировку. Вы можете использовать его для управления версиями, различения версий для разработчиков и рабочих версий или для интеграции этого с вашим конвейером CI/CD.

Все, код готов. Теперь мы можем включить инструментарий, чтобы наше приложение использовало его. Вызовем startMetisInstrumentation в основной точке входа и в тестах. Таким образом, мы можем получить информацию о производительности, когда мы используем наше приложение локально или когда запускаем набор автоматизированных тестов. Давайте теперь запустим приложение и воспользуемся curl для проверки конечной точки:

curl https://localhost:3000/titles/ratings/best

Это должно вернуть несколько заголовков. Давайте теперь перейдем к вашему проекту Metis, перейдем на вкладку «Недавние действия», и вы должны увидеть что-то вроде этого:

Вы можете видеть, что Metis перехватил вызов к конечной точке titles/rating/best. Мы видим, что конечная точка вернула HTTP-код 200. Теперь давайте нажмем на это выполнение, и мы должны попасть на этот экран:

Вы можете увидеть пролеты вверху. Их несколько, поскольку Sequelize изначально настраивает базу данных перед выполнением первого запроса. Последний диапазон указывает на фактический выполненный SQL-запрос. Вы можете нажать на вкладку SQL, чтобы увидеть текст запроса:

Вы можете увидеть запрос, отправленный Sequelize! Это тот же запрос, который мы видели в журналах. Однако на этот раз мы можем проанализировать его автоматически. Вернитесь на вкладку «Статистика» и увидите следующее:

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

Использование мониторинга базы данных для повышения производительности SQL-запросов

Добавим индекс. Код находится в теге Шаг_5. Сначала давайте создадим миграцию, которую мы будем выполнять при запуске приложения. Добавим src/models/migrations/001_create_title_ratings_indexed.sql:

CREATE INDEX IF NOT EXISTS title_ratings_index ON imdb.title_ratings(averagerating);

Это создаст индекс в таблице, чтобы наш запрос выполнялся быстрее. Давайте теперь реализуем небольшой код для запуска этой миграции. Добавьте этот метод в src/models/index.js:

(function seed(){
  db.seedDatabase = async () => {
    let files = await fsPromises.readdir('src/models/migrations/');
    for(let id in files) {
      if(files[id].endsWith(".sql")){
        let data = await fsPromises.readFile('src/models/migrations/' + files[id], 'utf8');
        console.log("Running " + data);
        try{
          console.log(await sequelize.query(data));
        }catch(e){
          console.log(e);
          throw e;
        }
      }
    }

    console.log("Done migrating");
  };
})();

Давайте теперь вызовем этот метод в точке входа и в тестах.

Давайте теперь перезапустим приложение и увидим это:

Отлично, миграция выполнена. Теперь мы можем снова запустить Curl и перейти к проекту Metis, чтобы увидеть это:

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

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

Улучшение других запросов

Теперь у нас есть все детали на месте. Давайте посмотрим, как использовать новую функцию мониторинга и наблюдения за базами данных с Sequelize для повышения производительности. Здесь мы можем сделать множество улучшений, поэтому я просто покажу некоторые из них. См. тег Step_6, чтобы увидеть больше, и прочитайте нашу статью Как Metis оптимизирует запросы, выполняемые с помощью Sequelize, чтобы получить еще больше идей.

Давайте вернемся к запросу, возвращающему фильмы для актера. Запустите его с помощью Curl:

curl https://localhost:3000/titlesForAnActor?nconst=nm1588970

Заходим в Метис и видим, что звонок захвачен:

Есть критические проблемы. Давайте посмотрим на них:

Мы видим, что запрос прочитал 70 миллионов строк. Это много! Посмотрим текст запроса:

SELECT
  TitleBasic.*,
  titleBasicTitlePrincipal.tconst AS titleBasicTitlePrincipal.tconst,
  titleBasicTitlePrincipal.ordering AS titleBasicTitlePrincipal.ordering,
  titleBasicTitlePrincipal.nconst AS titleBasicTitlePrincipal.nconst,
  titleBasicTitlePrincipal.category AS titleBasicTitlePrincipal.category,
  titleBasicTitlePrincipal.job AS titleBasicTitlePrincipal.job,
  titleBasicTitlePrincipal.characters AS titleBasicTitlePrincipal.characters
FROM
  (
 SELECT
   TitleBasic.tconst,
   TitleBasic.titletype,
   TitleBasic.primarytitle,
   TitleBasic.originaltitle,
   TitleBasic.isadult,
   TitleBasic.startyear,
   TitleBasic.endyear,
   TitleBasic.runtimeminutes,
   TitleBasic.genres
 FROM
   imdb.title_basics AS TitleBasic
 WHERE
   (
     SELECT
       tconst
     FROM
       imdb.title_principals AS titleBasicTitlePrincipal
     WHERE
       (
         titleBasicTitlePrincipal.nconst = 'nm1588970'
         AND titleBasicTitlePrincipal.tconst = TitleBasic.tconst
       )
     LIMIT
       1
   ) IS NOT NULL
 ORDER BY
   TitleBasic.startyear DESC
 LIMIT
   10
  ) AS TitleBasic
  INNER JOIN imdb.title_principals AS titleBasicTitlePrincipal ON TitleBasic.tconst = titleBasicTitlePrincipal.tconst
  AND titleBasicTitlePrincipal.nconst = 'nm1588970'
ORDER BY
  TitleBasic.startyear DESC;

Это определенно не то, чего мы ожидали. Давайте попробуем сделать это быстрее и введем индекс:

CREATE INDEX IF NOT EXISTS title_principals_nconst_idx ON imdb.title_principals(nconst) INCLUDE (tconst);

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

Давайте попробуем понять, почему Sequelize сгенерировал такой странный запрос. Мы объединили две таблицы, упорядочили строки по начальному году, а затем вернули десять строк. Однако Sequelize не знает, может ли объединение создавать дубликаты. В этом случае Sequelize необходимо использовать подзапросы для расчета правильного порядка. Мы можем это исправить, используя флаг дублирование:

return titleBasic
 .findAll({
  include: [{
   model: titlePrincipal,
   required: true,
   duplicating: false,
   as: 'titleBasicTitlePrincipal',
   where: {
    'nconst': nconst
   },
  }],
  order: [
   ['startyear', 'DESC']
  ],
  limit: 10
 });

Теперь мы можем перезапустить приложение и увидеть, что мы получаем вот такой запрос:

SELECT
 TitleBasic.tconst,
 TitleBasic.titletype,
 TitleBasic.primarytitle,
 TitleBasic.originaltitle,
 TitleBasic.isadult,
 TitleBasic.startyear,
 TitleBasic.endyear,
 TitleBasic.runtimeminutes,
 TitleBasic.genres,
 titleBasicTitlePrincipal.tconst AS titleBasicTitlePrincipal.tconst,
 titleBasicTitlePrincipal.ordering AS titleBasicTitlePrincipal.ordering,
 titleBasicTitlePrincipal.nconst AS titleBasicTitlePrincipal.nconst,
 titleBasicTitlePrincipal.category AS titleBasicTitlePrincipal.category,
 titleBasicTitlePrincipal.job AS titleBasicTitlePrincipal.job,
 titleBasicTitlePrincipal.characters AS titleBasicTitlePrincipal.characters
FROM
 imdb.title_basics AS TitleBasic
INNER JOIN imdb.title_principals AS titleBasicTitlePrincipal ON TitleBasic.tconst = titleBasicTitlePrincipal.tconst AND titleBasicTitlePrincipal.nconst = 'nm1588970'
ORDER BY
 TitleBasic.startyear DESC
LIMIT
 10;

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

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

Краткое содержание

В этом посте мы увидели, как создать мониторинг базы данных с помощью Sequelize. Мы увидели, как анализировать производительность SQL-запросов, как контролировать базу данных и как оптимизировать приложение с использованием фактического набора данных IMDb. Таким образом, вы сможете ускорить работу всех своих приложений и запросов еще до их запуска в производство. А если этого недостаточно, прочитайте нашу статью Улучшение производительности базы данных, чтобы решать проблемы с базами данных на профессиональном уровне.

Часто задаваемые вопросы

Какова цель использования Sequelize ORM с набором данных IMDB?

Мы можем упростить чтение объектов базы данных из приложения JavaScript и гораздо лучше выполнять запросы SQL.

Как я могу импортировать и предварительно обработать данные IMDB с помощью Sequelize?

Мы можем использовать базу данных Docker, которая предоставляет объекты IMDb. Затем мы можем сопоставить их с сущностями в Sequelize и прочитать из приложения JavaScript.

Каковы ключевые особенности Sequelize для анализа наборов данных IMDB?

Sequelize позволяет нам настраивать отношения и упрощать способ написания запросов. Мы можем автоматически проверять типы, существование столбцов или будет ли запрос вообще выполняться правильно.