Merge без потерь: куда исчезают неизвестные ключи

После добавления категории выручка выросла на 7%. Формула суммы не менялась. Причина оказалась в справочнике: одному SKU соответствовали две строки.

Merge — не «подтянуть колонку». Это операция над отношением двух таблиц, и её результат зависит от уникальности ключа. До соединения ответьте на три вопроса: какая таблица задаёт состав результата, сколько строк справа допустимо для одного ключа и что делать с отсутствующим соответствием.

Профиль ключей до Merge

Продажи содержат много строк на SKU, справочник товаров — ровно одну актуальную строку на SKU. Проверим это ожидание отдельным запросом: сгруппируем справочник по SKU, посчитаем строки и оставим группы больше одной. Если список не пуст, соединение откладываем. Автоматическое удаление дублей может скрыть разные категории или цены.

Ключи также нормализуют: убирают внешние пробелы, приводят регистр, проверяют тип. Но ведущие нули удалять нельзя без договора — SKU 0017 может отличаться от 17. Нормализация должна исправлять представление, а не угадывать смысл.

Почему здесь нужен Left Outer

Главная таблица — продажи. Мы обязаны сохранить каждую продажу, даже если товар отсутствует в справочнике. Поэтому выбираем левое внешнее соединение. Inner Join отбросил бы неизвестные SKU и занизил выручку, причём итог выглядел бы аккуратно.

Merged = Table.NestedJoin(
    Sales,
    {"SKU"},
    Products,
    {"SKU"},
    "Product",
    JoinKind.LeftOuter
),
WithMatchCount = Table.AddColumn(
    Merged,
    "ProductMatches",
    each Table.RowCount([Product]),
    Int64.Type
),
Expanded = Table.ExpandTableColumn(
    WithMatchCount,
    "Product",
    {"Category", "Brand"},
    {"Product.Category", "Product.Brand"}
)

Столбец ProductMatches — предохранитель. Значение 0 означает отсутствующий ключ, 1 — норму, больше 1 — размножение строки. Его можно удалить из финальной витрины, но сначала строим две проверки.

Anti Join как список работы

Left Anti Join возвращает продажи, которым не нашлось товара. Группируем их по SKU, считаем количество заказов и сумму. Получается приоритизированная очередь качества данных: сначала исправляем неизвестный SKU с миллионом выручки, а не тот, который встретился один раз.

Обратный Right Anti или Left Anti от справочника к продажам показывает товары без активности. Это уже другой бизнес-вопрос и не является ошибкой соединения.

Сверяем гранулярность и сумму

До и после Merge должны совпасть количество строк продаж и общая сумма, если справа гарантирован один товар. Увеличение строк указывает на дубли справочника; уменьшение — на неверный тип соединения или фильтр. Сумма может вырасти даже при незаметном числе дублей, поэтому проверяем оба показателя.

Строк до Merge48 211
Строк после Merge48 211
SKU без соответствия17
Строк с несколькими соответствиями0
Сумма до / послесовпадает

Задание: намеренно продублируйте один SKU в справочнике и сравните сумму до и после Merge. Затем добавьте ProductMatches и сделайте запрос, который останавливает публикацию при значении больше единицы.

Хорошее соединение не прячет качество справочника. Оно сохраняет факты, показывает отсутствующие связи и доказывает, что детализация не изменилась.

Проверьте себя
1. Какой тип соединения сохраняет все строки продаж при отсутствии товара в справочнике?
AInner
BLeft Outer
CLeft Anti
DFull Outer только
2. О чём говорит ProductMatches больше 1?
ASKU отсутствует
BСправа найдено несколько строк, и факт может размножиться
CФайл пуст
DТип данных всегда верен
3. Для чего полезен Left Anti Join продаж со справочником?
AЧтобы получить только найденные товары
BЧтобы построить список SKU без соответствия
CЧтобы удалить даты
DЧтобы объединить файлы папки