join теряет совпадения: одинаковая сортировка ключей и LC_ALL

Два TSV-файла отсортированы, общий идентификатор есть в обоих, но join сообщает, что вход не упорядочен, или выдаёт неожиданные пропуски. Слово «отсортированы» здесь слишком неопределённо: важны ключ, разделитель и правила сравнения.

join сопоставляет соседние группы одинаковых ключей. Он не ищет каждую запись по всему второму файлу. Значит, оба входа должны идти в порядке, который совпадает с его сравнением. Числовая сортировка идентификаторов и лексикографическое сравнение — разные порядки; локаль также может менять положение букв и знаков.

Подготовим обе стороны одним правилом

У нас отдельные строки с идентификатором и числом заказов, а во втором файле — идентификатор и регион. В значениях нет табуляции или переводов строки. LC_ALL=C делает сравнение байтовым и одинаковым на всех трёх шагах.

#!/usr/bin/env bash
folder=$(mktemp -d) || exit 1
export LC_ALL=C
printf 'u2	7
u10	3
u1	5
' > "$folder/orders.tsv"
printf 'u1	North
u2	South
u9	West
' > "$folder/users.tsv"
sort -t $'	' -k1,1 "$folder/orders.tsv" > "$folder/orders.sorted"
sort -t $'	' -k1,1 "$folder/users.tsv" > "$folder/users.sorted"
join -t $'	' "$folder/orders.sorted" "$folder/users.sorted"
printf 'Unmatched orders:
'
join -t $'	' -v 1 "$folder/orders.sorted" "$folder/users.sorted"

Основной результат содержит u1 с 5 и North, затем u2 с 7 и South. Отдельный вывод показывает u10 с 3: в справочнике такого пользователя нет. Исчезновение строки в обычном внутреннем соединении теперь объясняется конкретным отсутствующим ключом.

При байтовом порядке u10 идёт перед u2. Это правильно для строковых идентификаторов. Не добавляйте -n к sort только потому, что часть ключа похожа на число. Если ключ действительно числовой, нормализуйте его представление и выберите согласованные правила для всего конвейера.

Разделитель тоже часть формата

Без -t join по умолчанию рассматривает пробельные границы полей. Для TSV нужен именно символ табуляции; запись $'\t' создаёт его средствами Bash. Ключ второго столбца потребовал бы согласованных -k2,2 у sort и -1 2 либо -2 2 у join. Изменение только одного шага нарушает договор.

Заголовок TSV нельзя оставлять среди строк данных: он участвует в сортировке и может случайно соединиться с заголовком второй стороны. Вынесите заголовки отдельно и добавляйте после обработки. Для полноценного CSV с кавычками, запятыми внутри значений и переносами строк нужен CSV-парсер: join не интерпретирует такой формат.

Проверьте уникальность справочника

Если один ключ встречается дважды с обеих сторон, соединение создаёт комбинации совпадений. Сортировка не исправляет неоднозначность. Для справочника сначала проверьте повторяющиеся ключи через cut и uniq после сортировки, затем решите, какую запись считать актуальной.

Сохраняйте количество входных, совпавших и несовпавших записей рядом с результатом. Так изменение справочника будет видно до того, как итоговый отчёт станет выглядеть подозрительно маленьким. Для отладки достаточно взять один потерянный ключ и сравнить его байтовое написание в обоих входах: пробел в конце является частью значения при явной табуляции.

Чтобы соединять такие команды в повторяемые обработки, пригодятся потоки, текстовые утилиты и проверка статусов. Они входят в программу Bash Linux + Git вместе с авторскими задачами. Откройте курс и соберите собственный рабочий конвейер обработки файлов.

Продолжить на практике