Мой и моей команды опыт оптимизации и улучшения работы различных функций в рамках сервисов нашего офиса.
За последние несколько месяцев в моем офисе осуществлялась миграция технологий для каждой из своих услуг. Первоначально созданные с использованием Express.js, мы перенесли их в TypeScript с помощью платформы Nest.js. Когда мы с моей командой начали процесс миграции и провели оценку каждой функции в старых службах, мы решили реализовать несколько улучшений, чтобы сделать каждую функцию более эффективной и повысить производительность каждой конечной точки.
Итак, что же сделали я и моя команда?
Позвольте мне объяснить три пункта улучшения, которые мы реализовали. Я также надеюсь, что три пункта, которые я собираюсь упомянуть ниже, могут быть приняты и опробованы всеми читателями, поскольку они будут очень полезны для оптимизации услуг, которые читатели в настоящее время разрабатывают.
- Реализация "взять все, затем сопоставить"
Из-за очень сложной природы системы и участия в ее разработке множества команд мы столкнулись с чрезвычайно сложными данными. В результате мы часто сталкивались с необходимостью получить данные из таблицы A, где каждая запись в таблице A содержит массив данных из таблицы B. К сожалению, из-за характера наших требований выполнить соединение между этими таблицами было невозможно. .
Следовательно, для обработки такого извлечения данных многие функции в нашем старом сервисе прибегали к классическим методам, которые были относительно просты в реализации, но неэффективны, особенно при работе с большими объемами данных.
/** * Expected result: * [ * { * id: int, * name: string, * foo: string, * items: Array<object> * } * ] */ async function list() { /** * Expected result: * [ * { * id: int, * name: string, * foo: string * } * ] */ const getListA = await this.modelA.getListA(); /** * Expected result: * [ * { * id: int, * a_id: int, // id from table A * title: string, * bar: string * } * ] */ const result = []; for (const el of getListA) { const getListBbyAId = await this.modelB.getListBbyAId(el.id); result.push({ ...el, items: getListBbyAId, }); } return result; }
В приведенном выше сценарии кода эта функция устанавливает соединения с базой данных 1 + n для обработки извлечения данных в сценарии, о котором я упоминал ранее. Это означает, что если в таблице A есть 100 записей, которые необходимо получить, потребуется 1 соединение для получения данных из таблицы A и 100 соединений для получения данных из таблицы B. Что, если записей 1000? Это, несомненно, создаст значительную нагрузку на базу данных и приведет к снижению производительности функции.
Впоследствии мы попытались реализовать подход «взять все, затем сопоставить». В этом подходе мы создаем список всех идентификаторов для каждой записи данных, полученной из таблицы A. Эти списки затем используются для извлечения данных из таблицы B с помощью запроса «IN».
# before SELECT id, a_id, title, bar FROM B WHERE a_id = ? # after SELECT id, a_id, title, bar FROM B WHERE a_id IN (?)
Результат такой, как показано в приведенном ниже коде сценария:
async function list() { const getListA = await this.modelA.getListA(); // list id const aIds = []; for (const el of getListA) { aIds.push(el.id); } // get B data by list a_id const getListBbyAIds = await this.modelB.getListBbyAIds(aIds); const result = []; for (const el of getListA) { const temp = []; for (const el2 of getListBbyAIds) { if (el.id === el2.a_id) { temp.push(el2); } } result.push({ ...el, items: temp, }); } return result; }
После реализации улучшения с использованием приведенного выше фрагмента кода функции теперь нужно установить только 2 соединения с базой данных для получения данных: 1 соединение для получения данных из таблицы A и другое соединение для получения данных из таблицы B.
2. Преобразование от временной сложности n² к временной сложности 2n
Собственно, с помощью решения из пункта 1 мы добились значительной эффективности при подключении к базе данных. Однако наша работа не была завершена, поскольку мы все еще сталкивались с проблемами времени сложности при циклах.
Представьте, что данные, полученные из таблицы A, содержат 100 записей, а данные из таблицы B — 200 записей. Это означает, что функции потребуется выполнить цикл 100 раз для данных из таблицы A и 200 раз для сопоставления данных между таблицей A и таблицей B, в результате чего в общей сложности получится 20 000 циклов. Кратко это можно обозначить как временная сложность n². Очевидно, что в таком сценарии вышеуказанная функция все равно нуждается в дальнейшей оптимизации.
Итак, мы внесли улучшения в приведенный выше фрагмент кода, как показано ниже:
async function list() { ... // convert array of object to hashmap object const hashMapItem = {}; for (const el of getListBbyAIds) { if (!hashMapItem[el.a_id]) { hashMapItem[el.a_id] = []; } hashMapItem[el.a_id].push(el); } const result = []; for (const el of getListA) { result.push({ ...el, items: hashMapItem[el.id], }); } return result; }
Таким образом, вместо выполнения 20 000 циклов приведенная выше функция теперь требует всего 100 + 200 циклов. В терминах обозначений теперь его временная сложность составляет всего 2n.
3. Реализация игнорировать
Помимо упомянутых выше проблем, у нас также есть функция фильтрации данных по нескольким параметрам. Один из этих параметров отправляется в виде массива, что позволяет отправить значение массива длиной n.
Например, у нас есть функция получения списка пользовательских данных, которые можно фильтровать по городу и району на основе адреса пользователя. Фильтры городов и районов представляют собой массивы, позволяющие фильтровать города A, B, C и одновременно фильтровать районы W в городе A, X в городе A, Y в городе B, Z в городе C и т. д. Короче говоря, эта функция может одновременно фильтровать n городов и m районов.
До сих пор мы не столкнулись с серьезными проблемами с этой функцией. Однако нам, как ответственным программистам, всегда необходимо учитывать потенциальные проблемы, которые могут возникнуть при реализации каждого реализованного метода, и решать эти потенциальные проблемы как можно раньше.
Например, когда внешний интерфейс (FE) отправляет запрос на получение списка пользовательских данных с помощью этой функции, URL FE будет выглядеть следующим образом:
/user/list?city=A&city=B&city=C&district=W&district=X&district=Y&district=Z
Итак, какие потенциальные проблемы могут возникнуть в этом случае?
Представьте себе, что в системе зарегистрировано 100 городов, и в каждом из этих городов есть 100 районов. Теперь пользователь хочет отфильтровать 98 городов и только 9798 районов. Какой длины будет URL в этом сценарии?
Поскольку такие проблемы возможны, мы попытались реализовать метод «игнорировать». Вместо отправки 98 городов и 9798 районов в URL-запросе мы отправим только 2 города и 2 района с флагом, указывающим, какие данные фильтра следует игнорировать.
В результате URL, который будет отправлен внешним интерфейсом, будет выглядеть следующим образом:
/user/list?ignore_city=A&ignore_city=B&ignore_district=X&ignore_district=Y
И SQL-запрос будет следующим:
# before SELECT id, name FROM users WHERE city IN (?) AND district IN (?) # after SELECT id, name FROM users WHERE city NOT IN (?) AND district NOT IN (?)
Если какой-либо из трех методов, которые мы применили для оптимизации определенных функций в сервисах нашего офиса, нуждается в дальнейшей корректировке или если есть лучший подход, не стесняйтесь, дайте мне знать в разделе комментариев.
Увидимся в следующий раз!