Hive 튜토리얼(Tutorial)

Hive 튜토리얼(Tutorial)

Hive는 Apache Hadoop 기반의 데이터 웨어하우징 인프라예요. 대용량 데이터에 대한 손쉬운 요약(summarization)과 ad-hoc 쿼리, 분석을 위해 설계됐습니다. 데이터 타입/테이블/파티션 개념을 먼저 배우고, 실제 예시로 Hive의 기능을 익혀 봐요.

출처: 문서

본문

개념(Concepts)

Hive란 무엇인가(What Is Hive)

Hive는 Apache Hadoop 기반의 데이터 웨어하우징 인프라예요. Hadoop은 상용 하드웨어에서 데이터 저장과 처리를 위한 대규모 scale out과 내결함성(fault tolerance)을 제공합니다.

Hive는 대용량 데이터의 손쉬운 요약, ad-hoc 쿼리, 분석을 가능하게 하도록 설계됐어요. 사용자가 ad-hoc 쿼리, 요약, 데이터 분석을 쉽게 하게 해 주는 SQL을 제공합니다. 동시에 Hive의 SQL은 사용자가 자신의 기능을 통합해 커스텀 분석을 할 수 있는 여러 곳을 제공하는데, 예를 들어 사용자 정의 함수(UDF)가 있습니다.

Hive가 아닌 것(What Hive Is NOT)

Hive는 온라인 트랜잭션 처리(OLTP)를 위해 설계되지 않았어요. 전통적인 데이터 웨어하우징 작업에 가장 잘 쓰입니다.

시작하기(Getting Started)

Hive, HiveServer2, Beeline 설정에 대한 자세한 내용은 GettingStarted 가이드를 참고하세요. Book about Hive에는 시작에 도움이 될 만한 책들이 나열되어 있어요.

데이터 단위(Data Units)

세분화(granularity) 순서대로 Hive 데이터는 다음과 같이 구성됩니다:

  • 데이터베이스(Databases): 테이블, 뷰, 파티션, 컬럼 등의 이름 충돌을 피하기 위한 네임스페이스 기능을 해요. 데이터베이스는 사용자나 사용자 그룹에 대한 보안을 강제하는 데도 쓸 수 있습니다.
  • 테이블(Tables): 같은 스키마를 가진 동질의 데이터 단위예요. 예를 들어 page_views 테이블은 각 행이 다음 컬럼(스키마)으로 구성될 수 있어요:
    • timestamp — INT 타입으로, 페이지가 뷰된 UNIX 타임스탬프
    • userid — BIGINT 타입으로, 페이지를 본 사용자를 식별
    • page_url — STRING 타입으로, 페이지의 위치
    • referer_url — STRING 타입으로, 사용자가 현재 페이지에 오기 전의 페이지 위치
    • IP — STRING 타입으로, 페이지 요청이 이루어진 IP 주소
  • 파티션(Partitions): 각 테이블은 데이터가 어떻게 저장되는지 결정하는 하나 이상의 파티션 키를 가질 수 있어요. 파티션은 저장 단위일 뿐 아니라 지정된 기준을 만족하는 행을 효율적으로 식별하게 해 줍니다. 예를 들어 STRING 타입의 date_partition과 STRING 타입의 country_partition이 있어요. 파티션 키의 각 고유 값이 테이블의 파티션을 정의합니다. 예를 들어 "2009-12-23"의 모든 "US" 데이터는 page_views 테이블의 한 파티션입니다. 따라서 2009-12-23의 "US" 데이터만 분석한다면 테이블의 관련 파티션에만 쿼리를 실행할 수 있어 분석을 크게 빨라지게 합니다. 단, 파티션 이름이 2009-12-23이라고 해서 그 날짜의 데이터를 전부 또는 일부만 담는다는 뜻은 아니에요. 파티션은 편의상 날짜로 이름 지을 뿐이며, 파티션 이름과 데이터 내용의 관계를 보장하는 것은 사용자의 몫입니다! 파티션 컬럼은 가상 컬럼으로, 데이터 자체의 일부가 아니라 로드(load) 시 파생됩니다.
  • 버킷(Buckets, 또는 Clusters): 각 파티션의 데이터는 테이블의 어떤 컬럼의 해시 함수 값에 기반해 다시 버킷으로 나뉠 수 있어요. 예를 들어 page_views 테이블은 파티션 컬럼이 아닌 컬럼 중 하나인 userid로 버킷화될 수 있습니다. 이는 데이터를 효율적으로 샘플링하는 데 쓰입니다.

테이블이 파티셔닝되거나 버킷화될 필요는 없지만, 이런 추상화는 쿼리 처리 중 대량의 데이터를 프루닝(pruning)할 수 있게 해 더 빠른 쿼리 실행으로 이어집니다.

타입 시스템(Type System)

Hive는 아래 설명하는 원시(primitive) 및 복합(complex) 데이터 타입을 지원해요. 자세한 내용은 Hive Data Types 참고.

원시 타입(Primitive Types)
  • 타입은 테이블의 컬럼과 연결됩니다. 지원되는 원시 타입:
    • 정수(Integer): TINYINT(1바이트), SMALLINT(2바이트), INT(4바이트), BIGINT(8바이트)
    • Boolean: BOOLEAN — TRUE/FALSE
    • 부동소수점: FLOAT(단정밀도), DOUBLE(배정밀도)
    • 고정소수점: DECIMAL — 사용자 정의 scale과 precision의 고정소수점 값
    • 문자열: STRING(지정 문자셋의 문자 시퀀스), VARCHAR(최대 길이가 있는 문자 시퀀스), CHAR(정의된 길이의 문자 시퀀스)
    • 날짜·시간: TIMESTAMP(타임존 없는 날짜와 시간, "LocalDateTime" 의미), TIMESTAMP WITH LOCAL TIME ZONE(나노초까지 측정되는 시점, "Instant" 의미), DATE(날짜)
    • 바이너리: BINARY(바이트 시퀀스)

타입은 다음 계층(부모는 모든 자식 인스턴스의 슈퍼타입)으로 구성됩니다:

  • Type
    • Primitive Type
      • Number
        • DOUBLE → FLOAT → BIGINT → INT → SMALLINT → TINYINT
      • STRING
      • BOOLEAN

이 타입 계층은 쿼리 언어에서 타입이 어떻게 암묵적으로 변환되는지를 정의해요. 자식에서 조상으로의 암묵적 변환이 허용됩니다. 쿼리 식이 type1을 기대하는데 데이터가 type2라면, type1이 type 계층에서 type2의 조상이면 type2가 암묵적으로 type1로 변환됩니다. 이 계층은 STRING의 DOUBLE로의 암묵적 변환을 허용합니다.

명시적 타입 변환은 아래 #Built In Functions 섹션에 나온 cast 연산자로 할 수 있어요.

복합 타입(Complex Types)

복합 타입은 원시 타입과 다른 복합 타입들로부터 만들 수 있어요:

  • Structs: 타입 안의 요소는 DOT(.) 표기로 접근합니다. 예를 들어 STRUCT {a INT; b INT} 타입의 컬럼 c에서 a 필드는 c.a 식으로 접근합니다.
  • Maps(키-값 튜플): ['element name'] 표기로 요소에 접근합니다. 예를 들어 'group' -> gid 매핑으로 구성된 map M에서 gid 값은 M['group']으로 접근합니다.
  • Arrays(인덱스 가능한 리스트): 배열의 요소는 같은 타입이어야 해요. [n] 표기(n은 0부터 시작하는 인덱스)로 요소에 접근합니다. 예를 들어 ['a','b','c'] 요소의 배열 A에서 A[1]은 'b'를 반환합니다.

원시 타입과 복합 타입 생성 구조로 임의 깊이의 중첩 타입을 만들 수 있어요. 예를 들어 User 타입은 gender(STRING), active(BOOLEAN) 필드로 구성될 수 있습니다.

타임스탬프(Timestamp)

타임스탬프는 많은 혼란의 원인이 되어 왔기 때문에 Hive의 의도된 의미를 문서화해 둡니다.

Timestamp("LocalDateTime" 의미)

Java의 "LocalDateTime" 타임스탬프는 타임존 없이 년, 월, 일, 시, 분, 초로 날짜와 시간을 기록해요. 이 타임스탬프는 로컬 타임존과 무관하게 항상 같은 값을 가집니다. 예를 들어 "2014-12-12 12:34:56"은 년/월/일/시/분/초 필드로 분해되지만 타임존 정보는 없습니다. 특정 순간(instant)에 대응하지 않아요. 대부분의 애플리케이션에서는 timestamp보다 timestamp with local time zone을 크게 권장합니다.

Timestamp with local time zone("Instant" 의미)

Java의 "Instant" 타임스탬프는 데이터가 어디서 읽히든 일정하게 유지되는 시점을 정의해요. 그래서 타임스탬프는 로컬 타임존으로 보정되어 원래 시점과 일치합니다.

Type Value in America/Los_Angeles Value in America/New_York
timestamp 2014-12-12 12:34:56 2014-12-12 12:34:56
timestamp with local time zone 2014-12-12 12:34:56 2014-12-12 15:34:56

같은 순간을 나타내는데 타임존에 따라 표시값이 달라지는 걸 확인할 수 있어요.

내장 연산자와 함수(Built In Operators and Functions)

아래 나열된 연산자와 함수는 최신이 아닐 수 있어요. (Hive Operators and UDFs가 더 최신 정보입니다.) Beeline이나 Hive CLI에서 다음 명령으로 최신 문서를 확인하세요:

SHOW FUNCTIONS;
DESCRIBE FUNCTION <function_name>;
DESCRIBE FUNCTION EXTENDED <function_name>;

대소문자 비구분(Case-insensitive) — 모든 Hive 키워드는 대소문자를 구분하지 않습니다.

내장 연산자(Built In Operators)

관계 연산자(Relational Operators) — 피연산자를 비교하고 연산자 간 비교가 성립하는지에 따라 TRUE/FALSE 값을 생성:

Relational Operator Operand types Description
A = B all primitive types A가 B와 동등하면 TRUE, 아니면 FALSE
A != B all primitive types A가 B와 동등하지 않으면 TRUE, 아니면 FALSE
A < B all primitive types A가 B보다 작으면 TRUE
A <= B all primitive types A가 B보다 작거나 같으면 TRUE
A > B all primitive types A가 B보다 크면 TRUE
A >= B all primitive types A가 B보다 크거나 같으면 TRUE
A IS NULL all types A가 NULL이면 TRUE, 아니면 FALSE
A IS NOT NULL all types A가 NULL이면 FALSE, 아니면 TRUE
A LIKE B strings 문자열 A가 SQL 단순 정규식 B와 일치하면 TRUE. 문자 단위 비교. B의 _는 A의 임의 문자 하나와 일치(정규식 .와 유사), %는 A의 임의 개수 문자와 일치(.*와 유사). 예: 'foobar' LIKE 'foo'는 FALSE, 'foobar' LIKE 'foo___'는 TRUE, 'foobar' LIKE 'foo%'도 TRUE. %를 이스케이프하려면 \ 사용(\%는 % 문자 하나와 일치). 데이터에 세미콜론이 있고 검색하고 싶다면 이스케이프 필요, columnValue LIKE 'a\\;b'
A RLIKE B strings A나 B가 NULL이면 NULL, A의 (빈 부분 문자열 포함) 어떤 부분 문자열이 Java 정규식 B와 일치하면 TRUE. 예: 'foobar' rlike 'foo' TRUE, 'foobar' rlike '^f.*r$'도 TRUE
A REGEXP B strings RLIKE와 동일

산술 연산자(Arithmetic Operators) — 피연산자에 공통 산술 연산을 지원. 모두 숫자 타입 반환:

Arithmetic Operators Operand types Description
A + B all number types A+B의 결과. 결과 타입은 피연산자 타입들의 공통 부모. float와 int의 + 결과는 float
A - B all number types B를 A에서 뺀 결과
A * B all number types A*B 결과. 오버플로우 시 피연산자 하나를 타입 계층에서 더 높은 타입으로 캐스트
A / B all number types A를 B로 나눈 결과. 피연산자가 정수면 나눗셈의 몫
A % B all number types A를 B로 나눈 나머지
A & B all number types A와 B의 비트 AND
A | B all number types 비트 OR
A ^ B all number types A와 B의 비트 XOR
~A all number types A의 비트 NOT

논리 연산자(Logical Operators) — 논리 식 생성 지원. 피연산자의 boolean 값에 따라 boolean TRUE/FALSE 반환:

Logical Operators Operands types Description
A AND B boolean A와 B 모두 TRUE면 TRUE, 아니면 FALSE
A && B boolean A AND B와 동일
A OR B boolean A나 B 또는 둘 다 TRUE면 TRUE
A | B boolean A OR B와 동일
NOT A boolean A가 FALSE면 TRUE
!A boolean NOT A와 동일

복합 타입 연산자(Operators on Complex Types) — 복합 타입 요소에 접근하는 메커니즘 제공:

Operator Operand types Description
A[n] A는 Array, n은 int 배열 A의 n번째 요소. 첫 요소는 인덱스 0. 예: ['foo','bar'] 배열 A에서 A[0]은 'foo', A[1]은 'bar'
M[key] M은 Map<K,V>, key는 타입 K map에서 key에 대응하는 값. 예: {'f'->'foo','b'->'bar','all'->'foobar'}에서 M['all']은 'foobar'
S.x S는 struct S의 x 필드. 예: struct foobar {int foo, int bar}에서 foobar.foo는 foo 필드의 정수
내장 함수(Built In Functions)

Hive는 다음 내장 함수를 지원해요 (FunctionRegistry.java):

Return Type Function Name (Signature) Description
BIGINT round(double a) double의 반올림된 BIGINT 값
BIGINT floor(double a) double과 같거나 작은 최대 BIGINT 값
BIGINT ceil(double a) double과 같거나 큰 최소 BIGINT 값
double rand(), rand(int seed) 행마다 바뀌는 난수. seed를 지정하면 난수 시퀀스가 결정적
string concat(string A, string B,…) B를 A 뒤에 붙인 문자열. 임의 개수 인자 지원
string substr(string A, int start) start 위치부터 끝까지의 부분 문자열. substr('foobar', 4) = 'bar'
string substr(string A, int start, int length) start부터 length 길이의 부분 문자열. substr('foobar', 4, 2) = 'ba'
string upper(string A) / ucase(string A) 모든 문자를 대문자로
string lower(string A) / lcase(string A) 모든 문자를 소문자로
string trim(string A) 양 끝 공백 제거
string ltrim(string A) / rtrim(string A) 왼쪽/오른쪽 공백 제거
string regexp_replace(string A, string B, string C) A의 B 정규식과 일치하는 부분 문자열을 C로 치환
int size(Map<K.V>) map 타입의 요소 수
int size(Array) array 타입의 요소 수
value of <type> cast(<expr> as <type>) expr의 결과를 type으로 변환. 변환 실패 시 null. cast('1' as BIGINT)
string from_unixtime(int unixtime) UNIX epoch(1970-01-01 00:00:00 UTC) 초를 현재 시스템 타임존의 "1970-01-01 00:00:00" 형식 타임스탬프 문자열로 변환
string to_date(string timestamp) 타임스탬프 문자열의 날짜 부분. to_date("1970-01-01 00:00:00") = "1970-01-01"
int year/month/day(string date) 날짜/타임스탬프 문자열의 년/월/일 부분
string get_json_object(string json_string, string path) json 경로로 json 객체 추출. 잘못된 입력이면 null

내장 집계 함수(aggregate functions):

Return Type Aggregation Function Name (Signature) Description
BIGINT count(*), count(expr), count(DISTINCT expr[, expr_.]) count(*)은 NULL 포함 모든 검색 행 수, count(expr)은 식이 non-NULL인 행 수, count(DISTINCT expr)은 고유하고 non-NULL인 행 수
DOUBLE sum(col), sum(DISTINCT col) 그룹 요소 합 또는 그룹의 고유 값 합
DOUBLE avg(col), avg(DISTINCT col) 그룹 요소 평균 또는 고유 값 평균
DOUBLE min(col) 그룹의 컬럼 최솟값
DOUBLE max(col) 그룹의 컬럼 최댓값

언어 기능(Language Capabilities)

Hive의 SQL은 기본 SQL 연산을 제공합니다. 이 연산들은 테이블이나 파티션에 대해 동작해요:

  • WHERE 절로 테이블의 행을 필터링하는 능력
  • SELECT 절로 테이블의 특정 컬럼을 선택하는 능력
  • 두 테이블 간 equi-join 능력
  • 테이블에 저장된 데이터의 여러 "group by" 컬럼에 대한 집계 평가 능력
  • 쿼리 결과를 다른 테이블에 저장하는 능력
  • 테이블 내용을 로컬(예: nfs) 디렉터리로 다운로드하는 능력
  • 쿼리 결과를 hadoop dfs 디렉터리에 저장하는 능력
  • 테이블과 파티션 관리(create, drop, alter) 능력
  • 선택한 언어의 커스텀 스크립트를 커스텀 map/reduce 잡으로 끼워 넣는 능력

참고: 아래 많은 예제는 오래됐어요. 더 최신 정보는 LanguageManual에 있습니다.

사용과 예제(Usage and Examples)

  • Creating, Showing, Altering, and Dropping Tables
  • Loading Data
  • Querying and Inserting Data

테이블 생성, 표시, 변경, 삭제

자세한 내용은 Hive Data Definition Language를 참고하세요.

테이블 생성(Creating Tables)

위에서 언급한 page_view 테이블을 만드는 예제 문장:

    CREATE TABLE page_view(viewTime INT, userid BIGINT,
                    page_url STRING, referrer_url STRING,
                    ip STRING COMMENT 'IP Address of the User')
    COMMENT 'This is the page view table'
    PARTITIONED BY(dt STRING, country STRING)
    STORED AS SEQUENCEFILE;

이 예에서 테이블의 컬럼은 타입과 함께 지정됩니다. 코멘트는 컬럼 수준과 테이블 수준 양쪽에 붙일 수 있어요. partitioned by 절은 데이터 컬럼과 다르고 실제로 데이터와 함께 저장되지 않는 파티셔닝 컬럼을 정의합니다. 이런 식으로 지정하면 파일의 데이터는 필드 구분자로 ASCII 001(ctrl-A), 행 구분자로 개행을 쓴다고 가정합니다.

데이터가 위 형식이 아니면 필드 구분자를 매개변수화할 수 있어요:

    CREATE TABLE page_view(viewTime INT, userid BIGINT,
                    page_url STRING, referrer_url STRING,
                    ip STRING COMMENT 'IP Address of the User')
    COMMENT 'This is the page view table'
    PARTITIONED BY(dt STRING, country STRING)
    ROW FORMAT DELIMITED
            FIELDS TERMINATED BY '1'
    STORED AS SEQUENCEFILE;

행 구분자는 Hive가 아니라 Hadoop 구분자에 의해 결정되므로 현재는 변경할 수 없어요.

특정 컬럼으로 테이블을 버킷화하는 것도 좋아요. 효율적인 샘플링 쿼리를 실행할 수 있기 때문입니다. 버킷화가 없어도 테이블에 무작위 샘플링을 할 수 있지만, 쿼리가 모든 데이터를 스캔해야 하므로 비효율적입니다. 다음 예는 userid 컬럼으로 버킷화된 page_view 테이블입니다:

    CREATE TABLE page_view(viewTime INT, userid BIGINT,
                    page_url STRING, referrer_url STRING,
                    ip STRING COMMENT 'IP Address of the User')
    COMMENT 'This is the page view table'
    PARTITIONED BY(dt STRING, country STRING)
    CLUSTERED BY(userid) SORTED BY(viewTime) INTO 32 BUCKETS
    ROW FORMAT DELIMITED
            FIELDS TERMINATED BY '1'
            COLLECTION ITEMS TERMINATED BY '2'
            MAP KEYS TERMINATED BY '3'
    STORED AS SEQUENCEFILE;

위 예에서 테이블은 userid의 해시 함수로 32개 버킷으로 클러스터됩니다. 각 버킷 안에서 데이터는 viewTime 오름차순으로 정렬됩니다. 이런 구성은 클러스터된 컬럼(userid)에 대한 효율적인 샘플링을 가능하게 해 줍니다. 정렬 속성은 내부 연산자가 쿼리를 더 효율적으로 평가할 때 더 잘 알려진 데이터 구조를 활용하게 해 줍니다.

복합 타입 컬럼이 있는 예:

    CREATE TABLE page_view(viewTime INT, userid BIGINT,
                    page_url STRING, referrer_url STRING,
                    friends ARRAY<BIGINT>, properties MAP<STRING, STRING>
                    ip STRING COMMENT 'IP Address of the User')
    COMMENT 'This is the page view table'
    PARTITIONED BY(dt STRING, country STRING)
    CLUSTERED BY(userid) SORTED BY(viewTime) INTO 32 BUCKETS
    ROW FORMAT DELIMITED
            FIELDS TERMINATED BY '1'
            COLLECTION ITEMS TERMINATED BY '2'
            MAP KEYS TERMINATED BY '3'
    STORED AS SEQUENCEFILE;

ROW FORMAT과 STORED AS 절에 표시된 값은 시스템 기본값을 나타냅니다. 테이블 이름과 컬럼 이름은 대소문자를 구분하지 않아요.

테이블과 파티션 탐색(Browsing Tables and Partitions)
    SHOW TABLES;

웨어하우스의 기존 테이블을 나열합니다.

    SHOW TABLES 'page.*';

'page' 접두사로 테이블을 나열합니다. 패턴은 Java 정규식 문법을 따르므로 점은 와일드카드예요.

    SHOW PARTITIONS page_view;

테이블의 파티션을 나열합니다. 파티셔닝되지 않은 테이블이면 오류가 발생합니다.

    DESCRIBE page_view;

테이블의 컬럼과 컬럼 타입을 나열합니다.

    DESCRIBE EXTENDED page_view;

테이블의 컬럼과 모든 다른 속성을 나열합니다. 정보가 많고 예쁘지 않은 형식이라 디버깅에 주로 쓰입니다.

   DESCRIBE EXTENDED page_view PARTITION (ds='2008-08-08');

파티션의 컬럼과 모든 다른 속성을 나열합니다.

테이블 변경(Altering Tables)

기존 테이블을 새 이름으로 rename하기 (새 이름의 테이블이 있으면 오류):

    ALTER TABLE old_table_name RENAME TO new_table_name;

기존 테이블의 컬럼을 rename하기. 같은 컬럼 타입을 쓰고, 기존 컬럼마다 항목을 포함해야 해요:

    ALTER TABLE old_table_name REPLACE COLUMNS (col1 TYPE, ...);

기존 테이블에 컬럼 추가:

    ALTER TABLE tab1 ADD COLUMNS (c1 INT COMMENT 'a new int column', c2 STRING DEFAULT 'def val');

스키마 변경(컬럼 추가 같은)은 파티셔닝된 테이블이면 테이블의 옛 파티션의 스키마를 보존해요. 이 컬럼들에 접근하는 쿼리는 옛 파티션 위에서 암묵적으로 null 값이나 지정된 기본값을 반환합니다.

테이블과 파티션 삭제(Dropping Tables and Partitions)

테이블 drop은 아주 간단해요. 테이블 drop은 테이블에 만들어졌을 인덱스도 암묵적으로 drop합니다(향후 기능). 명령:

    DROP TABLE pv_users;

파티션 drop은 테이블을 alter해 파티션을 drop합니다.

    ALTER TABLE pv_users DROP PARTITION (ds='2008-08-08')

이 테이블이나 파티션의 어떤 데이터든 drop되며 복구되지 않을 수 있다는 점을 유의하세요.

데이터 로딩(Loading Data)

Hive 테이블에 데이터를 로드하는 방법은 여러 가지입니다. 사용자는 HDFS의 지정된 위치를 가리키는 외부 테이블을 만들 수 있어요. 이 용법에서 사용자는 HDFS put/copy 명령으로 파일을 지정 위치에 복사하고, 관련 row format 정보와 함께 이 위치를 가리키는 테이블을 만듭니다. 그 후 데이터를 변환해 다른 Hive 테이블에 insert할 수 있어요. 예를 들어 파일 /tmp/pv_2008-06-08.txt가 2008-06-08에 제공된 쉼표 구분 페이지 뷰를 담고 있고, 이를 page_view 테이블의 적절한 파티션에 로드해야 한다면:

    CREATE EXTERNAL TABLE page_view_stg(viewTime INT, userid BIGINT,
                    page_url STRING, referrer_url STRING,
                    ip STRING COMMENT 'IP Address of the User',
                    country STRING COMMENT 'country of origination')
    COMMENT 'This is the staging page view table'
    ROW FORMAT DELIMITED FIELDS TERMINATED BY '44' LINES TERMINATED BY '12'
    STORED AS TEXTFILE
    LOCATION '/user/data/staging/page_view';

    hadoop dfs -put /tmp/pv_2008-06-08.txt /user/data/staging/page_view

    FROM page_view_stg pvs
    INSERT OVERWRITE TABLE page_view PARTITION(dt='2008-06-08', country='US')
    SELECT pvs.viewTime, pvs.userid, pvs.page_url, pvs.referrer_url, null, null, pvs.ip
    WHERE pvs.country = 'US';

이 코드는 LINES TERMINATED BY 제한 때문에 오류가 발생함. FAILED: SemanticException 6:67 LINES TERMINATED BY only supports newline '\n' right now. 반환 타입과 저장 형식을 맞추는 게 중요합니다.

위 예에서 대상 테이블의 array/map 타입에 null이 삽입되지만, 적절한 row format을 지정하면 외부 테이블에서 오는 것일 수도 있어요. 이 방법은 HDFS에 데이터를 Hive로 쿼리·조작할 수 있게 할 메타데이터를 얹고 싶은 레거시 데이터가 이미 있을 때 유용합니다.

또한 시스템은 로컬 파일시스템의 파일에서 입력 데이터 형식이 테이블 형식과 같은 Hive 테이블로 직접 데이터를 로드하는 문법도 지원해요. /tmp/pv_2008-06-08_us.txt가 이미 US 데이터를 담고 있다면 이전 예의 추가 필터링이 필요 없습니다. 이 경우 LOAD는 다음 문법으로 할 수 있어요:

   LOAD DATA LOCAL INPATH /tmp/pv_2008-06-08_us.txt INTO TABLE page_view PARTITION(date='2008-06-08', country='US')

path 인자는 디렉터리(그 경우 디렉터리의 모든 파일 로드), 단일 파일 이름, 와일드카드(그 경우 일치하는 모든 파일 업로드)일 수 있어요. 인자가 디렉터리면 하위 디렉터리를 포함할 수 없습니다. 와일드카드는 파일 이름과만 일치해야 해요.

입력 파일이 매우 크면 사용자가 (Hive 외부 도구로) 데이터의 병렬 로드가 가능합니다. 파일이 HDFS에 있으면 다음 문법으로 Hive 테이블에 로드할 수 있어요:

   LOAD DATA INPATH '/user/data/pv_2008-06-08_us.txt' INTO TABLE page_view PARTITION(date='2008-06-08', country='US')

이 예에서 input.txt 파일의 array/map 필드는 null 필드로 가정합니다. 데이터 로드에 대한 더 많은 정보는 Hive Data Manipulation Language를, 외부 테이블 생성 예는 External Tables를 참고하세요.

데이터 쿼리와 삽입(Querying and Inserting Data)

하위 섹션: Simple Query, Partition Based Query, Joins, Aggregations, Multi Table/File Inserts, Dynamic-Partition Insert, Inserting into Local Files, Sampling, Union All, Array Operations, Map (Associative Arrays) Operations, Custom Map/Reduce Scripts, Co-Groups.

Hive 쿼리 연산은 Select에, insert 연산은 Inserting data into Hive Tables from queries와 Writing data into the filesystem from queries에 문서화되어 있습니다.

단순 쿼리(Simple Query)

활성 사용자 모두에 대해:

    INSERT OVERWRITE TABLE user_active
    SELECT user.*
    FROM user
    WHERE user.active = 1;

SQL과 달리 항상 결과를 테이블에 insert한다는 점에 주의하세요. Beeline이나 Hive CLI에서도 직접 쿼리를 실행할 수 있고, 내부적으로 임시 파일로 재작성되어 클라이언트에 표시됩니다.

파티션 기반 쿼리(Partition Based Query)

쿼리에서 어떤 파티션을 쓸지는 where 절의 파티션 컬럼 조건에 따라 시스템이 자동 결정합니다. 예를 들어 xyz.com 도메인에서 참조된 03/2008 월의 모든 page_views:

    INSERT OVERWRITE TABLE xyz_com_page_views
    SELECT page_views.*
    FROM page_views
    WHERE page_views.date >= '2008-03-01' AND page_views.date <= '2008-03-31' AND
          page_views.referrer_url like '%xyz.com';

위 테이블은 PARTITIONED BY(date DATETIME, country STRING)로 정의됐으므로 page_views.date를 사용합니다. 파티션을 다른 이름으로 지정했으면 .date가 예상대로 동작하지 않을 거예요.

조인(Joins)

2008-03-03의 page_view의 (성별로) 인구통계 분포를 얻으려면 page_view와 user 테이블을 userid 컬럼으로 조인해야 해요:

    INSERT OVERWRITE TABLE pv_users
    SELECT pv.*, u.gender, u.age
    FROM user u JOIN page_view pv ON (pv.userid = u.id)
    WHERE pv.date = '2008-03-03';

외부 조인은 LEFT OUTER, RIGHT OUTER, FULL OUTER 키워드로 조인을 수식해 어떤 종류의 외부 조인인지 나타냅니다:

    INSERT OVERWRITE TABLE pv_users
    SELECT pv.*, u.gender, u.age
    FROM user u FULL OUTER JOIN page_view pv ON (pv.userid = u.id)
    WHERE pv.date = '2008-03-03';

다른 테이블에 키의 존재 여부를 확인하려면 LEFT SEMI JOIN을 사용합니다:

    INSERT OVERWRITE TABLE pv_users
    SELECT u.*
    FROM user u LEFT SEMI JOIN page_view pv ON (pv.userid = u.id)
    WHERE pv.date = '2008-03-03';

둘 이상의 테이블 조인:

    INSERT OVERWRITE TABLE pv_friends
    SELECT pv.*, u.gender, u.age, f.friends
    FROM page_view pv JOIN user u ON (pv.userid = u.id) JOIN friend_list f ON (u.id = f.uid)
    WHERE pv.date = '2008-03-03';
집계(Aggregations)

성별로 고유 사용자 수를 세기:

    INSERT OVERWRITE TABLE pv_gender_sum
    SELECT pv_users.gender, count (DISTINCT pv_users.userid)
    FROM pv_users
    GROUP BY pv_users.gender;

여러 집계를 동시에 할 수 있지만, 두 집계가 서로 다른 DISTINCT 컬럼을 가질 수는 없습니다. 가능한 예:

    INSERT OVERWRITE TABLE pv_gender_agg
    SELECT pv_users.gender, count(DISTINCT pv_users.userid), count(*), sum(DISTINCT pv_users.userid)
    FROM pv_users
    GROUP BY pv_users.gender;

하지만 다음은 허용되지 않아요 (서로 다른 DISTINCT 컬럼 두 개):

    INSERT OVERWRITE TABLE pv_gender_agg
    SELECT pv_users.gender, count(DISTINCT pv_users.userid), count(DISTINCT pv_users.ip)
    FROM pv_users
    GROUP BY pv_users.gender;
다중 테이블/파일 인서트(Multi Table/File Inserts)

집계나 단순 select 출력은 여러 테이블이나 hadoop dfs 파일로 보낼 수 있어요. 예를 들어 성별 분포와 함께 연령별 고유 page view 분포를 찾으려면:

    FROM pv_users
    INSERT OVERWRITE TABLE pv_gender_sum
        SELECT pv_users.gender, count_distinct(pv_users.userid)
        GROUP BY pv_users.gender

    INSERT OVERWRITE DIRECTORY '/user/data/tmp/pv_age_sum'
        SELECT pv_users.age, count_distinct(pv_users.userid)
        GROUP BY pv_users.age;

첫 insert 절은 첫 group by 결과를 Hive 테이블로, 두 번째는 hadoop dfs 파일로 보냅니다.

동적 파티션 인서트(Dynamic-Partition Insert)

이전 예들에서 사용자는 어떤 파티션에 insert할지 알아야 하고, 하나의 insert 문으로 하나의 파티션만 넣을 수 있어요. 여러 파티션에 로드하려면 multi-insert 문을 써야 합니다:

    FROM page_view_stg pvs
    INSERT OVERWRITE TABLE page_view PARTITION(dt='2008-06-08', country='US')
           SELECT pvs.viewTime, pvs.userid, pvs.page_url, pvs.referrer_url, null, null, pvs.ip WHERE pvs.country = 'US'
    INSERT OVERWRITE TABLE page_view PARTITION(dt='2008-06-08', country='CA')
           SELECT pvs.viewTime, pvs.userid, pvs.page_url, pvs.referrer_url, null, null, pvs.ip WHERE pvs.country = 'CA'
    INSERT OVERWRITE TABLE page_view PARTITION(dt='2008-06-08', country='UK')
           SELECT pvs.viewTime, pvs.userid, pvs.page_url, pvs.referrer_url, null, null, pvs.ip WHERE pvs.country = 'UK';

특정 날짜의 모든 국가 파티션에 로드하려면 입력 데이터의 모든 국가마다 insert 문을 추가해야 해요. 이는 입력 데이터의 국가 목록에 대한 사전 지식을 요구하고 파티션을 미리 만들어야 하므로 매우 불편합니다. 동적 파티션 인서트(또는 multi-partition insert)는 입력 테이블을 스캔하면서 어떤 파티션을 만들고 채울지 동적으로 결정해 이 문제를 해결해요. Hive 0.6.0부터 있는 새 기능입니다. 동적 파티션 인서트에서 입력 컬럼 값을 평가해 이 행이 어느 파티션에 insert돼야 할지 결정합니다. 파티션이 아직 없으면 자동으로 만듭니다. 이 기능을 쓰면 필요한 모든 파티션을 만들고 채우는 데 insert 문 하나면 됩니다. 또 insert 문이 하나이므로 MapReduce 잡도 하나뿐이라 성능이 크게 향상되고 클러스터 작업량이 줄어듭니다.

한 insert 문으로 모든 국가 파티션에 로드하는 예:

    FROM page_view_stg pvs
    INSERT OVERWRITE TABLE page_view PARTITION(dt='2008-06-08', country)
           SELECT pvs.viewTime, pvs.userid, pvs.page_url, pvs.referrer_url, null, null, pvs.ip, pvs.country

multi-insert 문과의 여러 문법 차이점:

  • country는 값과 연결되지 않은 채 PARTITION 명세에 나타나요. 이 경우 country는 동적 파티션 컬럼입니다. 반면 ds는 값이 있으므로 정적 파티션 컬럼입니다. 동적 파티션 컬럼의 값은 입력 컬럼에서 옵니다. 현재 파티션 절에서 마지막 컬럼(들)만 동적 파티션 컬럼으로 허용돼요. 파티션 컬럼 순서가 계층 순서를 나타내기 때문입니다(dt가 루트 파티션, country가 자식). (dt, country='US') 같은 파티션 절은 지정할 수 없어요. 모든 날짜 파티션을 갱신하되 country 하위 파티션은 'US'로 한다는 뜻이 되기 때문입니다.
  • select 문에 pvs.country 컬럼이 추가됐어요. 이는 동적 파티션 컬럼의 대응 입력 컬럼입니다. 정적 파티션 컬럼은 값이 이미 PARTITION 절에 있으므로 입력 컬럼을 추가할 필요가 없습니다. 동적 파티션 값은 이름이 아니라 순서로 선택되며 select 절의 마지막 컬럼들로 취해집니다.

동적 파티션 인서트 문의 의미:

  • 동적 파티션 컬럼에 이미 비어 있지 않은 파티션이 있을 때(country='CA'가 어떤 ds 루트 파티션 아래 이미 존재), 입력 데이터에 같은 값('CA')이 보이면 덮어써져요. 이는 'insert overwrite' 의미와 일치합니다. 그러나 'CA' 값이 입력 데이터에 없으면 기존 파티션은 덮어써지지 않습니다.
  • Hive 파티션은 HDFS의 디렉터리에 대응하므로 파티션 값은 HDFS 경로 형식(Java의 URI)을 따라야 해요. URI에서 특별한 의미를 가진 문자('%',':','/','#')는 '%' 뒤에 2바이트 ASCII 값으로 이스케이프됩니다.
  • 입력 컬럼이 STRING과 다른 타입이면 HDFS 경로를 만들기 위해 먼저 STRING으로 변환됩니다.
  • 입력 컬럼 값이 NULL이거나 빈 문자열이면 행이 특수 파티션에 들어가는데, 그 이름은 hive.exec.default.partition.name 파라미터로 제어됩니다. 기본값은 HIVE_DEFAULT_PARTITION입니다. 기본적으로 이 파티션은 파티션 이름으로 유효하지 않은 값을 가진 모든 "bad" 행을 담아요. 이 접근의 단점은 bad 값을 잃고 Hive에서 선택하면 HIVE_DEFAULT_PARTITION으로 대체된다는 것입니다.
  • 동적 파티션 인서트는 짧은 시간에 많은 파티션을 생성해 리소스를 많이 먹을 수 있어요. 세 가지 파라미터가 정의됩니다:
    • hive.exec.max.dynamic.partitions.pernode (기본 100) — 각 매퍼/리듀서가 만들 수 있는 최대 동적 파티션 수. 넘으면 치명적 오류가 발생하고 잡이 종료됩니다.
    • hive.exec.max.dynamic.partitions (기본 1000) — 하나의 DML이 만들 수 있는 총 동적 파티션 수. 각 매퍼/리듀서는 한도를 안 넘었지만 총수가 넘으면 중간 데이터가 최종 위치로 이동되기 전 잡 끝에 예외가 발생합니다.
    • hive.exec.max.created.files (기본 100000) — 모든 매퍼/리듀서가 만드는 총 파일 수 최대. 새 파일이 만들어질 때마다 카운터 갱신. 총수가 초과하면 치명적 오류.
  • hive.exec.dynamic.partition.mode=strict 파라미터로 all-dynamic 파티션 경우를 막아요. strict 모드에서는 최소 하나의 정적 파티션을 지정해야 합니다. 기본 모드는 strict입니다. 또 hive.exec.dynamic.partition=true/false로 동적 파티션 자체를 허용할지 제어합니다. 기본값은 Hive 0.9.0 이전에는 false, 0.9.0 이후에는 true입니다.

트러블슈팅과 모범 사례:

특정 매퍼/리듀서가 만든 동적 파티션이 너무 많으면 치명적 오류가 발생하고 잡이 종료됩니다. 문제는 한 매퍼가 무작위 행 집합을 가져 (dt, country) 쌍의 수가 hive.exec.max.dynamic.partitions.pernode 한도를 넘을 가능성이 높다는 것입니다. 한 가지 해결책은 매퍼에서 동적 파티션 컬럼으로 행을 그룹화해 동적 파티션이 생성되는 리듀서로 분배하는 것입니다:

    FROM page_view_stg pvs
          INSERT OVERWRITE TABLE page_view PARTITION(dt, country)
                 SELECT pvs.viewTime, pvs.userid, pvs.page_url, pvs.referrer_url, null, null, pvs.ip,
                        from_unixtimestamp(pvs.viewTime, 'yyyy-MM-dd') ds, pvs.country
                 DISTRIBUTE BY ds, country;

이 쿼리는 Map-only 잡이 아닌 MapReduce 잡을 만듭니다. SELECT 절은 매퍼 계획으로 변환되고, 출력은 (ds, country) 값에 따라 리듀서로 분배됩니다. INSERT 절은 동적 파티션에 쓰는 리듀서의 계획으로 변환됩니다.

로컬 파일로 인서트(Inserting into Local Files)

출력을 로컬 파일로 써서 엑셀 스프레드시트에 로드하고 싶을 때:

    INSERT OVERWRITE LOCAL DIRECTORY '/tmp/pv_gender_sum'
    SELECT pv_gender_sum.*
    FROM pv_gender_sum;
샘플링(Sampling)

sampling 절은 전체 테이블 대신 데이터 표본에 대한 쿼리를 쓰게 해 줍니다. 현재 sampling은 CREATE TABLE 문의 CLUSTERED BY 절에 지정된 컬럼에 대해 수행돼요. 다음 예는 pv_gender_sum 테이블의 32개 버킷 중 3번째 버킷을 선택합니다:

    INSERT OVERWRITE TABLE pv_gender_sum_sample
    SELECT pv_gender_sum.*
    FROM pv_gender_sum TABLESAMPLE(BUCKET 3 OUT OF 32);

일반적으로 TABLESAMPLE 문법은 TABLESAMPLE(BUCKET x OUT OF y)입니다. y는 테이블 생성 시 지정된 버킷 수의 배수 또는 약수여야 합니다. 선택된 버킷은 bucket_number mod y가 x와 같으면 결정됩니다. TABLESAMPLE(BUCKET 3 OUT OF 16)은 3번째와 19번째 버킷을 고릅니다. 버킷은 0부터 번호가 매겨집니다. TABLESAMPLE(BUCKET 3 OUT OF 64 ON userid)은 3번째 버킷의 절반을 고릅니다.

Union All

union all도 지원합니다. 두 테이블(비디오를 게시한 사용자, 코멘트를 게시한 사용자 추적)의 union all 결과를 user 테이블과 조인해 모든 비디오 게시·코멘트 게시 이벤트에 대한 단일 주석 달린 스트림을 만들 수 있어요:

    INSERT OVERWRITE TABLE actions_users
    SELECT u.id, actions.date
    FROM (
        SELECT av.uid AS uid
        FROM action_video av
        WHERE av.date = '2008-06-03'

        UNION ALL

        SELECT ac.uid AS uid
        FROM action_comment ac
        WHERE ac.date = '2008-06-03'
        ) actions JOIN users u ON(u.id = actions.uid);
배열 연산(Array Operations)

테이블의 배열 컬럼은 다음과 같을 수 있어요:

CREATE TABLE array_table (int_array_column ARRAY<INT>);

pv.friends가 ARRAY 타입이라면 인덱스로 특정 요소를 얻을 수 있습니다:

    SELECT pv.friends[2]
    FROM page_views pv;

이 select 식은 pv.friends 배열의 세 번째 항목을 가져옵니다. size 함수로 배열 길이를 얻을 수도 있어요:

   SELECT pv.userid, size(pv.friends)
   FROM page_view pv;
맵 연산(Map (Associative Arrays) Operations)

맵은 연관 배열과 비슷한 컬렉션을 제공해요. pv.properties가 map<String, String> 타입이라면:

    INSERT OVERWRITE page_views_map
    SELECT pv.userid, pv.properties['page type']
    FROM page_views pv;

로 page_views 테이블에서 'page_type' 속성을 선택할 수 있습니다. 배열처럼 size 함수로 맵의 요소 수를 얻을 수 있어요:

   SELECT size(pv.properties)
   FROM page_view pv;
커스텀 Map/Reduce 스크립트(Custom Map/Reduce Scripts)

Hive 언어가 기본 지원하는 기능으로 커스텀 매퍼와 리듀서를 데이터 스트림에 끼워 넣을 수 있어요. map_script 매퍼와 reduce_script 리듀서를 실행하려면 TRANSFORM 절을 사용합니다. 컬럼은 사용자 스크립트에 전달되기 전에 string으로 변환되고 TAB으로 구분되며, 사용자 스크립트의 표준 출력은 TAB 구분 string 컬럼으로 취급됩니다. 스크립트는 표준 에러로 디버그 정보를 출력할 수 있고 hadoop 태스크 상세 페이지에 표시됩니다.

   FROM (
        FROM pv_users
        MAP pv_users.userid, pv_users.date
        USING 'map_script'
        AS dt, uid
        CLUSTER BY dt) map_output

    INSERT OVERWRITE TABLE pv_users_reduced
        REDUCE map_output.dt, map_output.uid
        USING 'reduce_script'
        AS date, count;

샘플 맵 스크립트 (weekday_mapper.py):

import sys
import datetime

for line in sys.stdin:
  line = line.strip()
  userid, unixtime = line.split('\t')
  weekday = datetime.datetime.fromtimestamp(float(unixtime)).isoweekday()
  print ','.join([userid, str(weekday)])

MAP와 REDUCE는 더 일반적인 select transform의 "문법적 설탕(syntactic sugar)"이에요. 내부 쿼리는 다음과 같이 쓸 수도 있습니다:

    SELECT TRANSFORM(pv_users.userid, pv_users.date) USING 'map_script' AS dt, uid CLUSTER BY dt FROM pv_users;

스키마 없는 map/reduce: "USING map_script" 뒤에 "AS" 절이 없으면 Hive는 스크립트 출력이 첫 탭 앞의 key와 첫 탭 뒤의 나머지 value로 2부분이라 가정합니다. 이는 "AS key, value"를 지정하는 것과 다릅니다(그 경우 탭이 여러 개면 value는 첫 탭과 둘째 탭 사이의 부분만 포함).

Distribute By와 Sort By: "cluster by" 대신 "distribute by"와 "sort by"를 지정해 파티션 컬럼과 정렬 컬럼을 다르게 할 수 있어요. 보통 파티션 컬럼은 정렬 컬럼의 접두사지만, 필수는 아닙니다.

    FROM (
        FROM pv_users
        MAP pv_users.userid, pv_users.date
        USING 'map_script'
        AS c1, c2, c3
        DISTRIBUTE BY c2
        SORT BY c2, c1) map_output

    INSERT OVERWRITE TABLE pv_users_reduced

        REDUCE map_output.c1, map_output.c2, map_output.c3
        USING 'reduce_script'
        AS date, count;
공동 그룹(Co-Groups)

map/reduce 사용자 커뮤니티에서 cogroup은 여러 테이블의 데이터를 커스텀 리듀서로 보내 특정 컬럼 값으로 행을 그룹화하는 꽤 흔한 연산이에요. UNION ALL 연산자와 CLUSTER BY 명세로 Hive 쿼리 언어에서 이를 달성할 수 있습니다:

   FROM (
        FROM (
                FROM action_video av
                SELECT av.uid AS uid, av.id AS id, av.date AS date

               UNION ALL

                FROM action_comment ac
                SELECT ac.uid AS uid, ac.id AS id, ac.date AS date
        ) union_actions
        SELECT union_actions.uid, union_actions.id, union_actions.date
        CLUSTER BY union_actions.uid) map

    INSERT OVERWRITE TABLE actions_reduced
        SELECT TRANSFORM(map.uid, map.id, map.date) USING 'reduce_script' AS (uid, id, reduced_val);

더 알아보기 (Learn more)

Hive는 대용량 배치 데이터에 SQL을 쓸 수 있게 해 주는 데이터 웨어하우징 도구예요. 타입 계층, 파티션/버킷, 동적 파티션 인서트, TRANSFORM을 이해하면 실무에서 바로 활용할 수 있습니다. 더 최신 문법은 LanguageManual 문서를 참고하세요.