Часто при создании приложений Qlik Sense (QlikView) формирует синтетические ключи, выводя предупреждения об их создании в журнал загрузки. При этом распространено мнение о том, что любой синтетический ключ это плохо, от него следует непременно избавляться: часто в требованиях к разработке и архитектуре приложений в крупных организациях даже указываются запреты на публикацию приложений содержащих синтетические ключи в продуктивной среде.
В данной статье мы разберемся что такое синтетические ключи и как с ними можно бороться, а главное нужно ли это делать.
Согласно официальной документации Qlik
Qlik Sense создает синтетические ключи, если в нескольких таблицах есть два общих поля или более. Эти ключи представляют собой анонимные поля, включающие все возможные сочетания составного ключа.
Если во время загрузки данных получено предупреждение о синтетических ключах, следует проверить структуру данных в просмотре модели данных. Необходимо убедиться, что в модели данных нет ошибки. Иногда это является причиной такого предупреждения, но в основном это связано с ошибкой в скрипте.
Наличие множества синтетических ключей зачастую свидетельствует о неправильной модели данных. Тем не менее, верный признак неправильной модели данных — это наличие синтетических ключей на основе других синтетических ключей.
Давайте резюмируем:
Синтетический ключ — это всего лишь связь нескольких таблиц по двум и более полям;
Синтетический ключ физически — это невидимое для составления визуализаций поле, которое не может быть использовано в выражениях на листе или в скрипте;
Наличие синтетического ключа может означать проблемы в модели данных, а может не означать. Обратите внимание, это действительно важно – синтетический ключ может означать проблему, но не обязательно означает проблему. В каждом конкретном случае ответ будет разный;
Синтетические ключи состоящие из синтетических ключей явный признак проблемы;
Проблемы синтетических ключей
Согласно той же документации основная проблема возникающая при формировании синтетических ключей связана с производительностью:
Согласно официальной документации Qlik
Если число синтетических ключей возрастает в зависимости от объемов данных, структуры таблиц и других факторов, в программе Qlik Sense они могут или не могут быть полноценно обработаны, а также может потребоваться дополнительное количество времени и/или памяти. В таком случае необходимо разработать скрипт заново, удалив все синтетические ключи.
Однако по опыту это далеко не так. Самая частая проблема синтетических ключей в том, что модель данных сформирована неправильно и пользователь, отбирающий данные или вычисляющий показатели, может получать неверные значения либо вообще не сможет записать выражение меры чтобы вычислить нужный показатель. Проблема с производительностью возникает не так часто, в тех случаях, когда в приложении огромное количество данных и синтетических ключей. Основная беда синтетических ключей — неясность.
Давайте рассмотрим несколько примеров синтетических ключей и определим проблемы, возникающие при их формировании, а также определим способы решения проблем.
Пример 1: Одинаковые названия свойств разных сущностей
Пусть у нас есть две таблицы Товары и Контрагенты. Таблица Товары выглядит следующим образом:
Код
Наименование
Категория
1
Гвозди
Скобяные
2
Шурупы
Скобяные
3
Скобы
Скобяные
Таблица Контрагенты выглядит следующим образом:
Код
Наименование
1
Вектор+
2
Прогресс
3
Центр-сервис
4
Меркурий
Если написать скрипт загрузки данных и загрузить данные ничего не изменяя, мы получим следующие результаты. В журнале загрузке данных будет сообщение о формировании синтетического ключа:
Модель данных будет выглядеть следующим образом (слева вид внутренней таблицы, справа вид исходной таблицы):
Появился ключ по двум полям Код и Наименование. И здесь нам необходимо задать следующие вопросы:
В поле Код которое собрано из двух таблиц одно и тоже свойство или нет? Когда мы выбираем в поле Код значение 1 какой результат мы ожидаем?
В поле Наименование собрано из двух таблиц одно и тоже свойство или нет? Когда мы выбираем в поле Наименование значение что мы отбираем — товары или контрагентов?
Как связаны товары и контрагенты в реальности?
Ответив на эти вопросы мы понимаем что поле Код содержит как свойство Код товара так и свойство Код контрагента, поле Наименование в свою очередь содержит как Наименование товара так и Наименование контрагента. То есть мы загрузили в одно поле разные сущности! Пользователь, отбирающий данные в поле Код не будет понимать что он выбирает — то ли товары по коду, то ли контрагентов.
Для исправления данной проблемы нам необходимо просто переименовать поля или применить оператор QUALIFY, например так:
QUALIFY Код, Наименование;
Однако я настоятельно не рекомендую использовать операторы QUALIFY/UNQUALIFY в приложениях размещаемых на рабочем сервере, поскольку они делают результат выполнения скрипта труднопредсказуемым и при разработке приходится держать в голове какие поля подлежат добавлению наименования таблицы в поле, а какие нет. Поэтому давайте просто переименуем поля при загрузке:
Товары: LOAD Код as ТоварКод, Наименование as ТоварНаименование, Категория as КатегорияНаименование INLINE [ Код Наименование Категория 1 Гвозди Скобяные 2 Шурупы Скобяные 3 Скобы Скобяные ] (delimiter is spaces);
Контрагенты: LOAD Код as КонтрагентКод, Наименование as КонтрагентНаименование INLINE [ Код Наименование 1 Вектор+ 2 Прогресс 3 Центр-сервис 4 Меркурий ] (delimiter is spaces);
Мы получаем модель данных, в которой таблицы никак не связаны, потому что нет между этими сущностям напрямую никакой связи в наших исходных данных.
Пример 2: Дублирование свойств
Пусть у нас есть две таблицы Товары и Продажи. Таблица Товары выглядит следующим образом:
ТоварКод
ТоварНаименование
Категория
1
Гвозди
Скобяные
2
Шурупы
Скобяные
3
Скобы
Скобяные
Таблица Продажи содержит следующие данные:
МагазинКод
ТоварКод
ТоварНаименование
Категория
Количество
Сумма
1
1
Гвозди
Скобяные
5
450
2
1
Гвозди
Скобяные
6
540
2
2
Шурупы
Скобяные
8
1600
1
1
Гвозди
Скобяные
10
900
3
2
Шурупы
Скобяные
2
400
3
2
Шурупы
Скобяные
11
2200
3
3
Шурупы
Скобяные
4
800
1
3
Скобы
Скобяные
3
270
Модель данных после загрузки будет выглядеть следующим образом (слева вид внутренней таблицы, справа вид исходной таблицы):
Появился ключ по трем полям ТоварКод и ТоварНаименование и Категория. Мы понимаем что поля ТоварКод, ТоварНаименование и Категория содаржат то, что в них и должно быть и никакой путаницы с другими сущностями (как в предыдущем примере нет). И здесь нам необходимо задать следующие вопросы:
Что из полей ТоварКод, ТоварНаименование, Категория является идентификатором а что свойством?
Для какой таблицы свойства первичны? Для какой строго необходимы?
Что будет если мы выберем значение в поле ТоварНаименование?
По примеру понятно что таблица Товары это справочник и она содержит все товары. Поле ТоварКод это ключевое поле которое хорошо свяжет таблицы Продажи и Товары и без использования полей ТоварНаименование и Категория. Последние являются всего лишь свойствами, которые первичны для таблицы-справочника Товары и вторичны для таблицы Продажи. То есть если товар не продавался, то в таблице продаж не будет записи о товаре и соотвественно не будет записей о его наименовании и категории.
Потому мы просто не загружаем или удаляем поля ТоварНаименование и Категория из таблицы Продажи:
DROP FIELDS ТоварНаименование, Категория FROM Продажи;
В примере выше мы использовав оператор DROP удаляем поля из таблицы и получаем модель данных, в которой нет синтетического ключа и таблицы связаны по одному полю — ТоварКод.
Пример 3: Похожие сущности в одну таблицу
Пусть у нас есть две таблицы Поставки и Списания. Таблица Поставки выглядит следующим образом:
ПоставщикКод
ПодразделениеКод
Дата
ТоварКод
Количество
Сумма
1
4
31.01.2020
3
191
11651
1
5
13.09.2020
3
22
1342
1
3
23.04.2020
7
249
15189
4
4
10.10.2020
3
217
13237
2
2
15.05.2020
2
346
21106
Таблица Списания выглядит следующим образом:
СписаниеСтатья
ПодразделениеКод
Дата
ТоварКод
Количество
Сумма
Статья 1
1
11.10.2020
2
8
844
Статья 2
2
18.08.2020
2
8
844
Статья 3
5
26.04.2020
3
8
844
Статья 4
2
16.07.2020
6
5
527,5
Статья 5
5
07.01.2020
3
4
422
Статья 1
3
18.09.2020
5
6
633
При загрузке таких таблиц в приложение мы получаем следующую модель данных:
Qlik Sense создал синтетический ключ сразу по пяти полям! Давайте подумаем похож ли этот пример на предыдущие два примера и можем ли мы применить решения, озвученные выше:
Можем ли мы попросту не загружать какие либо поля потому что есть таблица где они первичны и таблица где они вторичны? Нет, все поля нам нужны и отражают важные и неповторимые в других таблицах свойства сущностей — Поставок и Списаний.
Можем ли мы переименовать поля так чтобы не было одинаковых наименований? Да, но появится проблема — например если мы добавил в модель данных справочник товаров из предыдущего примера то к какой таблице его добавлять к Поставкам или к Списаниям? К обоим мы добавить не сможем, так как образуется циклическая ссылка.
Для разрешения проблемы с этим синтетическим ключем мы применим трансформацию данных — а именно соберем обе таблицы в одну используя префикс CONCATENATE. А для того, чтобы отделить строки при вычислении мер, добавим дополнительный столбец ТипДокумента в который запишем строку «Списание» для данных из таблицы Списания и «Поставка» для данных из таблицы Поставки. При этом результирующую таблицу назовем Факты (или Документы или ДвижениеТовара) так как она будет содержать несколько различных сущностей.
CONCATENATE(Факты) LOAD *, 'Списание' as ТипДокумента INLINE [ СписаниеСтатья ПодразделениеКод Дата ТоварКод Количество Сумма Статья 1 1 44115 2 8 844 Статья 2 2 44061 2 8 844 Статья 3 5 43947 3 8 844 Статья 4 2 44028 6 5 527,5 Статья 5 5 43837 3 4 422 Статья 1 3 44092 5 6 633 ](delimiter is spaces);
При выполнении скрипта мы получим следующую одну таблицу в модели данных:
Теперь для вычисления мер, в которых отделены Поставки и Списания мы можем использовать Анализ множеств. Например следующим образом:
Мера «Сумма списаний»
SUM({<ТипДокумента={'Списание'}>} Сумма)
Мера «Сумма поставок»
SUM({<ТипДокумента={'Поставка'}>} Сумма)
Пример 4: Добавляем дополнительное свойство к таблице
Пусть у нас есть две таблицы Продажи и Ответственные. Таблица Ответственые содержит фамилии отвественных сотрудников за товар в конкретном подразделении. Таблица Отвественные выглядит следующим образом:
ТоварКод
ПодразделениеКод
МенеджерТовара
1
1
Иванов
2
1
Иванов
3
1
Иванов
4
1
Петров
1
2
Сидоров
2
2
Сидоров
3
2
Сидоров
4
2
Сидоров
1
3
Николаев
2
3
Николаев
3
3
Александров
4
3
Александров
Таблица Продажи выглядит следующим образом:
ПодразделениеКод
Дата
ТоварКод
Количество
Сумма
2
03.10.2020
4
442
51051
3
13.07.2020
1
464
53592
3
02.03.2020
2
410
47355
1
15.01.2020
2
97
11203,5
2
28.11.2020
3
354
40887
При загрузке двух таблиц мы получаем следующую модель данных:
Появился синтетический ключ по двум полям ПодразделениеКод и ТоварКод. Но в данном примере таблицы действительно логически связываются по двум полям — ПодразделениеКод и ТоварКод, то есть невозможно сделать связь только по одному из этих полей. Как поступить в данной ситуации? Хорошим решением будет соединить две таблицы в одну с помощью JOIN.
LEFT JOIN(Продажи) LOAD * INLINE [ ТоварКод ПодразделениеКод МенеджерТовара 1 1 Иванов 2 1 Иванов 3 1 Иванов 4 1 Петров 1 2 Сидоров 2 2 Сидоров 3 2 Сидоров 4 2 Сидоров 1 3 Николаев 2 3 Николаев 3 3 Александров 4 3 Александров ](delimiter is spaces);
В примере мы используем LEFT JOIN чтобы у нас не поменялось количество записей в таблице Продажи. Но такая трансформация будет актуальна только в случае если нас не интересуют записи об отвественных по которым не было продаж и для каждого товара в подразделении определен только один ответственный. Поскольку если для товара в подразделении будет несколько отвественных, то в таблице Продажи данные будут задублированы.
Итоговая модель данных после использования LEFT JOIN будет выглядеть следующим образом:
Пример 5: Превращаем синтетический ключ в обычный
Бывают случаи аналогичные предыдущему примере: когда есть две таблицы, логическая связь между которыми осуществляется по нескольким полям, но вариант с JOIN не применим.
Например, мы имеем таблицы Продажи и НормыСписания. Таблица Продажи имеет следующую структуру:
ПодразделениеКод
Дата
Месяц
ТоварКод
Количество
Сумма
2
03.10.2020
Март 2020
4
442
51051
3
13.07.2020
Июль 2020
1
464
53592
3
02.03.2020
Март 2020
2
410
47355
1
15.01.2020
Январь 2020
2
97
11203,5
2
28.11.2020
Ноябрь 2020
3
354
40887
Таблица НормыСписания содержит суммы допустимые для списания за каждый месяц по каждому из подразделений, которые есть в данных, она имеет следующую структуру:
Месяц
ПодразделениеКод
НормаСписания
Январь 2020
1
1000000
Март 2020
1
1500000
Июль 2020
1
2000000
Ноябрь 2020
1
1700000
Январь 2020
2
1000000
Март 2020
2
1500000
Июль 2020
2
2000000
Ноябрь 2020
2
1700000
Январь 2020
3
1000000
Март 2020
3
1500000
Июль 2020
3
2000000
Ноябрь 2020
3
1700000
Если загрузить обе эти таблицы мы получим следующую модель данных:
Допустим, мы хотим считать по данной модели данных сумму по норме списаний за каждый месяц мерой SUM(НормаСписания), вариант с JOIN нам не подойдет, потому что он попросту задублирует нам данные так как к одному сочетанию ПодразделениеКод + Месяц из таблицы НормаСписания может быть сколько угодно (например 0 или 50) значений таблицы Продажи.
Есть еще несколько вариантов которые мы не опробовали в предыдущих примерах. Первый из них оставить данный синтетический ключ в покое, пусть себе будет. Какие проблемы от того что в этой модели данных есть синтетический ключ? Правильный ответ — никаких. В обеих таблицах не миллиард строк, поэтому проблем с производительностью не будет. Будут ли неправильно работать отборы? Нет не будут, потому что эти две таблицы действительно связываются логически по комбинации двух полей.
Второй вариант — сделать одно поле в которое будут входить оба поля составляющих синтетический ключ. Результат будет такой же как и в первом варианте, но пропадет это неприятное предупреждение в конце загрузки данных:
Самый простой способ сделать из двух полей одно — это сложить их как строки. Например таким образом:
ПодразделениеКод & ‘-‘ & Месяц as Ключ_ПодразделениеКодМесяц
Может появится вопрос — почему я не сложил просто две строки так:
ПодразделениеКод & Месяц as Ключ_ПодразделениеКодМесяц
Зачем потребовалось добавлять тире между значениями ПодразделениеКод и Месяц? Ответ очень простой — если ПодразделениеКод равно 1 и Месяц равен 12, то в поле Ключ_ПодразделениеКодМесяц будет значение 112. Но с другой стороны если ПодразделениеКод равно 11 и Месяц равен 2, то в поле Ключ_ПодразделениеКодМесяц будет тоже 112. А это неправильно, ключ должен быть уникален для уникальных пар кода подразделения и месяца. Разделитель в виде тире или любого другого символа позволяет сделать наше новое ключевое поле правильным для примеров указанных выше (1-12 и 11-2 соотвственно).
После загрузки мы получим следующую модель данных:
В модели нет синтетических ключей, но вот некоторым разработчикам не нравится то, что есть поле Ключ_Подразделения в котором очень длинная строка (Кстати не важно насколько длинное ключевое поле с точки зрения производительности работы связи между таблицами). Эту строку можно заменить на более короткий код следующим образом:
Autonumber(ПодразделениеКод & ‘-‘ & Месяц) as Ключ_ПодразделениеКодМесяц
Функция AutoNumber возвращает уникальное значение в виде целого числа для каждого уникального значения указанного в выражении.
Либо можно не меняя LOAD оператор в конце скрипта записать следующую строку:
Autonumber Ключ_ПодразделениеКодМесяц;
Оператор AutoNumber для всех полей указанных через запятую (в нашем случае всего одно поле) создает для каждого уникального значения поля уникальное целочисленное значение и записывает его вместо значения поля.
Итоги
Как вы уже понимаете не все так однозначно с синтетическими ключами. Синтетический ключ может свидетельствовать о проблеме, и действительно каждое предупреждение о формировании синтетического ключа является сигналом к проверке полученной модели данных. При этом для устранения синтетического ключа мы можем выполнить одно из следующих действий:
Не загружать или удалить поля которые дублируют друг друга;
Переименовать поля которые названы одинаково, но являются свойствами разных сущностей;
Произвести изменения в модели данных, например — использовать JOIN или CONCATENATE для объединения двух таблиц в одну. Можно также использовать различные паттерны, например таблицу линковки;
Исследовать синтетический ключ и понять что он не создает дополнительных проблем, оставить его в покое или преобразовать в новое ключевое поле.
Помните также о том, что рассмотренные в статье примеры это ключевые приемы, с помощью которых можно решить любую большую задачу. Достаточно лишь разбить любую большую задачу на много маленьких и правильно использовать изученные приемы к разным частям задачи.