Lookup skew, или бомба замедленного действия в вашей архитектуре

Lookup skew - так называется ситуация, когда к записи одного объекта привязано много записей другого объекта через Lookup связь. Перейдём сразу к примеру.

Престижная оптово-розничная финтех компания “Horns and hooves LTD”, лидер рынка консалтинга в сфере производства мягких игрушек и фармацевтики, хранит информацию о миллионах своих клиентов в Salesforce объекте Contact.
Для сегментации записей по важности клиента для компании были созданы кастомный объект Client_value__c с тремя записями и Lookup поле на контакте. Имена записей:“Bronze”,“Silver”,“Gold”. Предположим, что в системе 3 миллиона контактов, которые равномерно распределены между Client_value__c записями, по одному миллиону на каждую. На первый взгляд, ничего страшного, но есть одно “но”…

Lookup связь является ничем иным, как иностранным внешним ключом (foreign key). И каждый раз когда запись вставляется или обновляется, Salesforce блокирует записи, на которые указывают Lookup-ы, для сохранения целостности данных.
Это помогает избежать проблем, когда, например, в процессе обновления одной записи, другая удалена другим контекстом, или изменена таким образом, что стала неактуальной для текущей… Но вернёмся к блокировке.
Обычно, операции вставки/обновления записи происходят быстро и шансы на конкурентные запросы на блокировку одной и той же записи практически нулевые (при условии, что дочерних записей не слишком много).
Однако, “Horns and hooves LTD” не так просты, записей под одним родитетем - миллион, а процесс сохранения происходит долго из-за обилия кастомного кода, воркфлоу и, например, процессов в process builder-е. Таким образом, ошибки о невозможности заблокировать запись стали для них частым явлением.

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

  • Уменьшить время сохранения записи.
    Оптимизировать апекс код, выполняющийся в транзакции, согласно best practices, не забывая о сложности алгоритма (давеча была задача по уменьшению времени выполнения батча, так нашёл на проде код с O(n^3) :crazy_face:) и здравом смысле.
    Удалить ненужные воркфлоу или перенести их функционал в код. Воркфлоу выполняется куда медленнее кода.
    Выполнять только то, что нужно здесь и сейчас. Любые необязательные действия (в разрезе текущей транзакции) лучше вынести в асинхронные методы.
  • Оптимизировать логику использования рассматриваемой Lookup связи. Если брать за основу наш самый правдивый в мире пример, и предположить, что важность клиента определяется количеством/суммой сделок, то очевидно, что все клиенты изначально будут “Bronse”. В таком случае, имело бы смысл оставлять Lookup поле пустым для новых контактов, до момента набора ими какого-то количества тех самых сделок.
    Однако, с течением времени Lookup skew всё равно возможен, когда какая-либо категория клиентов станет достаточно большой.
  • Использовать picklist.
    …для случаев, когда имеется относительно немного возможных значений. Для нашего примера picklist идеально подошёл бы. И даже самому Salesforce ничто не мешает использовать picklist для указания индустрии Аккаунта, например, вместо того, чтобы создавать ненужный объект.
  • Уменьшить размер батчей при массовой вставке или обновлении skewed объектов. Это уменьшит время блокировки родительской записи каждым отдельным батчем и поможет пользователям избежать ошибок (с большей вероятностью). Хотя данный совет не отменяет того факта, что подобные мероприятия не стоит планировать на часы пиковой нагрузки на организацию :slight_smile:

Таким образом, хоть Lookup и является одним из ключевых инструментов построения модели данных в Salesforce, он тоже имеет свои подводные камни. И при создании самой классной и удобной на первый взгляд модели данных, всегда следует подумать дважды, как она будет себя вести, когда количество этих данных станет по-настоящему большим.
Всем масштабируемых решений! :slight_smile: