BigQuery UNNEST, ARRAY, STRUCT 사용 방법
- BigQuery Unnest, Array, Struct 사용 방법에 대해 작성한 글입니다
- 목차
들어가며
- BigQuery는 SQL 문법을 사용하고 있기 때문에, 많은 사람들이 처음에 쉽게 접할 수 있음
- 그러나 자주보기 힘든 ARRAY, STRUCT, UNNEST를 만나면 많은 사람들이 어려워함
- 특히 Google Analytics 데이터나 Firebase 데이터 작업시 Cannot access field name on a value with type ARRAY 이런 Error를 접함
- 자료형들에 대한 설명과 이런 데이터를 쿼리할 때 어떻게 사용해야 하는지에 대해 정리하려고 함
- 참고로, BigQuery는 비정규화되었을 때 성능이 가장 뛰어남
- 비정규화를 위해 중첩(Nested) 및 반복(Repeated) 열을 사용함
- 중첩된 레코드를 사용할 때 장점
- 빅쿼리 퍼포먼스 개선
- 데이터 저장 용량의 효율
- 스키마가 바뀌어도 유연하게 대응 가능
- 이 내용은 BigQuery Document에 나옴
BigQuery ARRAY
- 배열 함수 Document
- BigQuery는 데이터 유형이 동일한 값으로 구성된 목록을 ARRAY(배열)라 부름
- 파이썬의 List와 유사함(완전히 동일하진 않음)
- 하나의 행에 데이터 타입이 동일한 여러 값이 저장됨
- 빅쿼리 UI에서 배열을 보여줄 때 세로로 나열됨
SELECT [1,2,3] AS array_sample, 1 AS int_value UNION ALL SELECT [3,5,7] AS array_sample, 2 AS int_value
- 쿼리 결과
- 행을 보면 1과 2만 있고, 1 안에 array_sample에 1, 2, 3이 세로로 출력됨. 반면에 단순 숫자값을 나열한 int_value는 1개만 보임
- 행 하나에 딸려있는 경우 ARRAY!
- array_sample이 ARRAY(int_value는 INT64)
- ARRAY 생성 방법
1) 대괄호(
[
,]
) 사용SELECT [1, 2, 3] AS array_sample
- 2) ARRAY<> 사용
- ARRAY<타입>을 작성하고 괄호 사용타입>
SELECT ARRAY<INT64>[1,2,3] AS int_array
- 3) GENERATE 함수 사용
- GENERATE_ARRAY, GENERATE_DATE_ARRAY, GENERATE_TIMESTAMP_ARRAY 등 사용
- GENERATE_ARRAY(시작, 종료, 간격) : python에서 range(start, end, step)과 동일함
SELECT GENERATE_ARRAY(1, 10, 2) AS generate_array_data
- GENERATE_DATE_ARRAY도 GENERATE_ARRAY와 동일
SELECT GENERATE_DATE_ARRAY('2020-01-01', '2020-02-01', INTERVAL 1 WEEK) AS generate_date_array_data
- 4) ARRAY_AGG 사용
- ARRAY_AGG, ARRAY_CONCAT_AGG 등 사용
- Table에 저장된 데이터를 SELECT하고 ARRAY로 묶고싶은 경우 사용
WITH programming_languages AS (SELECT "python" AS programming_language UNION ALL SELECT "go" AS programming_language UNION ALL SELECT "scala" AS programming_language) SELECT ARRAY_AGG(programming_language) AS programming_languages_array FROM programming_languages
- 여기서 SELECT [“python”, “go”, “scala”] as programming_language로 가능한거 아니냐고 생각할 수 있는데, 데이터를 만드는 과정이라면 둘 다 가능하지만, Table에 저장된 데이터를 SELECT해서 집계할 땐 이렇게 해야함
- SELECT ARRAY[programming_language] AS programming_languages_array 이런 쿼리를 직접 돌려보면 배열이 아닌 행에 하나의 값만 저장되는 것을 볼 수 있음. ARRAY_AGG을 사용!
- 만약 배열 안에 NULL이 하나라도 있는 경우, ARRAY_AGG의 결과는 NULL이 나온다
- 이 경우 IGNORE NULLS을 추가하면 널을 제외하고 연산함
SELECT ARRAY_AGG(x IGNORE NULLS) FROM UNNEST([1, NULL, 2, 4, 6]) x
- ARRAY_AGG의 옵션 절에 궁금하면 ARRAY_AGG를 참고하면 된다(DISTINCT, IGNORE NULLS, ORDER BY 등을 사용할 수 있음)
- 배열 내 접근
- 배열의 N번째 값을 가져오고 싶은 경우 OFFSET, ORDINAL을 사용할 수 있음
- OFFSET : 0부터 시작
- ORDINAL : 1부터 시작
- 존재하지 않는 N을 지정하면 에러가 발생하는데, 이럴 경우 SAFE_를 앞에 붙여주면(SAFE_OFFSET, SAFE_ORDINAL) 에러가 발생하지 않고 NULL이 return됨
WITH programming_languages AS (SELECT "python" AS programming_language UNION ALL SELECT "go" AS programming_language UNION ALL SELECT "scala" AS programming_language) SELECT ARRAY_AGG(programming_language)[OFFSET(0)] AS programming_languages_array, ARRAY_AGG(programming_language)[ORDINAL(1)] AS programming_languages_array2 FROM programming_languages
- 배열을 역순으로 반환하고 싶은 경우
- ARRAY_REVERSE 함수 사용
WITH programming_languages AS (SELECT "python" AS programming_language UNION ALL SELECT "go" AS programming_language UNION ALL SELECT "scala" AS programming_language) SELECT ARRAY_REVERSE(ARRAY_AGG(programming_language)) AS programming_languages_array_reverse FROM programming_languages
- 배열의 길이(배열에 있는 요소들의 개수)가 궁금한 경우
- ARRAY_LENGTH 사용
WITH example_data AS( SELECT 'kyle' AS name, ['Python', 'SQL', 'R', 'Julia', 'Go'] AS preferred_language, 31 AS age UNION ALL SELECT 'max' AS name, ['Python', 'SQL', 'Scala', 'Java', 'Kotlin'] AS preferred_language, 29 AS age UNION ALL SELECT 'yun' AS name, ['Python', 'SQL'] AS preferred_language, 28 AS age ) SELECT *, ARRAY_LENGTH(preferred_language) AS preferred_language_len FROM example_data
BigQuery STRUCT
- 데이터 유형 Document
- 구조체로, BigQuery UI에서 RECORD로 표현됨
- 각각 유형(필수)과 필드 이름(선택사항)이 있는 정렬된 필드의 컨테이너
- C의 Struct
- Python 3.7에 나온 Data Class와 유사. 참고 Stackoverflow
- 처음엔 Python의 Dict과 유사한 느낌(절대 같지 않음!!!)이라 생각하면 편함
- Python에서 Dict in List, List in Dict, List in List 처럼 BigQuery의 ARRAY와 STRUCT도 Array in Struct, Struct in Array, Struct in Struct 등이 가능함
- 따라서 ARRAY를 접하다보면 STRUCT도 알아야 할 경우가 생김(그러나 ARRAY가 더 많이 사용됨)
- Firebase에서 저장된 데이터를 볼 때 특히!
- STRUCT 생성 방법
- 1) 소괄호(
(
,)
) 사용SELECT (1,2,3) AS struct_test
- 2) STRUCT<> 사용
- <> 안에 타입을 지정해서 사용
SELECT STRUCT<INT64, INT64, STRING>(1, 2, 'HI') AS struct_test
- 타입 앞에 이름을 지정할 수 있음
SELECT STRUCT<hi INT64, hello INT64, awesome STRING>(1, 2, 'HI') AS struct_test
- 또는 타입을 지정하지 않고, AS로 이름을 지정할 수 있음
SELECT STRUCT(1 as hi, 2 as hello, 'HI' as awesome) AS struct_test
- <> 안에 타입을 지정해서 사용
- 3)
STRUCT<x STRUCT<y INT64, z STRING>>
- 이런 표현을 종종 볼 수 있는데, 해석하면 바깥 STRUCT 안에 x라는 이름을 가진 STRUCT가 있고, 그 STRUCT엔 INT64 타입인 값이 1개, STRING 1개가 저장된 경우
SELECT STRUCT<struct_example STRUCT<y INT64, z STRING>>((2, 'HI')) AS struct_test
- 위 예시에선 바깥 STRUCT 이름은 AS struct_test로 정의함
- 4) ARRAY 안에 여러 STRUCT를 사용하고 싶은 경우
- ARRAY(SELECT AS STRUCT) 이런 형태로 사용
SELECT ARRAY( SELECT AS STRUCT 1 as hi, 2, 3 UNION ALL SELECT AS STRUCT 4 as hi, 5, 6 ) AS new_array
- 1) 소괄호(
BigQuery UNNEST
- UNNEST Document
- 예시를 위해 사람들이 선호하는 프로그래밍 언어와 나이가 저장된 데이터가 있다고 가정
- 배열에 통째로 접근하고 싶은 경우엔 평소처럼 컬럼을 지정
- preferred_language
WITH example_data AS( SELECT 'kyle' AS name, ['Python', 'SQL', 'R', 'Julia', 'Go'] AS preferred_language, 31 AS age UNION ALL SELECT 'max' AS name, ['Python', 'SQL', 'Scala', 'Java', 'Kotlin'] AS preferred_language, 29 AS age UNION ALL SELECT 'yun' AS name, ['Python', 'SQL'] AS preferred_language, 28 AS age ) SELECT name, preferred_language, age FROM example_data
- 결과는 다음과 같다
- 만약, Julia 언어를 선호하는 사람을 추출하고 싶다면 어떻게 해야할까?
- WHERE preferred_language = ‘Julia’을 사용하면 아래와 같은 오류가 발생함
- No matching signature for operator = for argument types: ARRAY
, STRING. Supported signatures: ANY = ANY at [21:7] - 이 오류는 preferred_language는 ARRAY
인데, Julia(STRING)랑 비교하려니 타입이 맞지않아 생기는 오류임
- 이런 상황에 배열을 평면화(Flatten)해서 배열에 있는 값을 펴줘야 함
- 배열을 펴줄 때 사용하는 것은 UNNEST로, Nest한 데이터를 UNNEST하게 만드는 것
- UNNEST 연산자는 ARRAY를 입력으로 받고 ARRAY의 각 요소에 대한 행이 한 개씩 포함된 테이블을 return함
- Nested는 중첩 및 반복 열 지정 Document에 더 자세한 설명을 볼 수 있음
- WHERE preferred_language = ‘Julia’을 사용하면 아래와 같은 오류가 발생함
- UNNEST() 사용하는 방법
- UNNEST한 결과와 Table을 CROSS JOIN함
SELECT alias_name.value FROM Table_A CROSS JOIN UNNEST(ARRAY 데이터) as alias_name
- CROSS JOIN을 명시하지 않고 쉼표를 사용해도 동일함
SELECT alias_name.value FROM Table_A, UNNEST(ARRAY 데이터) as alias_name
- 더 직관적인 이해를 위해 How to use the UNNEST function in BigQuery to analyze event parameters in Analytics 글에 나온 예시를 참고하자
- 아래와 같은 데이터가 있다
- crew = “Zoe”를 추출하고 싶어 UNNEST를 사용
SELECT * FROM `spaceships` CROSS JOIN UNNEST(crew) as crew_member
- 이 과정을 시각화하면 아래와 같음
- 이제 WHERE 조건에 crew_member = “Zoe”를 사용할 경우 어떤 흐름으로 되는지 보자
- UNNEST를 사용하면 각 행에 배열의 값을 뿌려준다(=평면화, 펴준다)
- 아래와 같은 데이터가 있다
다시 돌아와서, Julia를 선호하는 사람을 추출하는 사람을 찾기 위해 UNNEST를 사용해보자(필터링은 아직 하지말고)
WITH example_data AS( SELECT 'kyle' AS name, ['Python', 'SQL', 'R', 'Julia', 'Go'] AS preferred_language, 31 AS age UNION ALL SELECT 'max' AS name, ['Python', 'SQL', 'Scala', 'Java', 'Kotlin'] AS preferred_language, 29 AS age UNION ALL SELECT 'yun' AS name, ['Python', 'SQL'] AS preferred_language, 28 AS age ) SELECT name, prefer_lang, age FROM example_data, UNNEST(example_data.preferred_language) as prefer_lang
- FROM 절을 보면 FROM example_data, UNNEST(example_data.preferred_language) as prefer_lang로 명시함
- 즉, example_data의 preferred_language ARRAY를 UNNEST로 펴주고, prefer_lang으로 alias함
- 결과는 다음과 같다
- UNNEST 전에 그냥 ARRAY 접근했던 경우와 비교하자
- 이제 UNNEST 전 후에 어떻게 되는지 주의깊게 보자. 행이 왼쪽은 3, 오른쪽은 12!
- FROM 절을 보면 FROM example_data, UNNEST(example_data.preferred_language) as prefer_lang로 명시함
UNNEST 하고 prefer_lang = ‘Julia’ 조건을 준 쿼리는 아래와 같음
WITH example_data AS( SELECT 'kyle' AS name, ['Python', 'SQL', 'R', 'Julia', 'Go'] AS preferred_language, 31 AS age UNION ALL SELECT 'max' AS name, ['Python', 'SQL', 'Scala', 'Java', 'Kotlin'] AS preferred_language, 29 AS age UNION ALL SELECT 'yun' AS name, ['Python', 'SQL'] AS preferred_language, 28 AS age ) SELECT name, prefer_lang, age FROM example_data, UNNEST(example_data.preferred_language) as prefer_lang WHERE prefer_lang = 'Julia'
응용
- ARRAY에 특정 값이 있는지 확인하고 싶은 경우
- WHERE 절에서 UNNEST로 풀고, IN 사용
- Scala를 선호하는 사람을 찾고 싶은 경우
WITH example_data AS( SELECT 'kyle' AS name, ['Python', 'SQL', 'R', 'Julia', 'Go'] AS preferred_language, 31 AS age UNION ALL SELECT 'max' AS name, ['Python', 'SQL', 'Scala', 'Java', 'Kotlin'] AS preferred_language, 29 AS age UNION ALL SELECT 'yun' AS name, ['Python', 'SQL'] AS preferred_language, 28 AS age ) SELECT name, preferred_language, age FROM example_data WHERE 'Scala' IN UNNEST(example_data.preferred_language)
- 맨 아래에 WHERE ‘Scala’ IN UNNEST(example_data.preferred_language) 이렇게 필터링함
- ARRAY에 특정 조건이 일치하는 값을 검색하고 싶은 경우
- EXISTS 사용
- IN 조건이 여러개 있는 경우에도 사용 가능
- Go나 Scala를 선호하는 사람을 찾고싶은 경우
WITH example_data AS( SELECT 'kyle' AS name, ['Python', 'SQL', 'R', 'Julia', 'Go'] AS preferred_language, 31 AS age UNION ALL SELECT 'max' AS name, ['Python', 'SQL', 'Scala', 'Java', 'Kotlin'] AS preferred_language, 29 AS age UNION ALL SELECT 'yun' AS name, ['Python', 'SQL'] AS preferred_language, 28 AS age ) SELECT name, preferred_language, age FROM example_data WHERE EXISTS ( SELECT * FROM UNNEST(example_data.preferred_language) as prefer_lang WHERE prefer_lang IN ('Go','Scala') )
- 아래에 EXISTS ~ 부분 참고
- EXISTS 사용
- Firebase Analytics에 저장된 데이터 추출하기
- Firebase의 BigQuery Export Document
- BigQuery에서 Firebase 데이터 샘플을 제공함. https://bigquery.cloud.google.com/dataset/firebase-public-project:analytics
- 스키마는 다음과 같음
- 데이터를 미리보면 다음과 같음
- 우리가 아까 봤던 ARRAY보다 조금 더 복잡함
- event_params는 유저가 행동한 이벤트가 저장되고, user_properties는 유저의 고유 정보가 저장됨
- 두 RECORD 모두 형태는 비슷하지만, 저장된 파라미터가 다르기 때문에 2개의 UDF를 만들어서 사용함
- 회사라면 Persistent UDF 만들어서 활용하면 됨
- UDF에 대해 궁금한 경우 BigQuery UDF 사용하기 참고
# UDF for event parameters CREATE TEMP FUNCTION paramValueByKey(k STRING, params ARRAY<STRUCT<key STRING, value STRUCT<string_value STRING, int_value INT64, float_value FLOAT64, double_value FLOAT64 >>>) AS ( (SELECT x.value FROM UNNEST(params) x WHERE x.key=k) ); # UDF for user properties CREATE TEMP FUNCTION propertyValueByKey(k STRING, properties ARRAY<STRUCT<key STRING, value STRUCT<string_value STRING, int_value INT64, float_value FLOAT64, double_value FLOAT64, set_timestamp_micros INT64 >>>) AS ( (SELECT x.value FROM UNNEST(properties) x WHERE x.key=k) ); SELECT user_pseudo_id, event_name, event_timestamp, propertyValueByKey('num_levels_available', user_properties).string_value AS num_levels_available, paramValueByKey('board', event_params).string_value AS board, paramValueByKey('firebase_screen_class', event_params).string_value as firebase_screen_class from `firebase-public-project.analytics_153293282.events_20181003` LIMIT 10
- 아래의 결과가 나타남
- screen_view event_name에 board가 null인 이유는 screen_view 이벤트엔 board 파라미터가 없기 때문!
정리
- BigQuery ARRAY : 데이터 유형이 동일한 값으로 구성된 목록
- BigQuery STRUCT : 각각 유형(필수)과 필드 이름(선택사항)이 있는 정렬된 필드의 컨테이너
- ARRAY을 평면화(Flatten)하고 싶은 경우 UNNEST 사용
- UNNEST 전/후를 보면 이해하기 수월
- 과정이 이해가 안되면 GIF 참고!
- Firebase Analytics에 저장되는 데이터는 UDF를 만들어서 활용!
Reference
- How to use the UNNEST function in BigQuery to analyze event parameters in Analytics
- BigQuery Tip: The UNNEST Function
- BigQuery 중첩 및 반복 열 지정
- BigQuery 배열 다루기
- Google BigQuery 쉽게 쓰게 하기 (feat. ARRAY) : 갓꼬젯님의 글..!
카일스쿨 유튜브 채널을 만들었습니다. 데이터 분석, 커리어에 대한 내용을 공유드릴 예정입니다.
PM을 위한 데이터 리터러시 강의를 만들었습니다. 문제 정의, 지표, 실험 설계, 문화 만들기, 로그 설계, 회고 등을 담은 강의입니다
이 글이 도움이 되셨거나 의견이 있으시면 댓글 남겨주셔요.