みなさんこんにちは!ヒロポンです!
PowerShell で Get-なんとか の結果を | でつないで、絞ったり加工したり。動くには動くんです。でも、なんか遅い。
あるいは、絞ったつもりが意図通りに減ってない。
「あれ、この | って結局何やってるん??」ってなったこと、ないですか?
パイプラインを「なんとなく」で書いてると、だいたいここでつまずきます。
今回は PowerShell パイプライン Where-Object / Select-Object / ForEach-Object の使い分けを、パイプラインの仕組みから積み上げて見ていきます。この3つの役割がハッキリ分かれると、「1行でスッと書ける処理」がぐっと増えますよ!!
忙しい人向けに最初にまとめ
- パイプライン
|は、左のコマンドレットの出力を1件ずつ右へ流す仕組み。 - Where-Object = 条件で行を絞るフィルタ。
- Select-Object = 取り出す列(プロパティ)を選ぶ・先頭 N 件に絞る。
- ForEach-Object = 流れてくる各要素に処理をする(1件ずつ)。
- 詰まりどころは、
-Filterを持つコマンドレットなら Where-Object より -Filter が速い(取得元で絞るから)ことと、ForEach-Object と foreach 文のメモリの使い方が違うこと。
そもそもパイプラインは何をしているのか
まず仕組みから。
PowerShell のパイプラインとは、あるコマンドレットの出力(オブジェクト)を、次のコマンドレットの入力として
|で渡していく流れのことです。
ポイントは、渡っていくのが文字列ではなくオブジェクトだということ。
だから次のコマンドレットで .Name や .Status みたいにプロパティで扱える。ここが他のシェルと違う、PowerShell の強みなんですよね。
もう1つ大事なのが、基本的に1件ずつ流れること。全件そろうのを待ってから次に渡すわけじゃない。

左から右へ、オブジェクトが1件ずつ流れていく。
途中の Where-Object で条件に合わないものは落ちて、通ったものだけが次へ進む。この絵が頭にあると、あとの使い分けがスッと入ります。
では、真ん中の3つを1つずつ見ていきます。
Where-Object — 条件で行を絞る
Where-Object は、流れてくるオブジェクトのうち条件に合うものだけを通すフィルタです。$_(または $PSItem)が「今流れてる1件」を指します。
# 1〜10 のうち 5 より大きいものだけ
1..10 | Where-Object { $_ -gt 5 }
# → 6, 7, 8, 9, 10
実務ではオブジェクトのプロパティで絞るのが普通です。
# Status が Running のサービスだけ
$services | Where-Object { $_.Status -eq "Running" }
比較演算子は -eq(等しい)、-ne(等しくない)、-gt / -lt(大小)、-like(ワイルドカード)あたりを押さえておけば大半は書けます。
ここまでで、「Where-Object は行を減らすやつ」と覚えておけばOKです。
Select-Object — 列を選ぶ・先頭 N 件に絞る
Select-Object は役割が違います。取り出すプロパティ(列)を選ぶか、件数を絞るのが仕事です。
# Name と Id の列だけ取り出す
Get-Process | Select-Object Name, Id
# 先頭 3 件だけ
1..10 | Select-Object -First 3
# → 1, 2, 3
値だけを配列で取り出したいときは -ExpandProperty がいい感じに効きます。
# サービス名だけを文字列の配列で取り出す
$services | Select-Object -ExpandProperty Name
Where-Object が「行を減らす」なら、Select-Object は「列を選ぶ・件数を整える」。
ここが混ざりやすいので、役割で覚えるのがコツです。
ForEach-Object — 流れてくる各要素に処理をする
ForEach-Object は、パイプラインを流れてくる1件ずつに対して処理を実行します。ここでも $_ が「今の1件」。こんな感じで、流れてくる要素を順にさばいていくイメージです。
# 各要素を 2 倍する
1..3 | ForEach-Object { $_ * 2 }
# → 2, 4, 6
# 各サービスに対して再起動をかける、みたいな処理
$services | ForEach-Object { Restart-Service -Name $_.Name }
ここで「あれ、foreach 文でもよくない??」って思いますよね。
実はここに、業務SEが地味に詰まるポイントがあります。次で開示します。
ここまでで、Where で絞って、Select で列を選んで、ForEach-Object で1件ずつ処理する、という流れが見えてきた。3つを役割で並べると、こうなります。

最初に詰まるポイント
パイプラインは、この詰まりどころを知ってるかどうかで書き味がだいぶ変わります。
ハマりポイント①: -Filter を持つコマンドレットなら、Where-Object より -Filter が速い。
Where-Object は、いったん全件を取得してから条件に合わないものを落とします。件数が多いと、取得の時点でメモリと時間を食う。
一方、Get-ChildItem や Get-ADUser のように -Filter パラメータを持つコマンドレットなら、そっちを使うほうが速い。
理由は、-Filter は取得元(ファイルシステムのプロバイダや AD サーバー)側で絞り込んでから返すから。パイプラインに乗る件数がそもそも減るわけです。
# 遅め: 全ファイルを取ってから .log だけに絞る
Get-ChildItem C:\logs | Where-Object { $_.Name -like "*.log" }
# 速め: プロバイダ側で .log だけに絞って返す
Get-ChildItem C:\logs -Filter *.log
注意点として、-Filter はすべてのコマンドレットにあるわけじゃないです。
持ってるコマンドレット(Get-ChildItem, Get-ADUser など)で、かつ大量件数を扱うときに効く、という理解でOK。持ってないコマンドレットでは素直に Where-Object を使います。
ハマりポイント②: ForEach-Object と foreach 文はメモリの使い方が違う。
同じ「繰り返し」でも、この2つは中身が違います。
# ForEach-Object: パイプラインで1件ずつ流しながら処理(ストリーム)
Get-Content big.log | ForEach-Object { if ($_ -match "ERROR") { $_ } }
# foreach 文: 対象を先に全部メモリに読み込んでからループ
$lines = Get-Content big.log # ← ここで全行がメモリに載る
foreach ($line in $lines) { if ($line -match "ERROR") { $line } }
ForEach-Object(パイプライン版)は1件ずつ流れてくるものを順に処理するので、巨大ファイルでもメモリを一気に食いません。
対して foreach 文は、対象を先に全部変数に入れてから回すので、Get-Content で数百万行を丸ごと読むとメモリを圧迫します。
正直、私も最初は「どっちも同じ繰り返しやろ」と思って foreach で大きいログを全部読み込み、メモリがカツカツになって処理が引っかかったことがあります。パイプラインでそのまま流せばよかった、というオチ。
ここは1回踏むと体で覚えます。
なお、小さいデータなら foreach 文のほうが速いことも多いです(パイプラインのオーバーヘッドがない分)。
大量データはパイプライン、小さいデータは foreach 文、くらいの使い分けで実務は困りません。
私の現場ではこう使ってる
1人でサーバー運用の自動化スクリプトを書いてると、この3つはほぼ毎回セットで出てきます。
たとえばログ監視。「イベントログから、直近1時間のエラーだけ、必要な列だけ抜いて、1件ずつメール本文に整形する」。
これ、日本語で書くとそのままパイプラインになるんですよね。取得して → Where-Object で絞って → Select-Object で列を選んで → ForEach-Object で整形する。
最初はこれを、取得して変数に入れて、foreach で回して、if で分岐して……と長々書いてました。動くけど読みにくいし、直すのがしんどい。
パイプラインで役割ごとにつなぐ書き方に変えたら、1行〜数行でいい感じに収まって、後から見ても何やってるか一目で分かる。この「読み返せる」感じが、1人運用だと地味に効きます!!
まとめ
PowerShell のパイプラインは、| でオブジェクトを1件ずつ流す仕組み。真ん中で使う3つは、役割で覚えるのが近道でした。
- Where-Object: 条件で行を絞る。
- Select-Object: 列を選ぶ・先頭 N 件に絞る。
- ForEach-Object: 流れてくる各要素に処理する。
そして、-Filter を持つコマンドレットなら取得元で絞れるので Where-Object より速い、大量データは ForEach-Object(パイプライン)でメモリを節約。この2つを知っておくと、書き味と性能が一段上がります。
「なんとなく | でつなぐ」から、「役割で | を選ぶ」へ。
ここが切り替わると、今まで数十行だった処理が数行で書けるようになる。自動化スクリプトを書くのが、ちょっと楽しくなりますよ。
よくある質問
$_ と $PSItem は同じものですか?
同じです。$_ はパイプラインで今流れている1件を指す自動変数で、$PSItem はその別名(読みやすくした書き方)です。どちらを使っても動きます。スクリプトでは $PSItem のほうが意味が伝わりやすい、という人もいます。
Where-Object の { } の中で複数条件を書けますか?
書けます。-and と -or でつなぎます。例: Where-Object { $_.Status -eq "Running" -and $_.Name -like "Sql*" }。条件が複雑になってきたら、いったん変数に入れて段階的に絞ると読みやすいです。
Select-Object -First と -Last の違いは?
-First N は先頭から N 件、-Last N は末尾から N 件を取り出します。-Last はパイプラインの全件がそろうまで待つ必要があるので、巨大なデータで末尾だけ欲しいときは、そもそもの取得や並び順を工夫したほうが速いことがあります。
パイプラインの途中経過を確認するには?
途中に ForEach-Object { Write-Host $_; $_ } を挟むと、流れているものを表示しつつそのまま次へ流せます。あるいは、いったん Select-Object -First 5 で先頭だけ見て、意図通り絞れているか確認してから本番のスクリプトに組み込む、という進め方が安全です。
次に読むべき記事




以上!
執筆者
バイブス父さん — 業務 SE 7 年 (SIer 正社員 2 / フリーランス 5)。 現職は SEO 直轄部の AI アドバイザー兼 PL、 副業で中小 SIer の CTO。 SIer の正社員からフリーランスに転じ、 複数のエージェント経由で案件を回してきた経験ベースで「業務 SE 視点」 の技術 + キャリア記事を書いています。
🐦 X: @hiro_progra0524 (日々の現場メモ更新中)
📝 About Me で経歴詳細を見る



