В процессе разработки бизнес-приложений на платформе 1С перед нами часто возникает задача классификации контактных данных. Одной из наиболее распространенных является задача определения типа телефонного номера (мобильный или городской). Это критически важно для интеграции с SMS-шлюзами, настройки автоматических рассылок, валидации данных при вводе и построения аналитической отчетности.
Основная сложность заключается в том, что телефонные номера в базах данных часто хранятся в неструктурированном виде, коды мобильных операторов имеют разную длину (от 2 до 5 символов в зависимости от страны и региона), а использование медленных операторов вроде ПОДОБНО (LIKE) в тяжелых запросах приводит к существенному падению производительности СУБД. В этой статье мы подробно разберем несколько эффективных архитектурных и программных решений этой задачи.
На первый взгляд, задача кажется тривиальной. Можно попытаться написать запрос с конструкцией ПОДОБНО или использовать соединение по условию ИЛИ. Давайте проанализируем, почему этот путь ведет к техническому тупику на больших базах данных:
ПОДОБНО "%912%" или соединение таблиц по условию ПОДОБНО заставляет СУБД выполнять полное сканирование таблиц (Table Scan / Index Scan). Индексы в этом случае не работают, и запрос к справочнику с сотнями тысяч контрагентов будет выполняться недопустимо долго.Рассмотрим по шагам профессиональные методы решения этой проблемы, начиная с денормализации данных и заканчивая созданием оптимизированных классификаторов.
---
Если во главу угла ставится максимальное быстродействие при чтении данных и выполнении отчетов, наилучшим решением является денормализация. Вместо того чтобы вычислять «мобильность» номера каждый раз динамически в тяжелых запросах, мы определим тип номера один раз — в момент его записи в базу данных.
Для этого добавим в справочник или табличную часть контактной информации булевский реквизит ЭтоМобильный. Проанализируем алгоритм его заполнения в модуле объекта при записи:
Процедура ПередЗаписью(Отказ)
Если ОбменДанными.Загрузка Тогда
Возврат;
КонецЕсли;
// Проверяем, изменился ли номер телефона, чтобы не выполнять лишних вычислений
Если ЭтоНовый() Или ЭтотОбъект.НомерТелефона <> ЭтотОбъект.Ссылка.НомерТелефона Тогда
// Вызываем вспомогательную функцию для определения типа номера
ЭтотОбъект.ЭтоМобильный = РаботаСТелефонией.ОпределитьЭтоМобильный(ЭтотОбъект.НомерТелефона, ЭтотОбъект.Страна);
КонецЕсли;
КонецПроцедуры
Теперь наш запрос на получение мобильных телефонов становится невероятно простым, изящным и мгновенным, так как СУБД делает выборку по простому индексированному булеву полю:
ВЫБРАТЬ
Контрагенты.Ссылка КАК Контрагент,
Контрагенты.НомерТелефона КАК Номер
ИЗ
Справочник.Контрагенты КАК Контрагенты
ГДЕ
Контрагенты.ЭтоМобильный = ИСТИНА
Преимущества метода: Максимальная производительность СУБД. Индекс по полю ЭтоМобильный позволяет мгновенно выбирать нужные записи.
Недостатки: Требуется доработка структуры метаданных и проведение однократной обработки по заполнению нового реквизита для уже существующих записей в базе данных.
---
Если по каким-то причинам вносить изменения в структуру существующих справочников нельзя (например, база слишком большая или находится на поддержке без возможности изменения структуры), нам придется определять тип номера «на лету» в запросе. Чтобы избежать медленного оператора ПОДОБНО и условий ИЛИ, мы применим метод каскадных соединений по точным равенствам.
Для этого создадим вспомогательный Регистр Сведений КлассификаторКодов со следующей структурой:
Страна (СправочникСсылка.СтраныМира)Код (Строка, индексировать)ЭтоМобильный (Булево)Поскольку длина кодов может варьироваться от 3 до 5 символов, мы предварительно «нарежем» анализируемый номер на префиксы разной длины во временной таблице, а затем выполним серию левых соединений по жесткому равенству:
ВЫБРАТЬ
Телефоны.Ссылка КАК Ссылка,
Телефоны.Страна КАК Страна,
Телефоны.НомерТелефона КАК Номер,
ПОДСТРОКА(Телефоны.НомерТелефона, 1, 3) КАК Префикс3,
ПОДСТРОКА(Телефоны.НомерТелефона, 1, 4) КАК Префикс4,
ПОДСТРОКА(Телефоны.НомерТелефона, 1, 5) КАК Префикс5
ПОМЕСТИТЬ ВТ_ПодготовленныеНомера
ИЗ
Справочник.Контрагенты КАК Телефоны
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
Т.Ссылка КАК Контрагент,
Т.Номер КАК НомерТелефона,
ВЫБОР
КОГДА ЕСТЬNULL(К5.ЭтоМобильный, ЛОЖЬ) ТОГДА ИСТИНА
КОГДА ЕСТЬNULL(К4.ЭтоМобильный, ЛОЖЬ) ТОГДА ИСТИНА
КОГДА ЕСТЬNULL(К3.ЭтоМобильный, ЛОЖЬ) ТОГДА ИСТИНА
ИНАЧЕ ЛОЖЬ
КОНЕЦ КАК ЭтоМобильный
ИЗ
ВТ_ПодготовленныеНомера КАК Т
// Соединяем по самому длинному префиксу (5 символов)
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.КлассификаторКодов КАК К5
ПО К5.Код = Т.Префикс5
И К5.Страна = Т.Страна
// Если по 5 символам не нашли, пробуем 4 символа
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.КлассификаторКодов КАК К4
ПО К4.Код = Т.Префикс4
И К4.Страна = Т.Страна
И К5.Код ЕСТЬ NULL
// Если не нашли по 4, пробуем 3 символа
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.КлассификаторКодов КАК К3
ПО К3.Код = Т.Префикс3
И К3.Страна = Т.Страна
И К4.Код ЕСТЬ NULL
И К5.Код ЕСТЬ NULL
Почему это работает быстро? Операция ПОДСТРОКА в рамках одной таблицы выполняется СУБД чрезвычайно быстро. Все левые соединения происходят по точному равенству индексированных полей (оператор =), что позволяет планировщику СУБД использовать эффективный алгоритм Index Seek вместо сканирования таблиц. Использование условий И КX.Код ЕСТЬ NULL предотвращает лишние соединения, если более точное (длинное) совпадение уже было успешно найдено.
---
В Российской Федерации и ряде других стран регуляторы публикуют официальные реестры выделенных емкостей (диапазонов) номеров. Например, реестр Минцифры (ранее Россвязи) содержит диапазоны в формате: Код (3 знака), Начало диапазона (7 знаков), Конец диапазона (7 знаков), Оператор.
Поскольку переносимость номеров (MNP) позволяет менять оператора связи, но **запрещает перенос номеров между мобильными (DEF) и фиксированными (ABC) сетями**, проверка по диапазонам гарантирует абсолютную точность определения мобильности номера.
Для реализации этого метода создадим Регистр Сведений РеестрДиапазоновНомеров:
Код (Строка, 3 символа, индексировать)НачалоДиапазона (Число/Строка, 7 символов, индексировать)КонецДиапазона (Число/Строка, 7 символов, индексировать)ЭтоМобильный (Булево)Предположим, что номера телефонов в вашей базе очищены от кодов стран и хранятся в 10-значном формате (например, 9121234567). Напишем запрос с соединением по диапазонам:
ВЫБРАТЬ
Абоненты.Ссылка КАК Ссылка,
ПОДСТРОКА(Абоненты.НомерТелефона, 1, 3) КАК КодРегиона,
ПОДСТРОКА(Абоненты.НомерТелефона, 4, 7) КАК ТелоНомера
ПОМЕСТИТЬ ВТ_Абоненты
ИЗ
Справочник.ФизическиеЛица КАК Абоненты
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
А.Ссылка КАК ФизическоеЛицо,
ЕСТЬNULL(Реестр.ЭтоМобильный, ЛОЖЬ) КАК ЭтоМобильный
ИЗ
ВТ_Абоненты КАК А
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.РеестрДиапазоновНомеров КАК Реестр
ПО А.КодРегиона = Реестр.Код
И А.ТелоНомера МЕЖДУ Реестр.НачалоДиапазона И Реестр.КонецДиапазона
Анализ эффективности: Данный метод превосходно решает проблему так называемых «прямых» мобильных номеров (городских номеров в коде 495 или 812, которые переадресованы на мобильные SIM-карты). Если конкретный диапазон в коде 495 числится за мобильным оператором (например, МТС или Теле2), система безошибочно определит его как мобильный. Соединение по условию МЕЖДУ (BETWEEN) по индексированным полям отрабатывает на СУБД за миллисекунды.
---
Если ваша конфигурация разработана на базе современных решений от 1С, в ней гарантированно используется подсистема «Контактная информация» из БСП. Давайте проанализируем, как эта задача решается на уровне стандартов разработки 1С.
При вводе номера телефона БСП автоматически нормализует его и сохраняет в базу данных в формате XML или JSON (поле ЗначенияПолей). В этой структурированной строке уже разделены: код страны, код города, номер и добавочный. Нам не нужно парсить сырую строку самостоятельно!
Для работы с номерами рекомендуется использовать программные методы общего модуля УправлениеКонтактнойИнформацииКлиентСервер или УправлениеКонтактнойИнформацииСлужебный. Например, для интерактивной проверки или обработки при записи можно использовать следующий вызов:
// Пример разбора номера с помощью БСП
СтруктураНомера = УправлениеКонтактнойИнформацииКлиентСервер.ДетализироватьНомерТелефона(СтрокаНомера);
// На выходе получаем структуру с ключами: КодСтраны, КодГорода, Номер, Добавочный
КодГорода = СтруктураНомера.КодГорода;
Если БСП настроена корректно, вы можете опираться на типы контактной информации и классифицировать телефоны еще на этапе их ввода пользователем, исключая необходимость сложных вычислений в транзакционных запросах.
---
Подведем итог и выберем оптимальную стратегию в зависимости от условий вашей задачи:
ПОДОБНО.