BigQuery ARRAY 속성 EXCEPT 적용, REPLACE

1. * EXCEPT

BigQuery 는 편리하게도 SELECT 문에서 전체 속성을 지정하는 Wildcard(*) 를 사용 시,
특정 속성을 제외할 수 있는 문법을 제공한다.

가령, 속성 a, b, c, d, e, f 중 a, b, d, e, f 만 선택하고 싶을 경우 (=c 제외)

--아래와 같이 일일히 속성을 지정하는 것 대신,
SELECT a, b, d, e
FROM table


--다음과 같이 * EXCEPT 를 적용하면 효율적이다
SELECT * EXCEPT(c)
FROM table

2. ARRAY 속성 관련 제한사항

그러나 * EXCEPT 는 그 편리함에도 불구하고,
제외할 속성으로 ARRAY 내 nested column 을 특정할 수 없다는 단점이 있다.

(ARRAY, UNNEST 에 대해 잘 모른다면 다음 포스팅 참고)

3. REPLACE 사용하기

가령, ARRAY 속성 b 를 가진 테이블을 UNNEST 하여
속성 a, b.1, b.2, b.3, c 중 a, b.2, b.3, c 만 선택하고 싶을 경우 (=b.1 제외)

--직관적으로는 아래와 같이 쿼리를 하고 싶지만 실행 오류가 뜬다.
SELECT * EXCEPT(b.1)
FROM table, UNNEST(b) AS b


--이 때는 REPLACE 를 적용해 다음과 같이 쿼리해야 한다.
SELECT * REPLACE (ARRAY(SELECT AS STRUCT * EXCEPT(1) FROM UNNEST(b)) AS b)
FROM table

BigQuery 여러 테이블 한 번에 쿼리

1. 테이블 구성

BigQuery 는 GA 와 연계해서 사용하는 경우,
일반적으로 GA 데이터는 일 (daily) 단위의 별개 테이블로 BigQuery에 적재된다.

events_ & events_intraday_ tables in BigQuery for GA4 (Google Analytics 4)  - Optimize Smart

출처 : Optimize Smart

가령, 위 화면 좌측 Explorer에 events_ (9) 라는 테이블은
events_20210115
events_20210114
events_20210113

events_20210107
총 9개의 일 별 테이블이 있는 것이다.

events 라는 테이블 명에 ‘_YYYYMMDD’ 형태의 Suffix 가 적용되었고,
총 테이블 개수는 괄호 안에 ‘_(9)’ 와 같이 표현된다.

2. 여러 테이블 쿼리하기

위와 같은 테이블 구성에서, 그렇다면 여러 테이블들을 어떻게 한 번에 호출할까?

여러 테이블을 일일히 UNION 하는 방식도 있겠으나,
BigQuery 에서는 보다 효율적인 문법을 제시한다.

Wildcard 와 _TABLE_SUFFIX 를 사용을 통해서다.

events 테이블 중 2021-01-09 부터 2021-01-12 까지의 테이블을 조회해야 한다고 가정해보자.

--아래와 같은 방식 대신,
SELECT *
FROM 'dbrt-ga4.analytics_207472454.events_20210109'
UNION ALL 
SELECT *
FROM 'dbrt-ga4.analytics_207472454.events_20210110'
UNION ALL 
SELECT *
FROM 'dbrt-ga4.analytics_207472454.events_20210111'
UNION ALL 
SELECT *
FROM 'dbrt-ga4.analytics_207472454.events_20210112'



-- Wildcard 와 _TABLE_SUFFIX 를 사용하는 것이 더 효율적이다.
SELECT *
FROM 'dbrt-ga4.analytics_207472454.events_202101*'
WHERE _TABLE_SUFFIX BETWEEN '09' AND '12'

참고
https://cloud.google.com/bigquery/docs/querying-wildcard-tables

BigQuery UNNEST 친절한 설명

1. ARRAY 란?

Oracle 과 같이 기존에 전통적인 RDBMS 문법을 기반으로 쿼리를 배웠을 경우,
BigQuery 에서 이런 저런 데이터를 다루다 보면 간혹 낯선 풍경과 조우하는 경우가 있다.

그 중 하나가 BigQuery 의 데이터형(data type) 중 하나인 ARRAY 이다.

BigQuery에서 배열은 데이터 유형이 동일한 0개 이상의 값으로 구성된 순서가 지정된 목록을 의미합니다.

Google BigQuery Doc

별로 와닿지 않는 정의다…
풀어서 설명을 해보도록 하겠다.

일반적으로 기업에서 BigQuery 를 사용한다고 하면,
Google Analytics (이하 GA) 와 연동해서 사용하는 경우가 많으므로
GA 데이터를 샘플 삼아 설명하도록 하겠다.

GA 는 Web 에서 고객이 남기는 행동들을 Log 형태로 수집을 하고,
수집된 raw data는 JSON 형식에서 대략 다음과 같다.

[
  {
    "visitId": "123456789",
    "visitStartTime": "1622124798",
    "totals": {
      "visits": "1",
      "hits": "1",
      "pageviews": "1"
    }
  }
]

(참고) visitStartTime 값이 왜 1622124798 인지 궁금하신 분들은 관련 포스팅을 참고

JSON 문법을 잘 모르더라도,
위 내용을 아래와 같이 직관적으로 파악할 수 있을 것이다.

👨 visitId 가 123456789 이고
🕒 1622124798 UNIX time 에 들어와서
🔢 총 방문 수를 1회, hit 수는 1회, 페이지 조회 수는 1회 기록했구나.

자, 그러면 위 JSON 데이터를 쿼리가 가능하도록 table 형태로 보면 어떨까?

2. Why UNNEST?

위 데이터를 표로 만들 경우, 결과는 다음과 같을 것이다.

visitIdvisitStartTimetotals
1234567891622124798“visits”: “1”,
“hits”: “1”,
“pageviews”: “1”
table1. UNNEST 전

자, 한 번 보자.
딱 봐도 문제가 있지 않은가? (불편 +1)

table1. 불편한 부분

totals 컬럼의 값을 펼쳐서 추가적인 컬럼으로 만들고 싶은 욕구가 생긴다.
마치 R 에서의 reshape 패키지 cast 함수처럼.

visitIdvisitStartTimetotals.visitstotals.hitstotals.pageviews
1234567891622124798111
table2. UNNEST 후


table1 에서 table2 로 변경을 해주는 작업.
이것이 바로 UNNEST 함수다.

3. UNNEST Syntax

UNNEST 의 사용 문법은 다음과 같다.

--visitId 별 페이지 조회 수

SELECT visitId, t.pageviews
FROM table1, UNNEST(totals) AS t

테이블 내 값을 컬럼으로 펼치는 과정에서 CROSS JOIN 이 들어가기 때문에,
FROM table1 뒤에 쉼표(,) 가 붙는다.

UNNEST(totals) 에 대한 alias 가 필수는 아니지만,
복잡한 JOIN 이 들어간 쿼리에서 해당 속성을 특정할 수 있어 사용을 권고한다.

외 참고 링크

Google 에서 직접 GA 샘플 데이터셋을 제공하기도 하니,
실무적인 연습이 필요한 분은 아래 링크를 참고.
Google Analytics sample dataset for BigQuery

UNIX time 이란? (= Epoch = POSIX ) & 쿼리 시 주의 사항

1. UNIX time 이란?

UNIX time 이란, 1970년 1월 1일 00:00:00 UTC 로부터 현재까지의 누적된 초(seconds) 값을 의미한다.

가령,
‘2021-05-23 09:00:00 KST’
위 시간을 UNIX time으로 표현한다면

① KST(local time)를 UTC 로 변환해서, ‘2021-05-23 00:00:00 UTC’
② UNIX time 의 기준 시간인 ‘1970-01-01 00:00:00 UTC’ 에서부터 누적 초를 계산

아래 숫자 값을 도출하게 된다.
‘1621728000’

2. 왜 UNIX time 인가?

UNIX time 은 UNIX 운영체제를 개발한 벨 연구소에서 정의한 개념이다.

Date/Timestamp 데이터형을 Numeric 데이터형으로 표현 시,
기존에 Date/Timestamp가 갖는 한계점을 해결할 수 있어 UNIX time 의 개념이 도입되었다.

Date/Timestamp 의 한계점
1. 로컬 시간대(ex. KST) 명시 필요
2. 비연속적, 비선형적인 값이므로 계산 시 변환 필요

하필 기준 시간이 1970년 1월 1일 00시 00분 00초 UTC 인 이유는,
UNIX 운영체제의 최초 출시년도가 1971년이어서 그렇다.
근방에 그럴싸한 시간을 임의로 잡은 것이다.

UNIX time 을 epoch time 또는 POSIX time 이라고도 부르는데,
이는 epoch 라는 단어가 특정 시대를 구분짓는 기준점(reference point from which time is measured = reference epoch) 이라는 의미를 가졌기 때문이고,
POSIX 는 UNIX 운영체제를 기반으로 둔 운영체제 인터페이스여서 같은 의미로 사용된다.

3. 쿼리 시 주의사항

UNIX time 을 취급할 때 쿼리 시 간과하기 쉬운 부분은, 다름 아닌 UTC 기준이라는 것이다.

하여 당신이 영국이나 스페인 같이 UTC +00:00 timezone 국가에 거주하지 않는 이상,
UNIX time 을 취급할 때는 항상 로컬 시간대로 변환하는 추가 작업을 해야 한다.
(이게 은근 까먹기 쉽다)

가령, Google Analytics 에서 수집한 고객 행동 데이터를 기반으로
접속 시간(hour)을 추출하는 쿼리를 작성한다고 하면,
다음과 같이 9시간에 해당되는 초(32,400)를 더해주어야 한다.

–BigQuery 기준
EXTRACT(HOUR FROM TIMESTAMP_SECONDS(CAST(visitStartTime+32400 AS INT64))) hh

(참고) BigQuery 의 Google Analytics 스키마에서는 시간을 UNIX time 으로 제공한다.

그외. 흥미로운 점

UNIX time 관련해서 2038년 문제라고 해서,
2038년이 되면 UNIX time 값이 32bit 시스템에서는
0 또는 음수로 표현되버리는 사태가 벌어질 것이라고 한다.
(2038년 이후에는 32bit 시스템의 허용된 숫자 표현 범위를 넘어버리게 되므로)

2038년이면 필자가 아직 살아있을 때일 것 같은데,
어떤 일이 일어날 지가 궁금하다.
해결책은 64bit 시스템으로 변경하는 것이라고 하는데 과연 시스템 교체가 수월할지 의문…

PostgreSQL ROUND 함수 데이터형

PostgreSQL

ROUND function 시 데이터형을 명시해야 정확한 값이 나온다.

(예시)

SELECT ROUND((1412/231),2)
FROM 스키마.테이블;

--결과 : 6.00
SELECT ROUND((1412/231::numeric),2)
FROM 스키마.테이블;

--결과 : 6.12
SELECT round((1412::numeric/231),2)
FROM 스키마.테이블;

--결과 : 6.12
SELECT round((1412::numeric/231::numeric),2)
FROM 스키마.테이블;

--결과 : 6.12

비교 연산자(!=)와 NULL, 발생 가능한 실수

비교 연산자에서는 NULL이 배제된다.
(반면 논리 연산자에서는 NULL이 배제되지 않는다.)

 

가령,

Query 1.

SELECT *
FROM A FULL OUTER JOIN B ON ~
WHERE A.컬럼1 = ‘값1’ OR B.컬럼2 = ‘값2’

 

위 Query 1. 결과는 아래 Query 2.  결과와 일치하지 않는다.

 

Query 2.

SELECT *
FROM A FULL OUTER JOIN B ON ~
WHERE A.컬럼1 != ‘값1’ AND B.컬럼2 = ‘값2’

UNION

SELECT *
FROM A FULL OUTER JOIN B ON ~
WHERE A.컬럼1 = ‘값1’ AND B.컬럼2 != ‘값2’

(A, B의 컬럼 차수 및 데이터형은 일치한다고 가정한다)

 

A.컬럼1 과 B.컬럼2 의 NULL 값이 결과에서 배제되기 때문에
해당 컬럼의 NULL 존재 여부에 따라서 Query 2. 의 결과 수가 Query 1.보다 적을 수도 있다.

비교 연산자에서 NULL이 배제된다는 점을 고려했을 때,
Query1 과 동일한 결과를 도출하기 위해서 올바른 Query는 다음과 같다.

 

Query 3.

SELECT *
FROM A FULL OUTER JOIN B ON ~
WHERE A.컬럼1 = ‘값1’ AND (B.컬럼2 != ‘값2’ OR B.컬럼2 IS NULL)

UNION

SELECT *
FROM A FULL OUTER JOIN B ON ~
WHERE (A.컬럼1! = ‘값1’ OR A.컬럼1 IS NULL) AND B.컬럼2 = ‘값2’

(A, B의 컬럼 차수 및 데이터형은 일치한다고 가정한다)

 

결론 : 비교 연산자와 논리 연산자를 함께 사용할 경우, NULL 처리에 유의하자!

GROUP BY 절의 ALIAS 사용 제한 시 대안

GROUP BY 절

  1. GROUP BY 절을 통해 소그룹별 기준을 정한 후, SELECT 절에 집계 함수를 사용한다.
  2. 집계 함수의 통계 정보는 NULL 값을 가진 행을 제외하고 수행한다.
  3. GROUP BY 절에서는 SELECT 절과는 달리 ALIAS 명을 사용할 수 없다.
  4. 집계 함수는 WHERE 절에는 올 수 없다. (집계 함수를 사용할 수 있는 GROUP BY 절보다 WHERE 절이 먼저 수행된다)
  5. WHERE 절은 전체 데이터를 GROUP으로 나누기 전에 행들을 미리 제거시킨다.
  6. HAVING 절은 GROUP BY 절의 기준 항목이나 소그룹의 집계 함수를 이용한 조건을 표시할 수 있다.
  7. GROUP BY 절에 의한 소그룹별로 만들어진 집계 데이터 중, HAVING 절에서 제한 조건을 두어 조건을 만족하는 내용만 출력한다.
  8. HAVING 절은 일반적으로 GROUP BY 절 뒤에 위치한다.

 

☞ 대안 :

특수한 상황에 의해 반드시 ALIAS를 GROUP BY 절에 사용해야할 경우,

      FROM 절에 Inline View를 생성 →  별칭을 제거 →  GROUP BY 실행