JOIN

NR == FNR 는 입력으로 두 개의 파일을 다룰 때 간단히 사용되는 방법입니다. FNR 값은 입력 파일이 변경될 때마다 0 으로 reset 되어 다시 시작하고 NR 값은 계속해서 누적되므로 결과적으로 NR == FNR 가 되는 경우는 처음 파일에만 해당됩니다. 따라서 NR == FNR { a[$0]; next } 는 첫 번째 파일을 처리하기 위한 부분으로 레코드 값을 index 로 하는 array 를 생성합니다. 뒤에 next 가 있으므로 이후의 코드는 실행되지 않고 계속해서 다음 레코드로 이동하겠죠. $0 in aa 라는 array 에 index 값으로 $0 가 있는지 조회하는 방법이므로 결국 file1, file2 두 파일의 공통 라인에 대해서만 참이 되게 됩니다.

# 라인 111, 222, 333 은 file1, file2 공통
$ cat file1      $ cat file2
AAA              XXX
111              111
222              222
BBB              333
CCC              YYY
333              ZZZ
....................................................

# file1 와 file2 공통 라인 프린트 (intersection)
$ awk 'NR == FNR{ a[$1]; next } $1 in a' file1 file2
111
222
333

# file1 에만 있는 라인 프린트 (file2 file1 순서 주의)
$ awk 'NR == FNR{ a[$1]; next } !($1 in a)' file2 file1
AAA
BBB
CCC

# file2 에만 있는 라인 프린트 (file1 file2 순서 주의)
$ awk 'NR == FNR{ a[$1]; next } !($1 in a)' file1 file2
XXX
YYY
ZZZ

JOIN 은 data 파일과 map 파일에 공통적으로 존재하는 키값을 이용해 일종의 치환 작업을 합니다. map 파일에 존재하는 키값에 한에서 data 파일에서 키가 사용되면 상관없는데 그렇지 않고 map 파일에 존재하지 않는 키가 data 파일에서 사용되면 여기서 LEFT JOIN 과 INNER JOIN 의 차이가 생깁니다. LEFT JOIN 은 map 파일에 해당 키값이 없을 경우 해당 컬럼 값을 공백으로 두고 나머지 정보들을 표시하는데 반해 INNER JOIN 은 아무런 정보도 표시하지 않습니다.

$ cat data
20081010 123 xxx
20081011 234 def
20081012 512 xyz
20081013 512 abc
20081014 717 def
20081015 999 zzz   <--- map 파일에는 없는 키

$ cat map
abc withdrawal
def payment
xyz deposit
xxx balance
........................................

# SELECT d.$1, d.$2, m.$2 FROM data d
#    LEFT JOIN map m ON d.$3 = m.$1
$ awk 'NR == FNR{ a[$1]=$2; next } { print $1,$2,a[$3] }' map data
20081010 123 balance
20081011 234 payment
20081012 512 deposit
20081013 512 withdrawal
20081014 717 payment
20081015 999          <---- LEFT JOIN 결과

# SELECT d.$1, d.$2, m.$2 FROM data d
#    INNER JOIN map m ON d.$3 = m.$1
$ awk 'NR == FNR{ a[$1]=$2; next }
    $3 in a { print $1,$2,a[$3] }        # INNER JOIN 을 위한 키값 체크
' map data
20081010 123 balance
20081011 234 payment
20081012 512 deposit
20081013 512 withdrawal
20081014 717 payment

앞서 사용한 data, map 파일에 이어 map2 파일을 추가하여 두 개의 파일을 JOIN 합니다. 먼저 map, map2 두 개의 파일을 array 로 읽어들여야 하는데 NR == FNR 는 첫 번째 파일에만 적용되므로 여기서는 사용할 수 없고 BEGIN 블록에서 getline 을 이용해 직접 읽어들입니다.

$ cat map2
123 spring
234 summer
512 autumn
717 winter
...............................................

# SELECT d.$1, m2.$2, m.$2 FROM data d
#    LEFT JOIN map  m  ON d.$3 = m.$1
#    LEFT JOIN map2 m2 ON d.$2 = m2.$1
$ awk 'BEGIN {
    while (getline < ARGV[1] > 0) m[$1]  = $2
    while (getline < ARGV[2] > 0) m2[$1] = $2
    while (getline < ARGV[3] > 0) {
        print $1,m2[$2],m[$3]
    }
}' map map2 data

20081010 spring balance
20081011 summer payment
20081012 autumn deposit
20081013 autumn withdrawal
20081014 winter payment
20081015                <---- LEFT JOIN 결과

# SELECT d.$1, m2.$2, m.$2 FROM data d
#    INNER JOIN map  m  ON d.$3 = m.$1
#    INNER JOIN map2 m2 ON d.$2 = m2.$1
$ awk 'BEGIN {
    while (getline < ARGV[1] > 0) m[$1]  = $2
    while (getline < ARGV[2] > 0) m2[$1] = $2
    while (getline < ARGV[3] > 0) {
       if (( $2 in m2 ) && ( $3 in m )) {        # INNER JOIN 을 위한 키값 체크 
           print $1,m2[$2],m[$3]
       }
    }
}' map map2 data

20081010 spring balance
20081011 summer payment
20081012 autumn deposit
20081013 autumn withdrawal
20081014 winter payment

GROUP BY 와 JOIN 을 함께 사용

# 사용할땐 데이터 이외의 헤더 부분은 삭제해야 합니다.
$ cat Shippers
ShipperID     ShipperName         Phone
----------+-----------------+---------------
1     ,    Speedy Express   , (503) 555-9831
2     ,    United Package   , (503) 555-3199
3     ,    Federal Shipping , (503) 555-9931

$ cat Orders
OrderID  CustomerID  EmployeeID  OrderDate  ShipperID
--------+-----------+----------+-----------+----------
10248        90          5       1996-07-04     3
10249        81          6       1996-07-05     1
10250        34          4       1996-07-08     2
10251        84          3       1996-07-08     1
10252        76          4       1996-07-09     2
10253        34          3       1996-07-10     2
10254        14          5       1996-07-11     2

#####################  SQL 문  #######################

SELECT S.ShipperName, COUNT(O.OrderID) AS NumberOfOrders
FROM Orders O
LEFT JOIN Shippers S ON O.ShipperID = S.ShipperID
GROUP BY ShipperName
ORDER BY NumberOfOrders

 ShipperName       NumberOfOrders
----------------  --------------
Federal Shipping  1             
Speedy Express    2             
United Package    4    

######################  AWK  ##########################

# $1 : Shippers.ShipperID
# $2 : Shippers.ShipperName
# $5 : Orders.ShipperID
$ awk '
NR == FNR{ s[$1]=$2; next } 
{ o[$5]++ } 
END {
    PROCINFO["sorted_in"] = "@val_num_asc"
    for ( i in o ) { printf "%-20s %d\n", s[i], o[i] }
}
' FS=' *, *' Shippers FS=' ' Orders

Federal Shipping     1
Speedy Express       2
United Package       4

CROSS JOIN

CROSS JOIN 은 Cartesian product 라고도 하는데 두 집합 A, B에 대해 a∈A이고 b∈B인 모든 순서쌍(a, b)의 집합을 말합니다. 그러니까 만약에 테이블 A 에 10 개의 row 가 있고 테이블 B 에 10 개의 row 가 있으면 CROSS JOIN 을 하게 되면 모두 10 X 10 = 100 개의 row 가 생기게 됩니다. joining everything to everything 이기 때문에 따로 ON 절이 없고 만약에 테이블 하나가 empty 면 result set 은 0 이 됩니다.

이것이 어디에 필요한지 예제를 통해 알아보면 먼저 아래와 같은 테이블이 있을 경우 "각 Store 에서 판매된 Product 별로 Amount 를 SUM 해서 보여달라" 라고 하면 간단히 GROUP BY 를 하면 됩니다.

$ cat sales                        $ cat stores       $ cat products 

Store    Product   Amount          Store              Product        
-------+---------+-------          -----              -------        
lg24     snack     300             lg24               milk           
lg24     snack     200             du                 bread          
lg24     soda      150             7ele               soda           
lg24     soda      250                                snack          
du       milk      150
du       bread     100
du       bread     200
7ele     snack     100
7ele     milk      150
7ele     bread     200
7ele     bread     150
.......................................................................

SELECT Store, Product, SUM(Amount) as Sum
FROM sales
GROUP BY Store, Product
.........................................

$ awk '
{ a[$1,$2] += $3 }
END { 
    PROCINFO["sorted_in"] = "@ind_str_asc"
    for ( i in a ) { 
        split(i, b, SUBSEP) 
        printf "%-10s %-10s %5d\n", b[1], b[2], a[i] 
    }
}' sales

7ele       bread        350
7ele       milk         150
7ele       snack        100
du         bread        300
du         milk         150
lg24       snack        500
lg24       soda         400

그런데 "어떤 상점에서 어떤 품목이 판매가 안됐는지 알 수 없으니 그런 품목에 대해서는 0 으로 표시해달라" 라고 할 경우엔 모든 상점에서 모든 품목을 표시해야 하므로 이때는 CROSS JOIN 을 해야 합니다.

SELECT S.Store, P.Product
FROM stores S
CROSS JOIN products P
..........................

SELECT S.Store, P.Product, ISNULL(C.Sum,0) as Sum
FROM stores S
CROSS JOIN products P
LEFT JOIN
    (SELECT Store, Product, SUM(Amount) as Sum
     FROM sales 
     GROUP BY Store, Product) C
ON
  S.Store = C.Store AND
  P.Product = C.Product
................................................................

$ awk 'BEGIN {
    while (getline < ARGV[1] > 0) s[$1]
    while (getline < ARGV[2] > 0) p[$1]
    while (getline < ARGV[3] > 0) a[$1,$2] += $3 

    PROCINFO["sorted_in"] = "@ind_str_asc"
    for ( i in s )  {                         # CROSS JOIN 을 위한 이중 for 문
        for ( j in p ) { 
            printf "%-10s %-10s %5d\n", i, j, ( a[i,j] ? a[i,j] : 0 )
        }
    }
}' stores products sales

7ele       bread        350
7ele       milk         150
7ele       snack        100
7ele       soda           0
du         bread        300
du         milk         150
du         snack          0
du         soda           0
lg24       bread          0
lg24       milk           0
lg24       snack        500
lg24       soda         400

FULL JOIN (exclusive)

다음은 file1, file2 의 공통부분을 제외하고 각 파일에만 존재하는 데이터를 출력합니다.

SELECT *
FROM file1 f1 FULL JOIN file f2
ON f1.value = f2.value
WHERE f1.value IS NULL OR f2.value IS NULL
..........................................

$ cat file1      $ cat file2
AAA              XXX
111              111
222              222
BBB              333
CCC              YYY
333
............................

$ awk 'BEGIN { 
    while (getline < ARGV[1] > 0) a[$1]=$1
    while (getline < ARGV[2] > 0) b[$1]=$1
    for (i in a) if ( !b[i] ) print i
    for (i in b) if ( !a[i] ) print i
}' file1 file2
AAA
CCC
BBB
XXX
YYY

다음은 FULL JOIN 을 이용해 local directory 와 remote directory 를 비교하는 예입니다. remote 파일을 비교하는 것이므로 파일을 다운로드하지 않고 md5sum 을 이용해 비교합니다. local 과 remote 파일의 md5sum 값이 다르다는 것은 파일이 수정되었다는 의미고 아래 libtlpi.a 파일은 remote 에만 존재하는 파일이 됩니다.

이 방법은 md5sum 값만을 이용해 비교하는 것으로 동일한 내용의 파일이 있을 경우 ( 예를 들면 cp 에 의해 생성된 ) 는 비교되지 않습니다. 또한 hash collision 이 있을 수 있습니다. ( md5sum 을 sha1sum 이나 이상으로 변경해보세요 )

$ awk 'BEGIN { 
    while (getline < ARGV[1] > 0) a[$1]=$0
    while (getline < ARGV[2] > 0) b[$1]=$0
    for (i in a) if ( !b[i] ) print a[i]
    for (i in b) if ( !a[i] ) print b[i]
}' <( find BUILD -type f -exec md5sum {} + ) \
<( ssh user@remote 'find path/to/remote/BUILD -type f -exec md5sum {} +' ) 

aeabb93eddf837d8dd45cbf94d0ef7d2  BUILD/pty_master_open.d
e507a57525fabe57695683442ecde7e3  BUILD/pty_fork.o
a0cc9039df2eab09f2b145e54070a081  path/to/remote/BUILD/libtlpi.a   # remote 에만 존재하는 파일
a123503825f130377d49e594e3f73504  path/to/remote/BUILD/pty_fork.o
bc2a1a57bae6472a1bb1655bd404244b  path/to/remote/BUILD/pty_master_open.d

FPAT 을 이용한 JOIN

datafile
------------------------------------------------------------------------------
{{AA1}}'s design will serve as the basis for the planned Mars {{BB1}} rover. In December {{BB2}}, {{AA1}}'s two-year mission was extended indefinitely. As of {{AA2}} 26, 2017, {{AA1}} has been on Mars for 1738 sols (1785 total days) since landing on August 6, . . . . . . .
mapfile1
------------------------------------
{{AA1}} = Curiosity
{{AA2}} = June
mapfile2
------------------------------------
{{BB1}} = 2020
{{BB2}} = 2012

세 개의 파일을 처리하는 경우인데 이전에 BEGIN 블록을 이용하는 방법과는 다르게 BEGINFILE 블록을 사용했습니다.

$ awk -f - <<\EOF mapfile1 mapfile2 RS='.' FPAT='{{[A-Z0-9]+}}' datafile
BEGINFILE { 
    if ( ARGIND < 3 ) {          # 1, 2 번째 파일일 경우만 실행
        while (getline < FILENAME > 0) a[$1] = $3
        nextfile
    }
}{
    r = $0                       # sub 함수의 실행결과로 $0 변수에 대입연산이 
    for (i=1; i<=NF; i++) {      # 일어나면 필드 관련 변수들이 재설정 되므로
        sub($i, a[$i], r)        # r 변수에 대입해 사용합니다.
    } 
    printf "%s", r RT
}
EOF
Curiosity's design will serve as the basis for the planned Mars 2020 rover. In
^^^^^^^^^                                                       ^^^^
December 2012, Curiosity's two-year mission was extended indefinitely. As of
         ^^^^  ^^^^^^^^^
June 26, 2017, Curiosity has been on Mars for 1738 sols (1785 total days)
^^^^           ^^^^^^^^^
since landing on August 6, .......