Зачем 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-й колонки до конца строки» как ключ — иногда это не то, что хочешь.
Полезные модификаторы
-nnumeric-rreverse (можно на уровне ключа:-k 2,2nr)-hhuman numeric (K, M, G)-Vversion sort:v1.2 < v1.10(отлично для sortировки git-тегов)-Mmonth sort: Jan, Feb, Mar-Rrandom (для shuffling — например, sample N строк)-fcase-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
Возможности кроме просто дедупликации.
Канонический pattern: sort | uniq -c | sort -rn
Это самая частая DE-конструкция на bash. Логика:
- sort — сгруппировать одинаковые строки рядом
- uniq -c — добавить count каждой группе
- sort -rn — отсортировать по count (numeric) в обратном порядке (десятки -> один)
- 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. Алгоритм:
- Читает chunks по
--buffer-size(default ~16MB) - Сортирует chunks в памяти, пишет на disk во временные файлы
- Делает 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.
Попробуй сам
- Базовая численная сортировка:
printf "10\n2\n100\n3\n1\n" | sort -n - CSV-сортировка:
cat > /tmp/data.csv <<EOF alice,30,500 bob,25,800 carol,28,300 EOF sort -t ',' -k 3 -n /tmp/data.csv - Топ-5 самых длинных слов в /etc/dictionaries-common/words (если есть):
awk '{print length, $0}' /usr/share/dict/words 2>/dev/null \ | sort -rn | head -5 - Уникальные команды из истории:
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работает одинаково.
Главное
- 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 -ccount,-dтолько дубликаты,-uтолько уникальные.- На больших файлах:
sort --parallel=N --buffer-size=1G. Использует external merge sort. LC_ALL=C sort— быстрее и detrministic, всегда в скриптах.- Альтернатива для preserving order:
awk '!seen[$0]++'.