はじめに
シェルスクリプトでSQLのJOINを実現する方法について、王道のjoinコマンドと、awkを使った方法をそれぞれ考えてみる。
またSQLのGROUP BYもawkで実現してみる。
前提
今回使用するデータとして、以下の商品マスタと注文トランザクションを用意する。
# products(pid, pname, price)表
$ cat products
p1 pencil 100
p2 pen 200
p3 eraser 150
# orders(oid, pid(FK(products.pid), cnt)表
$ cat orders
o1 p1 3
o1 p2 2
o2 p1 3
o2 p3 1
joinコマンドでJOIN
SQLのJOINと同等の働きをするコマンドがその名の通りjoinコマンドである。
SELECT oid, pid, pname, cnt
FROM products
JOIN orders
USING (pid)
このSQLの結果は次のようになる。
o1 p1 pencil 3
o1 p2 pen 2
o2 p1 pencil 3
o2 p3 eraser 1
joinコマンドでも同等の結果を得ることができるが、注意点として、joinコマンドを使う前に結合キーでソートしなければいけない。
$ sort orders -k2 | join -1 2 -2 1 - products
p1 o1 3 pencil 100
p1 o2 3 pencil 100
p2 o1 2 pen 200
p3 o2 1 eraser 150
ここで使ったコマンドオプションは各ファイルのキーのカラム位置を示すオプション-1
、-2
のみ。
SQLと全く同じように項目並べ替えたければ、awkでやってもいいが、標準の-oオプションがある。
-o FILENUM.FIELD...(コンマまたは空白で区切り)
の書式で指定する。
-o FIELD-LIST...
出力行のフォーマットに FIELD-LIST を用いる。 FIELD-LIST の各要素は、一文字 0 ' または M.N の形式である。 ここで M はファイル番号で 1 ' または 2 'である。 N は正の整数で、フィールドの番号である。
フィールド指定 0 ' は join フィールドを表す。 ほとんどの場合は、 0 ' の機能は M.N を用いて join フィールドを明示的に指定するやり方でも再現可能であろう。 しかし、 (-a や -v オプションを使ったときに) ペアにならなかった行も表示する場合は、両方のファイルに そのような行があると、 FIELD-LIST で M.N を使うやり方では join フィールドを指定することはできない。 join にこの機能を与えるため、 POSIX で 0 ' フィールド指定の記述が発明された。
FIELD-LIST の各要素はコンマまたは空白で区切られる。 一つの -o オプションの後に複数の FIELD-LIST 引数を指定することも出来る。 -o オプション以降の 全てのリストの値は結合される。 FIELD-LIST の指定は、 (-a や -v オプションに由来するものも含め) 全ての出力行に適用される。
-oオプションとawkそれぞれの方法を書いてみるが、awkを使う例ではawkに渡される各列の順番を把握していなければいけないのに対して、-oオプションでは各ファイルの並び順を把握していればいいので、わかりやすく、awkを使うことはないだろう。
# -oオプションで指定
$ sort orders -k2 | join -1 2 -2 1 -o 1.1 1.2 2.2 1.3 - products
o1 p1 pencil 3
o2 p1 pencil 3
o1 p2 pen 2
o2 p3 eraser 1
# awkで指定
$ sort orders -k2 | join -1 2 -2 1 - products | awk '{print $2,$1,$4,$3}'
o1 p1 pencil 3
o2 p1 pencil 3
o1 p2 pen 2
o2 p3 eraser 1
他によくつかうjoinのオプションは次のようなもので、-aをつけると外部結合も表現できるし、-1,-2,-jで結合カラムも指定できる。
-a FILENUM
print unpairable lines coming from file FILENUM, where FILENUM is 1 or 2, corresponding to FILE1 or FILE2-j FIELD
equivalent to ‘-1 FIELD -2 FIELD’-t CHAR
use CHAR as input and output field separator-1 FIELD
join on this FIELD of file 1-2 FIELD
join on this FIELD of file 2
awkでJOIN
joinコマンドを使わずにawkで実現することもできる。ただし全くエレガントではない。awkの練習として書いてみる。
while read oid pid cnt
do
pname=$(awk -v "pid=$pid" '$1==pid {print $2}' products)
echo "$oid $pid $pname $cnt"
done < orders
-vオプションでシェル変数をawk変数に渡して、キー($1)と変数が等しい行のほしい列($2)を取得するというもの。
ほしい列が先のように$2の一つであればそれほど大変ではないが、複数列ほしいとなると、配列を使用することになる。
例えば次のようなSQLと同じ結果を取得する場合だ。
SELECT oid, pid, pname, price, cnt
FROM products
JOIN orders
USING (pid)
結果
o1 p1 pencil 100 3
o1 p2 pen 200 2
o2 p1 pencil 100 3
o2 p3 eraser 150 1
awkとbashの配列を使い、次のように書く。
while read oid pid cnt
do
pinfo=($(awk -v "pid=$pid" '$1==pid {print $2, $3}' products))
echo "$oid $pid ${pinfo[0]} ${pinfo[1]} $cnt"
done < orders
awkで複数列をprintして、その標準出力をpinfoという変数に格納することで、配列として扱う。
JOINと計算式
SELECTした値を掛けたりするような場合、シェルスクリプトでどのように表現するか、awkや「echo + bashでの計算」の例を記載する。
次のSQLを題材とする。
SELECT oid, pid, pname, price * cnt
FROM products
JOIN orders
USING (pid)
結果
o1 p1 pencil 300
o1 p2 pen 400
o2 p1 pencil 300
o2 p3 eraser 150
awkで計算する場合
joinコマンドで結合を行い、結果の出力にawkを使って掛け算を表現する。
sort orders -k2 | join -1 2 -2 1 -o 1.1 1.2 2.2 1.3 2.3 - products |
awk '{print $1,$2,$3,($4*$5)}'
echo + bashでの計算の場合
awkで結合を行い、結果の出力にechoの-nオプションを使い、改行の抑制をして表示を調整する。またbashで計算する。
while read oid pid cnt
do
pinfo=($(awk -v "pid=$pid" '$1==pid {print $2, $3}' products))
echo -n "$oid $pid ${pinfo[0]} "
echo $((${pinfo[1]} * $cnt))
done < orders
awkでGROUP BY
集計をawkで実行するには、awkの連想配列を使う。連想配列のキーにGROUP BY句に入れる値を設定する。SELECT句でSUMを行いたいのであれば、連想配列の値には足し込みをしてあげればよい。
$ cat orders
o1 p1 3
o1 p2 2
o2 p1 3
o2 p3 1
$ awk '{arr[$2]+=$3} END{for(i in arr) print i, arr[i]}' orders
p1 6
p2 2
p3 1
今回は前段でJOIN結合を取り上げているので、JOIN + GROUP BYのSQLと同じ結果をシェルスクリプトで実現してみたい。
SELECT oid, sum(price * cnt)
FROM products
JOIN orders
USING (pid)
GROUP BY oid
結果
o1 700
o2 450
GROUP BYする前のJOINだけの構文であれば、先にjoinコマンドで実現していた。今回のSQLに合うように射影を最小限に減らして再掲する。
$ sort orders -k2 | join -1 2 -2 1 -o 1.1 1.3 2.3 - products |
awk '{print $1,($2*$3)}'
o1 300
o2 300
o1 400
o2 150
このコマンド結果、非常にシンプルになったので、はじめに解説したawkの集計方法を使う。
$ sort orders -k2 | join -1 2 -2 1 -o 1.1 1.3 2.3 - products |
awk '{print $1,($2*$3)}' |
awk '{arr[$1]+=$NF} END{for(i in arr) print i, arr[i]}'
o1 700
o2 450