Bashのシェルスクリプトやコマンドライン操作において、重複データを整理する際に頻繁に利用されるのが「sort | uniq」というパイプラインの組み合わせです。
初心者の方は、なぜuniq単体ではなく、事前にsortを実行しなければならないのか疑問に思うことも多いでしょう。
この動作原理を正しく理解することは、データ処理のミスを防ぐだけでなく、Linux/UNIXシステムの設計思想を深く知るきっかけにもなります。
本記事では、uniqコマンドがなぜsortを必要とするのか、その内部的な仕組みと実践的な活用方法を詳しく解き明かしていきます。
uniqコマンドの基本仕様と「隣接行」の制約
uniqコマンドは、入力されたテキストから重複した行を排除、あるいは抽出するためのツールです。
しかし、このコマンドには非常に重要な特性があり、それは「直前の行と同じ内容であるかどうか」のみを判定基準にしているという点です。
つまり、uniqはファイル全体を俯瞰して重複を探すのではなく、ストリームとして流れてくるデータを1行ずつ読み込み、現在の行と1つ前の行を比較しているに過ぎません。
このため、同じ内容の行が離れた場所に存在している場合、uniq単体ではそれらを「重複」として認識することができないのです。
具体的に、ソートされていないデータをuniqに渡した場合の挙動をコードで確認してみましょう。
# 重複が離れているサンプルデータを作成
cat << EOF > sample.txt
apple
orange
apple
banana
orange
EOF
# uniq単体で実行
uniq sample.txt
apple
orange
apple
banana
orange
このように、実行結果を見ると全く重複が排除されていないことがわかります。
「apple」や「orange」といった単語が複数出現していますが、それらが連続して並んでいないため、uniqはそれぞれを独立したデータとして処理してしまった結果です。
なぜsortと組み合わせる必要があるのか
前述の通り、uniqが隣接する行しか比較しないのであれば、重複行を確実に削除するためには「同じ内容の行をあらかじめ隣り合わせに並べる」という工程が必要になります。
この役割を担うのがsortコマンドです。
sortコマンドを実行すると、テキストデータは辞書順(あるいは指定した順序)に並び替えられます。
これにより、同一の文字列を含む行は必ず上下に隣接した状態になります。
この「整列された状態」のデータをパイプラインでuniqに渡すことで、初めて正確な重複削除が可能になるのです。
# sortしてからuniqを実行
sort sample.txt | uniq
apple
banana
orange
今度は期待通りに「apple」と「orange」の重複が消え、ユニークな値だけが抽出されました。
これが、Bash操作における「sort | uniq」が黄金律と呼ばれる理由です。
アルゴリズムの効率性とメモリ管理の視点
なぜuniqコマンドは、自らソート機能を持ったり、ファイル全体をメモリに保持して重複をチェックしたりしないのでしょうか。
その理由は、UNIXの設計哲学である「一つのプログラムには一つのことを完璧にやらせる」という思想に基づいています。
もしuniqがファイル全体の重複を管理しようとすると、巨大なファイルを扱う際に出現したすべての行をメモリ(ハッシュテーブルなど)に保持しなければなりません。
これでは、数テラバイトあるようなログファイルを処理する際にメモリ不足でシステムが停止してしまう恐れがあります。
一方で、現在のuniqの仕様であれば、直前の1行分だけを保持すればよいため、メモリ消費量を極限まで抑えることができます。
データの並び替えという重い処理は専門のツールであるsortに任せ、自分は比較だけに特化するという役割分担がなされているのです。
uniqコマンドの便利なオプション活用例
sortと組み合わせることで、uniqは単なる重複削除以上の力を発揮します。
実務で頻繁に利用される便利なオプションをいくつか紹介しましょう。
出現回数をカウントする(-cオプション)
最も利用頻度が高いのが、各行がいくつ重複していたかを表示する-c(–count)オプションです。
アクセスログの集計などで非常に重宝します。
# 各単語の出現回数を数える
sort sample.txt | uniq -c
2 apple
1 banana
2 orange
このように、左側に出現回数、右側に内容が表示されます。
ここからさらにsort -rn(数値の降順でソート)を繋げることで、「出現回数が多い順のランキング」を簡単に作成できます。
重複している行だけを表示する(-dオプション)
データの中にどのような「被り」があるのかを確認したいときは、-d(–repeated)オプションを使用します。
# 重複がある行のみを出力
sort sample.txt | uniq -d
apple
orange
1回しか出現しなかった「banana」は除外され、2回以上出現した「apple」と「orange」だけが表示されます。
一度しか出現しない行だけを表示する(-uオプション)
逆に、完全にユニークなデータだけを知りたい場合は-u(–unique)オプションが有効です。
# 重複がない行のみを出力
sort sample.txt | uniq -u
banana
重複がある「apple」と「orange」が排除され、孤立したデータのみを抽出できました。
sort -u と uniq の違いとは?
実は、sortコマンド自体にも-uという重複削除オプションが存在します。
sort -uを実行すれば、sort | uniqと全く同じ結果を得ることが可能です。
「それならuniqを個別に使う必要はないのでは?」と思われるかもしれませんが、両者には明確な使い分けの基準があります。
| 機能・特徴 | sort -u | sort | uniq |
|---|---|---|
| シンプルさ | コマンドが短く、記述が容易 | パイプで繋ぐためやや長い |
| 柔軟性 | 重複削除のみ可能 | 重複カウント(-c)や重複抽出(-d)が可能 |
| 処理速度 | 一般的にパイプを通さない分、わずかに高速 | データの転送オーバーヘッドがある |
| 主な用途 | 単に重複を消したリストが欲しい時 | 件数集計や複雑なフィルタリングをしたい時 |
結論として、「ただ重複を消したいだけなら sort -u」を使い、「出現回数を調べたり、特定の重複条件で抽出したいなら sort | uniq」を使うのが正解です。
パフォーマンスを最大化するためのヒント
大規模なデータを扱う場合、sortの処理がボトルネックになることがあります。
Bashでのデータ処理をより高速にするためのTipsをいくつか紹介します。
ロケール設定(LC_ALL=C)の利用
デフォルトのロケール設定では、言語固有の複雑な並び替え規則が適用されるため、ソートが低速になることがあります。
純粋なバイナリデータや英数字のログファイルを扱う場合、環境変数を一時的に変更することで劇的に速度が向上します。
# 高速なソートとユニーク処理
export LC_ALL=C
sort large_log.txt | uniq -c
LC_ALL=Cを指定することで、マルチバイト文字の複雑な比較をスキップし、単純なバイト比較を行うようになります。
これにより、データ量によっては数倍以上の速度差が出ることも珍しくありません。
大規模ファイルでのディスク利用
sortはメモリに収まらないサイズのデータを処理する場合、一時ファイルをディスクに書き出します。
もし/tmpディレクトリの容量が少ない場合や、高速なSSDを別にマウントしている場合は、-Tオプションで一時ファイルの保存先を指定するとエラー回避や高速化に繋がります。
よくある間違い:ソート順の不一致
たまに遭遇するトラブルとして、「sortしたはずなのにuniqが正しく動作しない」というケースがあります。
これは、sortに渡したオプションと、データの性質が合致していない場合に起こります。
例えば、大文字小文字を区別せずに重複削除したい場合、sort側でもその設定をしておかなければなりません。
# 誤った例:sortは区別し、uniqは無視しようとする
cat << EOF > sample2.txt
Apple
apple
EOF
sort sample2.txt | uniq -i
この場合、sortの結果として「Apple」と「apple」が並びますが、ロケールによってはこれらが離れて配置される可能性もゼロではありません。
確実を期すならば、sort -f(大文字小文字を無視してソート)とuniq -iを組み合わせるなどの配慮が必要です。
基本的には、「uniqに渡す直前のデータが、期待通りに連続して並んでいるか」を常に意識するようにしましょう。
まとめ
Bashにおいてuniqとsortを組み合わせて使う理由は、uniqが「隣接する行との比較」しか行わない極めてシンプルな設計になっているからです。
このシンプルさは、メモリ消費を抑え、大規模なストリームデータを効率的に処理するための合理的な選択の結果です。
最後に、今回のポイントを振り返ります。
uniqは直前の行としか比較しないため、事前にsortによる整列が不可欠。sort | uniq -cはログ解析やデータ集計の基本パターン。- 単純な重複削除だけが目的であれば、
sort -uの方が効率的。 - 巨大なファイルを扱う際は
LC_ALL=Cで高速化を検討する。
これらの特性を正しく理解し、状況に応じて最適なコマンドを選択できるようになれば、Bashでのデータ加工がより一層スムーズになるはずです。
「なぜこの組み合わせなのか」という裏側のロジックを忘れずに、日々のコマンド操作に役立ててください。
