Learning Platform
Глоссарий Troubleshooting
Урок 08.02 · 20 мин
Начальный
sortuniqPipelineAggregationText processing

Зачем sort + uniq

Эти две утилиты в паре делают то, что в SQL делает GROUP BY ... ORDER BY ... COUNT(*). И они работают на потоковых данных — без загрузки всего в память (sort использует disk-based merge sort на больших input).

Канонический pattern:

# Топ-5 самых популярных IP в access.log:
$ awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -5
   1247 192.168.1.10
    893 10.0.0.45
    412 192.168.1.50
    288 10.0.0.123
    156 172.16.0.20

Каждый шаг — отдельная утилита. Это UNIX-философия: каждая программа делает одно, pipe-ы соединяют. Альтернатива в Python потребовала бы 20+ строк с Counter или pandas.

sort: сортировка строк

$ sort file              # lexicographic
$ sort -n file           # numeric: 2 < 10 < 100
$ sort -r file           # reverse: убывание
$ sort -k 2 file         # sort by 2nd column
$ sort -t ',' -k 3 file  # CSV: delimiter ',', 3-я колонка
$ sort -u file           # unique: дубликаты пропустить
$ sort -h file           # human-readable: 1K < 1M < 1G

По умолчанию sort использует lexicographic (lexical) сравнение — символ за символом по ASCII/locale. Числа сортируются как строки: 10 идёт раньше 2, потому что '1' < '2'. Для чисел всегда нужен -n:

$ printf "10\n2\n100\n3\n" | sort
10
100
2
3

$ printf "10\n2\n100\n3\n" | sort -n
2
3
10
100

Ключи: -k

-k N сортирует по N-й колонке (разделитель — whitespace по умолчанию, в -t ',' — запятая, в -t '|' — пайп).

$ cat data.csv
apple,10,USA
banana,5,Ecuador
cherry,20,Turkey

$ sort -t ',' -k 2 -n data.csv
banana,5,Ecuador
apple,10,USA
cherry,20,Turkey

Можно комбинировать несколько ключей: sort -k 2,2n -k 1,1 — сначала по 2-й колонке numeric, потом по 1-й lexicographic. Это многоуровневая сортировка как ORDER BY col2, col1 в SQL.

Формат ключа: -k F[,F][MODIFIERS]. Без второй F sort использует «от F-й колонки до конца строки» как ключ — иногда это не то, что хочешь.

Полезные модификаторы

  • -n numeric
  • -r reverse (можно на уровне ключа: -k 2,2nr)
  • -h human numeric (K, M, G)
  • -V version sort: v1.2 < v1.10 (отлично для sortировки git-тегов)
  • -M month sort: Jan, Feb, Mar
  • -R random (для shuffling — например, sample N строк)
  • -f case-fold (ignore case)
# Сортировка файлов по размеру (du output)
$ du -sh /var/log/* | sort -h
12K   /var/log/dmesg.old
1.2M  /var/log/auth.log
45M   /var/log/syslog
1.8G  /var/log/journal

# Сортировка по версии
$ git tag | sort -V | tail
v2.10.0
v2.10.1
v2.11.0
v2.11.1
v2.12.0

sort -u vs sort | uniq

$ sort -u file
$ sort file | uniq

Оба удаляют дубликаты. Разница:

  • sort -u делает уникальность внутри sort’а — быстрее, меньше памяти.
  • sort | uniq требует двух passes по данным.

Если просто нужны уникальные строкиsort -u. Если нужны uniq -c (count), uniq -d (duplicates only) — комбинация необходима.

uniq: но только соседние

Важнейший gotcha: uniq смотрит только соседние строки. Он не строит hash-map всех viewed lines. Если две одинаковые строки разделены другой — uniq не заметит:

$ printf "a\nb\na\nb\n" | uniq
a
b
a
b
# Никаких дубликатов не удалено!

$ printf "a\nb\na\nb\n" | sort | uniq
a
b
# Теперь работает

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

Правило: всегда sort | uniq. sort обязателен.

Флаги uniq

Что умеет uniq

Возможности кроме просто дедупликации.

uniqдедуплицировать (default)Из соседних дубликатов оставляет одну строку.
uniq -cпрефикс с countКаждая строка получает 'N имя_строки'. КРИТИЧНО для DE: основной way to count groups.
uniq -dтолько дубликатыВыводит только строки, которые встречались > 1 раза. Полезно для duplicate detection в данных.
uniq -uтолько уникальныеПротивоположность -d: строки, встретившиеся РОВНО один раз.
uniq -icase-insensitive'ERROR' и 'error' считаются одинаковыми.
uniq -f Nигнорировать первые N полейuniq -f 1 — игнорировать первое поле (часто timestamp) при сравнении.
uniq -s Nигнорировать первые N символовuniq -s 20 — пропустить первые 20 символов (часто timestamp фиксированной длины).
uniq -w Nсравнивать только первые N символовuniq -w 10 — учитывать только первые 10 символов (например, дату из ISO timestamp).

Канонический pattern: sort | uniq -c | sort -rn

Это самая частая DE-конструкция на bash. Логика:

  1. sort — сгруппировать одинаковые строки рядом
  2. uniq -c — добавить count каждой группе
  3. sort -rn — отсортировать по count (numeric) в обратном порядке (десятки -> один)
  4. head — top N
# Топ-10 ошибок в логах Airflow
$ grep ERROR scheduler.log \
    | awk -F'msg=' '{print $2}' \
    | sort | uniq -c | sort -rn | head -10
    287 Connection refused
    143 Timeout 30s
     67 OOM killed
     45 Disk full
     12 DAG not found

Это эквивалент SQL:

SELECT msg, COUNT(*) AS cnt
FROM logs
WHERE level = 'ERROR'
GROUP BY msg
ORDER BY cnt DESC
LIMIT 10;

Но работает на потоке любой длины без необходимости таблицы.

GROUP BY в SQL — агрегация данных в базе данных

DE-сценарии

1. Распределение HTTP-кодов в access.log

$ awk '{print $9}' access.log | sort | uniq -c | sort -rn
  9821 200
   543 404
   287 304
    45 500
    12 502

Видно, что есть 45 пятисоток (5xx server errors) — это сигнал.

2. Самые большие файлы в директории

$ du -ah /var/log/airflow | sort -h | tail -10
  234K  /var/log/airflow/scheduler.log.5.gz
  4.5M  /var/log/airflow/dag_processor_manager.log
  12M   /var/log/airflow/scheduler.log
  ...
  1.2G  /var/log/airflow

sort -h понимает суффиксы K, M, G — сортировка человекочитаемая.

3. Дубликаты email в CSV

# Найти строки с дубликатами email (колонка 2 в CSV)
$ awk -F',' '{print $2}' users.csv | sort | uniq -d
[email protected]
[email protected]

uniq -d выводит только строки, которые встретились более одного раза. Идеально для data quality checks.

4. Топ-N с группировкой по полю

Когда тебе надо группировать по полю не последовательно (не всю строку), используй cut/awk для извлечения поля, потом sort | uniq -c:

# Топ-10 запросов по endpoint в Nginx-логе
$ awk '{print $7}' access.log | sort | uniq -c | sort -rn | head -10

$7 в combined-формате nginx — это URL. Сначала вытащили URL, потом группировка-count-сортировка.

sort и большие файлы

sort работает на input любой длины: использует external merge sort для файлов больше RAM. Алгоритм:

  1. Читает chunks по --buffer-size (default ~16MB)
  2. Сортирует chunks в памяти, пишет на disk во временные файлы
  3. Делает k-way merge временных файлов

Можно тюнить:

$ sort --buffer-size=1G --parallel=4 huge_file.txt > sorted.txt

--parallel=N использует N тредов на in-memory sort. На современных серверах сортировка 100GB файла с 64GB RAM занимает считанные минуты на NVMe.

Альтернативы:

  • GNU parallel + sort на chunks
  • Если данные уже отсортированы по ключу — используй sort -m (merge-only)
  • Для очень больших данных лучше pandas/DuckDB/Spark, но bash хорош до десятков GB.

LC_ALL: locale влияет

Сортировка зависит от locale (язык/региональные настройки), потому что разные языки имеют разные правила сортировки:

$ printf "a\nA\nb\nB\n" | sort        # locale-зависит
$ printf "a\nA\nb\nB\n" | LC_ALL=C sort  # ASCII-сравнение

В locale en_US.UTF-8 сортировка может ставить A и a рядом (по слову, не по букве). В C/POSIX — строгий ASCII: A < a (потому что 0x41 < 0x61).

Для reproducibility в скриптах всегда LC_ALL=C sort. Без этого результат может различаться на двух машинах с разным locale.

# В скриптах:
$ export LC_ALL=C
$ ./script.sh

Бонус — LC_ALL=C sort намного быстрее: ASCII-сравнение легче, чем UTF-8 multibyte с правилами locale.

Попробуй сам

  1. Базовая численная сортировка:
    printf "10\n2\n100\n3\n1\n" | sort -n
  2. CSV-сортировка:
    cat > /tmp/data.csv <<EOF
    alice,30,500
    bob,25,800
    carol,28,300
    EOF
    sort -t ',' -k 3 -n /tmp/data.csv
  3. Топ-5 самых длинных слов в /etc/dictionaries-common/words (если есть):
    awk '{print length, $0}' /usr/share/dict/words 2>/dev/null \
      | sort -rn | head -5
  4. Уникальные команды из истории:
    history | awk '{print $2}' | sort | uniq -c | sort -rn | head

macOS-различия

  • BSD sort на macOS:
    • Не поддерживает --parallel — для thread-parallel sort нужен GNU sort (brew install coreutils, потом gsort)
    • -h (human numeric) поддерживается
    • -V (version sort) поддерживается
    • -R (random) поддерживается
  • uniq на macOS работает идентично.
  • LC_ALL=C работает одинаково.
Проверка знанийKnowledge check
Объясни, почему 'uniq file' (без sort) часто даёт неправильный результат. Приведи минимальный пример. Какой паттерн правильный?
ОтветAnswer
uniq смотрит ТОЛЬКО на соседние (адjacent) строки. Он сравнивает текущую строку с предыдущей и пропускает, если они равны. Между несоседними дубликатами он не видит связи. Пример: printf 'a\nb\na\nb\n' | uniq выводит a, b, a, b — никаких дедупликаций, потому что 'a' и 'a' не соседи. Это by design: uniq потоковый, держит в памяти только last seen line, что позволяет работать с infinite streams без памяти O(N). Правильный pattern: sort | uniq. sort приводит дубликаты в смежные позиции, потом uniq их дедуплицирует. Альтернатива — awk '!seen[$0]++' file: awk строит hash в памяти, дедуплицирует без sort и СОХРАНЯЕТ порядок (sort | uniq меняет порядок). Когда какой: sort | uniq дешевле по памяти на больших файлах. awk быстрее если файл небольшой и нужен исходный порядок. На потоках логов с timestamp'ом — awk не нужен (sorted by time).

Главное

  • sort — флаги: -n (numeric), -r (reverse), -k N (column), -t SEP (delimiter), -u (unique), -h (human numeric), -V (version).
  • uniq работает только на соседних строках — поэтому всегда в паре с sort.
  • Канон: sort | uniq -c | sort -rn — эквивалент GROUP BY ... ORDER BY COUNT(*) DESC.
  • uniq -c count, -d только дубликаты, -u только уникальные.
  • На больших файлах: sort --parallel=N --buffer-size=1G. Использует external merge sort.
  • LC_ALL=C sort — быстрее и detrministic, всегда в скриптах.
  • Альтернатива для preserving order: awk '!seen[$0]++'.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Команда 'sort numbers.txt' для файла со строками '10', '2', '100', '3' возвращает '10, 100, 2, 3'. Это баг?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5