Особенности применения SOQL запросов в триггерах

Я думаю, всем знакомы лимиты SOQL на количество запросов и кол-во возвращаемых записей, но вот недавно я столкнулся с новой для меня ошибкой при использовании SOQL запроса в триггере на объектах с большим кол-вом записей:

System.QueryException: Non-selective query against large object type (more than 200000 rows).

Для лучшего понимания ситуации приведу фрагмент описания бизнес процесса:

Стояла задача: при создании нового Person Account проверить на уникальность специальный номер и отправить информацию на апрув менеджеру; для этого использовался кастомный объект, куда вносились данные и отправлялись на проверку менеджеру.

Было принято решение на BEFORE триггере кастомного объекта проверить в хендлере уникальность этого номера, и в случае совпадения номера на существующем аккаунте — не создавать объект, а вернуть ошибку.

Компания международная, работает по всему миру, идентификатором страны была валюта, записанная в кастомное поле, по которому уже можно было сделать селективный отбор и получить в существующей базе аккаунтов на 1,5М рекордов выборку по нужной стране до 20к записей.

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

Окончательно это выглядело примерно так:

List<Account> accList = [ SELECT Id, SpecialNumber__c
                          FROM Account
                          WHERE Currency = :CURRENCY_MY_COUNTRY
                                  AND IsPersonAccount = TRUE
                                  AND (SpecialNumber__c IN : listNewSpecialNumbers) ];

т.е. при таком поиске нам должен был вернуться список аккаунтов только с совпадающим SpecialNumber__c…

Но не тут-то было, при сохранении объекта с дублирующимся номером я получил не ошибку о дубликате, а вот это:

ERROR 10:03:28:311

"Error uploading *** entity: local_16285789966144, message: ***Trigger: execution of BeforeInsert\n\ncaused by: System.QueryException: Non-selective query against large object type (more than 200000 rows). Consider an indexed filter or contact salesforce.com about custom indexing.\nEven if a field is indexed a filter might still not be selective when:\n1. The filter value includes null (for instance binding with a list that contains null)\n2. Data skew exists whereby the number of matching rows is very large (for instance, filtering for a particular foreign key value that occurs many times)\nFailing Query:…"

Хорошо, зная о лимитах, мы идем перепроверить запрос в Execute Anonymous, запускаем код — все отлично работает, возвращается одна запись с дублирующимся SpecialNumber__c.

Пробуем использовать SOQL в методе цикла, согласно бест практикам — тоже не помогает.

Находим в хелпах Salesforce, что при такой ошибке, для поиска необходимого содержимого филда можно сделать его индексируемым ‘check the box External ID’, но в нашем случае это невозможно, поскольку это подразумевает уникальность номера, а мы не можем этого гарантировать, так как уникальность номера гарантирована только в пределах одной страны, а стран у нас много.

Т.е. нам необходимо для условия WHERE применить индексируемое поле. Открываем документацию Apex Developer Guide и находим , что индексируемыми полями являются:

  • Primary keys (Id, Name, and OwnerId fields)
  • Foreign keys (lookup or master-detail relationship fields)
  • Audit dates (CreatedDate and SystemModstamp fields)
  • RecordType fields (indexed for all standard objects that feature them)
  • Custom fields that are marked as External ID or Unique

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

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

...
Id recTypeId = getRecordTypeIdbyName('Account', RECORDTYPE_NAME);
List<Account> accList = [ SELECT Id, SpecialNumber__c
                          FROM Account
                          WHERE RecordTypeId = :recTypeId
                               AND IsPersonAccount = TRUE
                               AND SpecialNumber__c IN : listNewSpecialNumbers ];

private static Id getRecordTypeIdbyName (String objectName, String strRecordTypeName) {
    return Schema.getGlobalDescribe()
                 .get(objectName)
                 .getDescribe()
                 .getRecordTypeInfosByName()
                 .get(strRecordTypeName)
                 .getRecordTypeId(); }
...

Подведем итоги:

Если вы запускаете триггер для объектов, которые имеют более 200 000 записей, вы можете получить ошибку «System.QueryException: against large object type.». Таким образом система пытается избежать длительного времени выполнения и завершает неизбирательные запросы SOQL (Non-selective query).

Для наилучшей производительности запросы SOQL должны быть избирательными (Selective query), особенно для запросов внутри триггеров.

Быстрое исправление может заключаться в том, чтобы сделать рассматриваемое поле внешним идентификатором (external ID). Поскольку внешние идентификаторы индексируются автоматически, это создаст индекс и может решить проблему.

Но лучшим решением Selective query — использовать в условии стандартные индексируемые поля. Необходимо, чтобы один из фильтров запроса находился в индексированном поле, а фильтр запроса уменьшал результирующее количество строк ниже порогового значения, определенного системой.

  • В целях сохранения конфиденциальности бизнес процессы и имена переменных были изменены. Все возможные совпадения являются случайными.
10 Вподобань

Differences between the ‘External ID’ field and the ‘Unique ID’ field setting
https://help.salesforce.com/s/articleView?id=000325076&type=1

Note: You can have multiple records with the same External ID, though this it is not recommended.

1 Вподобання

Вы можете запросить у Salesforce проиндексировать дополнительные поля.

1 Вподобання

yes, you are right -

Note: You can have multiple records with the same External ID, though this it is not recommended.

This is one of the reasons why it was decided not to use ‘External ID’

1 Вподобання

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