สรุปคอร์ส : Achieving Advanced Insights with BigQuery จาก Coursera
สวัสดีทุกๆ คนครับ วันนี้เราจะมาต่อกันกับคอร์สที่ชื่อว่า Achieving Advanced Insights with BigQuery ซึ่งนับว่าเป็นคอร์สที่ 3 ใน 4 คอร์ส จาก From Data to Insights with Google Cloud Platform Specialization แล้วนะ และเหมือนเช่นเคย เพื่อนๆ ยังสามารถย้อนกลับไปอ่านบทความที่แอดได้เขียนไว้ตั้งแต่คอร์สแรกตามลิงค์ด้านล่าง
- Course 1 : Exploring and Preparing your Data with BigQuery
- Course 2 : Creating New BigQuery Datasets and Visualizing Insights
โดยคอร์สนี้จะเกี่ยวกับการใช้ SQL, BigQuery Schema concept และวิธีการทำงานเพื่อให้ได้ Advanced Insights รวมถึงการ Unpack nested fields จาก ARRAY และ STRUCTs อีกด้วย เพื่อไม่เป็นการเสียเวลา เรามาเริ่มจาก Module 10 ไปพร้อมๆ กันเลย
Module 10 : Advanced Functions and Clauses
แอดอยากให้ลองทบทวน Functions บน SQL ที่เราได้เรียนไปก่อนหน้านี้กันสักนิด
- การทำงานกับ String โดยการใช้
FORMAT()
- Aggregation Functions
SUM(), COUNT(), AVG(), MAX()
… - การปรับเปลี่ยน Data Type
CAST()
- การทำงานกับ Date
PARSE_DATETIME()
ทีนี้เราจะมาต่อที่ Functions อื่นๆ กันบ้าง
Statistical Functions
Standard Deviation
ใน BigQuery เราสามารถเขียน SQL เพื่อคำนวนหาค่าสถิติได้ เช่น ค่า Standard Deviation หรือที่เราเรียกกันว่า ส่วนเบี่ยงเบนมาตรฐาน โดยใช้ Syntax แบบนี้ STDDEV()
ส่วนเบี่ยงเบนมาตรฐาน ในทางสถิติศาสตร์ คือ การวัดการกระจายแบบหนึ่งของกลุ่มข้อมูล
ลองมารันใน bigquey-public-data กันดีกว่า โดยเราจะเลือก dataset ที่ชื่อว่า irs_990_2017

สมมุติว่าเราต้องการจะหาส่วนเบี่ยงเบนมาตรฐาน เพื่อดูการกระจายตัวของจำนวนพนักงาน ใน Field ที่ชื่อว่า noemplyeesw3cnt
SELECT STDDEV(noemplyeesw3cnt) AS stdev_employee_count FROM `bigquery-public-data.irs_990.irs_990_2017`
เท่านี้ก็ได้ส่วนเบี่ยงเบนมาตรฐานกันแล้ว

Correlation
หากเราต้องการหาค่า Correlation เพื่อดูความสัมพันธ์ของตัวแปร ใน BigQuery ก็ทำได้เช่นกัน โดยใช้ Syntax หน้าตาแบบนี้ CORR(Field1, Field2)
และเจ้า Correlation หรือที่เรียกกันว่า ค่าสหสัมพันธ์ เป็นค่าที่ใช้ในการดูทิศทางความสัมพันธ์ระหว่างตัวแปร 2 ตัว โดยมี Correlation Coefficient (r) หรือ ค่าสัมประสิทธิ์สหสัมพันธ์ เป็นตัวบอกถึงความสัมพันธ์นี้
โดยจะมีค่าอยู่ระหว่าง -1.0 ถึง +1.0 เราสามารถวิเคราะห์ออกมาได้ 3 รูปแบบ คือ ค่าใกล้ 1.0 หมายความว่าตัวแปรทั้ง 2 ตัวมีความสัมพันธ์เชิงบวก (Positive correlation), ค่าใกล้ -1.0 หมายความว่าตัวแปรทั้ง 2 ตัวมีความสัมพันธ์ในเชิงลบ (Negative correlation) และค่าเข้าใกล้ 0 แสดงว่าตัวแปรทั้ง 2 ตัวไม่มีความสัมพันธ์กันเลย
นอกจากทิศทางความสัมพันธ์ +/- ยังสามารถบอกถึงระดับความสัมพันธ์ได้อีกด้วย เช่น ตัวแปรสองตัวที่มีความสัมพันธ์มากหรือน้อย ภาษาอังกฤษเราใช้คำว่า magnitude หรือ strength ยิ่งค่า correlation มีค่าเข้าใกล้ |1| มากเท่าไหร่ (absolute) ความสัมพันธ์จะยิ่งสูงมากขึ้นเท่านั้น ในกรณีที่ r = |1| เราเรียกว่า “perfect correlation” มาลองดูเกณฑ์การวัดระดับค่า r ที่นักสถิตินิยมใช้ในตารางด้านล่าง (อ้างอิงจาก DataRockie)
Absolute r | Interpretation |
0.1 – 0.3 | small |
0.3 – 0.5 | medium |
0.5 – 1.0 | large |
หากว่าวันนี้เราอยากจะหาความสัมพันธ์ระหว่างรายได้ totprgmrevnue
กับรายจ่าย totfuncexpns
SELECT CORR(totprgmrevnue, totfuncexpns) AS corr_rev_expense FROM `bigquery-public-data.irs_990.irs_990_2017`
จะเห็นได้ว่า Correlation ระหว่างรายได้กับรายจ่าย เข้าใกล้ 1.0 หมายถึง ยิ่งรายได้มากก็จะมีรายจ่ายมากตามไปด้วย

Approximate Aggregate Functions
เจ้า Function นี้ จะช่วยให้เราประมาณค่าจำนวนของ Data ที่มีอยู่ใน Data set ได้ โดยจะมี Syntax ตามนี้ APPROX_COUNT_DISTINCT()
เราลองมาใช้ Function นี้เปรียบเทียบกับ COUNT(DISTINCT)
กันดู
SELECT APPROX_COUNT_DISTINCT(ein) AS approx_count, #นับแบบคร่าวๆ COUNT(DISTINCT (ein)) AS count #นับโดยละเอียด FROM `bigquery-public-data.irs_990.irs_990_2017`
จะเห็นว่าผลที่ได้มีความแตกต่างกันนิดหน่อย

ข้อดีของมันอยู่ที่เมื่อเราต้องทำงานกับ Data จำนวนมากหลาย Petabytes หากเราใช้ COUNT(DISTINCT)
ต้องใช้เวลาในการประมวลผลมาก แต่เจ้าตัว APPROX_COUNT_DISTINCT
จะประมวลผลได้เร็วกว่า ซึ่งผลที่ออกมาค่อนข้างใกล้เคียงกัน เราอาจจะพอหยวนๆ กับ Accuracy ที่ไม่จำเป็นต้องเป๊ะ แต่ได้ผลที่รวดเร็ว
ลองใช้กับ bigquery-public-data ที่ชื่อว่า github_repos ในตาราง contents ซึ่งมีขนาด 2.3 TB

ใช้ APPROX_COUNT_DISTINCT()
เพื่อนับจำนวนแถวแบบเร็วๆ ใช้เวลา 1 วินาที ในการ Run query

แต่พอลองใช้ COUNT(DISTINCT)
จะเห็นว่าใช้เวลาในการ Run query เยอะกว่ากันหลายเท่าตัว จาก 1 วินาที เป็น 4.4 วินาที

Window Functions
หลังจากเรารู้เกี่ยวกับ Standard syntax ไปแล้ว ลองมาดูการเขียน SQL เพื่อทำการจัดอันดับ หรือการหาหมายเลขของ Row กันดีกว่า ลองมาดูตัวที่น่าสนใจของ Window Functions

แอดจะอธิบายเจ้าตัว RANK()
ที่ปกติมักใช้กันบ่อยๆ เรามีตารางพนักงาน ที่มี Field อยู่ 3 Fields คือ
- firstname
- department
- startdate

โจทย์ของเราก็คือ เราอยากจะจัดลำดับอายุของพนักงาน โดยแบ่งตาม Department
- ขั้นตอนแรก เราต้องแบ่งตารางออกเป็น 2 ส่วน แยกตามแผนก คล้ายๆ กับการทำ Group By ที่เรียกว่า
PARTITION BY
- ขั้นที่สอง ทำการเรียงลำดับตามอายุงาน คือเรียงตาม startdate โดยการทำ
ORDER BY
- ขั้นที่สาม ใส่หมายเลขอันดับเข้าไปด้วย
RANK()
RANK()
จากสามขั้นตอนด้านบน การใช้ RANK()
มีส่วนประกอบอยู่ 3 ส่วน คือ
RANK() OVER
PARTITION BY
ORDER BY

Syntax ก็จะหน้าตาแบบนี้
SELECT firstname, department, startdate, RANK() OVER ( PARTITION BY department ORDER BY startdate ) AS rank FROM Employees
User-Defined Function (UDF)
ใน BigQuery ก็สามารถเขียน Function เองได้ตามที่เราต้องการ เหมือนกับภาษาอื่นๆ เช่น R, Python โดยจะมี Syntax หน้าตาแบบนี้
CREATE TEMPORARY FUNCTION
: เป็นการบอกว่าเราจะสร้าง Function ใหม่ และกำหนดว่าจะรับ Parameters กี่ค่า มีได้ตั้งแต่ 0 เป็นต้นไปRETURNS [data_type]
: เป็นการเจาะจงว่าผลลัพธ์จะคืนค่าเป็น Data type ประเภทไหนLANGUAGE [language]
: เป็นการเจาะจงว่า Function นี้เป็นของภาษาอะไรAS [external_code]
: เป็นการบอกว่า Function นี้จะรันด้วย code แบบไหน
แอดลองสร้างตารางง่ายๆ ขึ้นมา 1 ตาราง ใน Google sheet แล้ว Import table เข้าไป โดยมีรายชื่อคนแค่ Field เดียว

ทีนี้เรามาลองเขียน Function Greeting เพื่อให้ ออกมาเป็น Hello, ชื่อคน เรียงกันไปเรื่อยๆ
CREATE TEMPORARY FUNCTION greeting (a STRING) RETURNS STRING LANGUAGE js AS """ return "Hello, " + a + "!" """;
เมื่อ Create Function เรียบร้อยแล้วลองใช้ SQL รันออกมาว่าได้ผลแบบที่ต้องการมั้ย
SELECT greeting(string_field_0) AS everyone FROM `propane-atrium-285914.friends.Test`

ข้อเสียของการใช้ User-Defined Function คือ Query สามาราถใช้พร้อมกันได้แค่ 6 Query เท่านั้น จะเห็นว่ากระทบต่อ Performance อย่างชัดเจน เพราะอย่างที่บอกไปในบทความก่อนๆ โดยปกติแล้วถ้า Query ทั่วไป จะรันพร้อมกันได้ถึง 50 Query
Sub-query
Sub-query คือ การเขียน Query ซ้อน Query โดยจะประมวลผลเรียงจาก Query ที่อยู่ด้านในสุดก่อน แล้วค่อยเรียงการประมวลผลออกมาตามลำดับ
ประโยชน์ของ Sub-query
- สามารถสร้างตารางชั่วคราวเพื่อใช้ในการ Query ต่อไปได้จากจากตารางที่สร้างขึ้น
- ทำให้เขียน Query เพื่อตอบโจทย์ในส่วนที่ซับซ้อนมากขึ้น
ลองใช้ Sub-query เพื่อแก้ปัญหาดูสักเคส แอดใช้ bigquery-public-data ที่ชื่อว่า stackoverflow ในตาราง posts_questions เป็นข้อมูลเกี่ยวกับ posts ต่างๆ ที่คนเข้ามาถามกันใน stackoverflow

สังเกตจาก Schema จะเห็นว่ามีข้อมูล ค่อนข้างหลาย Fields

ตัวอย่างข้อมูลหน้าตาจะประมาณนี้ (จริงๆ แล้วยังมีอีกหลาย Fields แต่ว่า Text จาก Fields title, body มันกินพื้นที่ไปเยอะเลย)

โจทย์ของเราคือ อยากได้ข้อมูล Post ที่เกี่ยวกับ SQL หรือ BigQuery ที่มียอด views มากกว่า 1,000 ลองมาเขียนโค้ดกันเลย
ขั้นตอนแรกแอดจะหา id ของ Posts ซึ่งในที่นี้เป็นตัว Primary key จากการใช้ WHERE
เพื่อ Filter ค่าที่เราจะเอา และใช้ LIKE
เข้ามาช่วย
SELECT id FROM `bigquery-public-data.stackoverflow.posts_questions` WHERE LOWER(title) LIKE '%sql% #เลือกชื่อ Post ที่มีคำว่า sql OR LOWER(title) LIKE '%bigquery' #เลือกชื่อ Post ที่มีคำว่า bigquery
จากนั้นเราก็ทำการ SELECT
ข้อมูลทุก Fields ที่มี id ตรงตามเงื่อนไขนี้ และเพิ่มเงื่อนไขของยอด view เข้าไป
SELECT * FROM `bigquery-public-data.stackoverflow.posts_questions` WHERE id IN ( SELECT id FROM `bigquery-public-data.stackoverflow.posts_questions` WHERE LOWER(title) LIKE '%sql%' OR LOWER(title) LIKE '%bigquery') AND view_count > 1000

งงกันมั้ยเอ่ย พูดง่ายๆ ก็คือ เราเขียน Query เพื่อลด Scope ของ Data ลง โดยจะเหลือแค่ id ของ Post ที่มีคำว่า sql และ bigquery จากนั้นเขียน Query อีกชุดนึงเพื่อดึง Data ทั้งหมดของ Post id ที่ถูก Scope ลงมาจากเดิมแล้วนั่นเอง เพื่อนๆ สามารถประยุกต์ใช้ Sub-query กับการ JOIN
เพื่อเพิ่มความทรงพลังในการ Query ได้อีกด้วย
**Noted : Sub-query สามารถใช้ได้ทั้ง 3 statements SELECT , FROM, WHERE
อ่านเพิ่มเติม ที่นี่
CTE’s (Common Table Expressions)
Common Table Expressions คือ การสร้างตารางแบบชั่วคราวไว้ใช้เพื่ออ้างอิงในการ Query ซึ่งจะว่าไปแล้วก็คล้ายๆ กับ Concept sub-query ด้านบนเหมือนกัน ส่วนประกอบของ CTE’s คือ Syntax ที่ชื่อว่า WITH
และ AS
เราลองใช้โจทย์เดียวกันกับข้างบน คือ เราอยากได้ข้อมูล Post ที่เกี่ยวกับ SQL หรือ BigQuery ที่มียอด views มากกว่า 1,000
ก่อนอื่นเราสร้างตาราง Temporary ก่อนด้วยการใช้ CTE
WITH Postsql_bq #ตั้งชื่อตาราง AS ( SELECT id AS pid FROM `bigquery-public-data.stackoverflow.posts_questions` WHERE LOWER(title) LIKE '%sql%' OR LOWER(title) LIKE '%bigquery') #เงื่อนไขของตาราง ใช้ query จาก sub-query ได้เลย
หลังจากนั้นก็มา Query แต่ครั้งนี้จะเพิ่ม Syntax INNER JOIN
เข้ามา เพื่อเป็นการ JOIN Primary key ระหว่าง 2 ตาราง คือ pid ของตาราง Postsql_bq และ id ของตารางหลัก
SELECT * FROM `bigquery-public-data.stackoverflow.posts_questions` AS a INNER JOIN Postsql_bq ON a.id = Postsql_bq.pid #JOIN primary key WHERE a.view_count > 1000
ผลออกมาเหมือนกันเป๊ะ แต่ใช้เวลาเร็วกว่านิดหน่อย

ข้อดีของ CTE’s คือ เราสามารถใช้ตารางนี้ไปได้เรื่อยๆ จนกว่าเราจะปิด BigQuery ได้เลย แต่หากเป็นตารางที่ต้องใช้ในทุกๆ วัน แอดแนะนำให้ Create View จะเหมาะสมกว่าจ้า
ก่อนไป Module ถัดไป พักสายตากันสักแปป
Module 11 : Schema Design and Nested Data Structures
หลังจากที่มึนๆ กับ SQL มาสักพัก เราจะมาแวะเรียนเกี่ยวกับทฤษฎีและพัฒนาการของ Database กันสักหน่อย ลองมาดูตัว bigquery-public-data ที่ชื่อว่า irs_990_2015 ซึ่งเป็นข้อมูลทางบัญชีที่เกี่ยวกับรายรับรายจ่ายต่างๆ มาลองหาวิธีที่เหมาะสมในการออกแบบ Schema เพื่อจัดการเก็บ Data กันเลย
Traditional Relational Database Architecture

Option 1 : Add each Expense field as a New Column
เอา Dimensions ต่างๆ ขึ้นมาเป็น Fields หรือ คอลัมน์ โดยแต่ละ Record จะมีค่า ein (Employer Identification Number) ที่ไม่ซ้ำกัน

ดูแบบเผินๆ ก็ค่ดูอนข้างเป็น Structure ดีแล้ว แต่ปัญหามันอยู่ตรงที่ว่า ถ้าสมมุติในปี 2016 มีประเภทของรายจ่ายที่เพิ่มขึ้นจากปี 2015 สัก 10 Fields เราจะทำยังไงดี ? ก็ต้องไปปรับเปลี่ยน Schema เลยใช่มั้ย ? ถ้าเราจะใช้ UNION
กับตารางในปีก่อนๆ ก็จะเจอปัญหาที่คอลัมน์ไม่เหมือนกันอีก
ปัญหาหลัก คือ แล้วเมื่อไหร่ตารางจะนิ่ง บางปีอาจจะมี Fields น้อยลงหรือเพิ่มขึ้น การที่เราต้องคอยเปลี่ยนแปลง Schema ตลอดเวลา ถือว่าเป็นการออกแบบ Schema ที่แย่ ทางที่ดีคือเราต้องทำให้จำนวนของ Fields ที่เราต้องการเก็บมีความยืดหยุ่น ตอนนี้เห็นได้ชัดแล้วว่าการเก็บ Data แบบ WIDE table นั้นไม่สามารถ Scalable ได้
Option 2 : Break Out Expenses into another Lookup Table
แยกค่าใช้จ่ายแต่ละประเภทออกจากกันซะเลย แทนที่จะเก็บ Data ทุกอย่างไว้ในตารางเดียว จากตัวอย่างก็แยกออกมาเป็น 3 ตาราง
- Organization : มี Primary Key เป็น Company ID เก็บข้อมูลรายละเอียดบริษัท
- Historical Transactions : มี Primary Key เป็น Company ID และมี Foreign Key เป็น Expense Code เก็บข้อมูลรายจ่ายของบริษัท
- Code Lookup Tables : มี Expense Code เป็น Primary Key เก็บข้อมูลชนิดของรายจ่าย
ด้วยวิธีนี้ เราจะสามารถเพิ่มจำนวนของประเภทรายจ่ายได้แบบไม่ต้องกังวลเรื่อง Schema เลย เพราะเป็นการเพิ่มจำนวน Row เท่านั้น

ซึ่งกระบวนการแบบนี้ใน Database เราเรียกกันว่า Normalization เป็นส่วนหนึ่งของทฤษฎีทาง Database คือการแตกข้อมูลออกจากตารางที่เก็บข้อมูลเป็นจำนวนมากไว้ แต่ทุกๆ ตารางที่แตกออกมาต้องมีความสัมพันธ์กัน
Normalization Benefit
Scalable Individual Tables คือ เราสามารถเพิ่มจำนวนข้อมูลของแต่ละตารางได้ตามที่เราต้องการ ต่อให้ปีใหม่ๆ จะมีประเภทรายจ่ายเพิ่มขึ้นมามากน้อยแค่ไหนเราก็ไม่หวั่นนนน

Normalization Drawback
JOINs are now a Necessity คือ เมื่อแยกได้.. ก็รวมได้เหมือนกัน เราสามารถเขียน Query โดยใช้การ JOIN
เพื่อรวมข้อมูลจากตารางต่างๆ กลับมาเป็นตารางเดิมได้

BigQuery Architecture
มี 3 Key Innovations ที่สำคัญคือ
1. Column-Based Data Storage
Columns, Records และข้อมูลต่างๆ ที่จริงแล้ว ถูก compressed แยกแต่ละ columns และเก็บอยู่ที่ Massive hard drive ใน cloud เรียกว่า Google Colossus เป็น Storage layer ตัวล่าสุด ทีนี้เราลองมาดูการเก็บข้อมูลแบบ Column-Based กัน
หากเป็น Traditional Database ข้อมูลจะถูกเก็บเป็นรายแถวเหมือนกับรูปด้านซ้าย ซึ่งส่งผลให้การทำงานของ Query จำเป็นต้องไปหาทุกๆ Columns ทำให้มี Performance การ Query ค่อนข้างต่ำ
แต่ Google ได้เป็นผู้บุกเบิกการเก็บข้อมูลแบบ Column และตีพิมพ์ไว้ในชื่อ Dremel สมัยปี 2010 ทำให้เราได้วิธีการบีบอัดข้อมูลเพื่อเก็บไว้ใน Database แบบใหม่ และมี Performance การ Query ที่เร็วขึ้นมาก เพราะไม่จำเป็นต้องไปไล่หาทุก Columns กล่าวคือ
- การเก็บข้อมูลที่มีความสัมพันธ์กันจะวนลูปน้อยลง
- Columns สามารถบีบอัดข้อมูลของตัวเองได้
- สามารถเข้าไปหาข้อมูลในเฉพาะ Columns ที่เราสนใจโดยไม่ไปแตะ Columns ทั้งหมด

2. Break Apart Tables into Pieces
ตารางข้อมูลไม่ได้ถูกเก็บต่อกันไปเรื่อยๆ แบบตึกที่สูงมากๆ ตึกเดียว แต่มันถูกแยกออกเป็นตารางเล็กๆ มากมาย เมื่อข้อมูลถูกเรียกใช้จะมีการทำงานกันแบบ parallel เพื่อส่งข้อมูลกลับมายังผู้ใช้งานได้ ไม่ว่าข้อมูลในตารางจะมีมากแค่ไหนก็ไม่เป็นปัญหาเพราะจะถูกแบ่งออกภายหลังอยู่แล้ว โดยจะถูกแบ่งออกเป็น Chunks เช่น Chunks ละ 100 MB หรือบางทีอาจจะเล็กกว่านั้นก็ได้ ซึ่งเราจะเรียกแต่ละส่วนว่า Different Shard

ลองมาดูการทำงานของ BigQuery engine แบบชัดๆ กันสักหน่อย (ดูภาพปลากรอบด้านล่างตามไปด้วยน้า) การทำงานของ BigQuery จะแตกแขนงการทำงานร่วมกันออกเป็น tree of work ดูอย่าง Syntax ในภาพที่ต้องการจะเอาข้อมูล Company โดยเรียงลำดับตามชื่อ Company
ในการ Query 1 ครั้ง เช่น Q1 ถือว่าเป็นงานชิ้นหนึ่งที่ถูกส่งกระจายให้ Workers แต่ละคน ในตัวอย่างจะมี Workers ทั้งหมด 3 คน Worker 1 อาจจะทำการ SELECT
คอลัมน์ Worker 2 ทำการ JOIN
table และ Worker 3 ทำการ GROUP BY
โดยในการทำงานจะต้องเข้าไปจอง Slot ของ Worker ก่อน และหากงานที่ Worker 1 ทำนั้น ต้องการข้อมูลจาก Worker 2 งานของ Worker 1 จะถูกส่งให้ Worker 2 ได้ โดยเรียกวิธีการแบบนี้ว่า Shuffling of data หรือ Shuffle Step

- มี 2,000 Workers เพื่อรองรับการทำงานหลายๆ Query พร้อมกัน (on-demand tier) ซึ่งเราสามารถกำหนดได้จากส่วนของ user groups
- “Fairness model” เพื่อการแบ่งใช้งาน Resources อย่างเท่าเทียม
3. Store Nested Fields within a Table
นอกจาก Relational Table แล้ว BigQuery ยังสามารถเก็บข้อมูลที่มีความสัมพันธ์แบบ Parent-child อยู่ในตารางเดียวกันได้ด้วย โดยจะเป็นข้อมูลที่เก็บลึกลงไปเป็นชั้นๆ ที่เรียกว่า Nested and repeated rows
Introducing Nested and Repeated Fields
ลองย้อนดูกันตั้งแต่ Normalized concept เราแยกตารางออกมาจากตารางเดิม เพื่อเพิ่มความสามารถในการเพิ่มจำนวน Rows แทน Columns จากนั้นเราก็ Denormalized ด้วยการ JOIN ทีนี้เราลองมาดูกันว่า Repeated concept จะเป็นอย่างไร
หากลองดูในรูปด้านล่าง Repeated หน้าตาจะประหลาดๆ นิดนึง คือมีข้อมูล Sub ย่อยลงไปอีกชั้นเหมือนการกด Tab บน Keyboard จาก cities_lived (repeated) เป็น Field ที่เก็บประวัติที่อยู่ของคนคนนี้เอาไว้ว่าเคยอยู่ที่ไหนมาแล้วบ้าง

มาดูตัวอย่าง Dataset ของ Transactions ของบริษัทต่างๆ ที่เก็บข้อมูลแบบ Nested Schema

หน้าตาของ Nested Schema ก็เป็นแบบการกด Tab จริงๆ คือแหว่งเข้าไปเลย 555+ พูดง่ายๆ มันเหมือนกับการเก็บข้อมูลเฉพาะของแต่ละบริษัท โดยเก็บเป็นราย transaction ของการจ่ายเงิน การเก็บข้อมูลแบบนี้ ไม่จำเป็นต้องใช้ JOIN
เลย เพราะสามารถเก็บข้อมูลทั้งหมดย่อยลงไปได้เป็นชั้นๆ ภายใต้ Singular records เช่น บริษัท NY Association Inc. แค่ Record เดียวสามารถมี Child ได้ถึง 3 Records คือ ข้อมูลในแถบสีน้ำเงินอยู่ภายใต้ Row เดียวกัน ดังนั้นจากรูปตัวอย่าง จะมีทั้งหมด 2 Rows ได้แก่ NY Association Inc. และ ACME Co.
ประโยชน์ของ Nested Schema
- ลด Cost ของการ Joins เพราะมี Internal Joins ผ่าน child อยู่แล้ว
- หากใช้
SELECT(DISTINCT Company ID)
ในช่องสีเทาๆ จะไม่ถูกคิด Performance ในการ Run query
Working with Repeated Fields
Introducing Arrays and Structs
ARRAYs
Arrays คือ การเก็บข้อมูลรูปแบบหนึ่งเป็น ordered lists มีจำนวนของข้อมูลได้ตั้งแต่ 0 เป็นต้นไป แต่ทุกๆ ค่าใน Arrays ต้องมี Data Type เหมือนกัน โดยจะแบ่งข้อมูลด้วย comma เช่น
[ 0.1, 0.2, 0.3, 0.4] เป็น Float เหมือนกันทั้งหมด หรือจะเป็น Arrays ของผลไม้ในรูปด้านล่าง

เรามาลองสร้าง Arrays ของผลไม้บน BigQuery กันเลยดีกว่า
SELECT ['raspberry','blackberry','strawberry','cherry'] #ใส่ข้อมูลใน Array AS fruit_array #ตั้งชื่อ Array
ผลที่ได้จะออกมาหน้าตาแบบนี้ สังเกตได้ว่ามันมีแค่ 1 Row เท่านั้น หน้าตาคล้ายๆ กับตัวอย่าง Nested Schema ของ Transactions ด้านบนเลย

เราสามารถตรวจสอบขนาดของ Array หรือจำนวน items ที่อยู่ใน Array ได้ โดยใช้คำสั่งนี้
WITH fruits AS ( SELECT ['raspberry','blackberry','strawberry','cherry'] AS fruit_array ) SELECT ARRAY_LENGTH(fruit_array) AS array_size #ใช้ ARRAY_LENGTH() เพื่อนับจำนวนข้อมูลที่อยู่ใน Array FROM fruits;
ผลที่ได้จะเป็นจำนวน items ใน Array

BigQuery จะมอง Array เป็น Flattened array และมอง Field ทั่วไปเป็น Normal Field ลองดูตัวอย่าง เราจะลองเพิ่มลูกค้าเข้าไปอีกสัก 1 คน
SELECT ['raspberry','blackberry','strawberry','cherry'] AS item, 'Michael' AS customer
ผลที่ได้จะออกมาแบบนี้

Michael จะกลายเป็น Parent record ที่มี Child records เป็น item ที่เค้ามีอยู่ ซึ่งโดยปกติแล้วข้อมูลแบบนี้จะถูกแยกเป็นตาราง Customers กับตาราง Orders และตาราง Details ทีนี้เราลองมาดูกันว่าเราจะ access elements ที่อยู่ใน Array ได้ยังไง ?
UNNEST()
UNNEST คือ Query ที่ใช้กระจาย Elements ใน Array ออกมา และ Return ค่าเป็นราย Row
สมมุติว่าเราจะ UNNEST item ของคุณ Michael จากตัวอย่างด้านบนออกมาเป็น Syntax หน้าตาก็จะได้ประมาณนี้
SELECT items, customer_name FROM UNNEST(['raspberry','blackberry','strawberry','cherry']) AS items #ใช้ UNNEST ใน FROM statements CROSS JOIN (SELECT 'Michael' AS customer_name)
อธิบายอีกทีเพื่อไม่ให้งงจนเกินไป ขั้นแรกเรา SELECT items, customer_name
เพื่อให้ทั้ง 2 Fields จับคู่กันก่อน จากนั้น Unpack Array ของ items โดยใช้ UNNEST()
และใช้คู่กับ CROSS JOIN
ซึ่งเป็นการ JOIN ค่าเข้าด้วยกันทั้งหมดของ items และ customer คำสั่งนี้จะทำการ Copy ชื่อ Michael ตามไปทุก items

นอกจาก UNNEST
ก็ยังมี ARRAY_AGG
เพื่อใช้ Aggregate Arrays หรือการสร้าง Arrays นั้นเอง สมมุติว่าเราสร้างตารางผลไม้ 3 rows

จากนั้นเราอยากสร้างเป็นตะกร้าผลไม้ ให้ผลไม้ทุกอันมาอยู่ใน Array เดียวกัน ลองใช้ ARRAY_AGG
มาช่วย
SELECT ARRAY_AGG(fruit) AS fruit_basket FROM fruits;
เท่านี้เราก็ได้ตารางผลไม้แล้วจ้า

แล้วถ้าเรามีผลไม้หลายๆ ตะกร้า เราจะสามารถเลือกตะกร้าบางตะกร้าได้ไหม คำตอบคือ ได้ !! เพราะเราสามารถใช้ WHERE
ร่วมกับ UNNEST
ได้นั่นเอง
#สร้างตะกร้าใส่สินค้า 3 ตะกร้า WITH groceries AS ( SELECT ['apple','pear','banana'] AS basket UNION ALL SELECT ['carrot','apple'] AS basket UNION ALL SELECT ['water','wine'] AS basket) #Filter เอาเฉพาะตะกร้าที่มี apple SELECT ARRAY( SELECT items FROM UNNEST(basket) AS items WHERE 'apple' IN UNNEST(basket)) AS contain_apple FROM groceries;

STRUCTs
Struct คือ การเก็บข้อมูลอยู่ในรูปของ Flexible Container จะมี Concept คล้ายๆ กับ Array แต่จะต่างกันตรงที่ Struct สามารถเก็บข้อมูลได้ทุกประเภทรวมถึง Array ด้วย ลองมาสร้าง Struct ใน BigQuery กันเลย
SELECT STRUCT(31 AS age, 'Yos' AS name) #ใส่ข้อมูลลงไปใน STRUCT()
เราก็จะได้ Struct ที่มีค่า อายุ และชื่อ ซึ่งเจ้า Struct จะเก็บค่า age เป็น integer และ name เป็น string ทั้ง 2 Columns จะถูกเก็บอยู่ใน 1 Struct สังเกตได้ว่า Struct จะเก็บข้อมูลเป็น Columns จะต่างจาก Array ที่เก็บข้อมูลเป็น Rows

ลองสังเกตผลลัพธ์ด้านบนอีกที จะมีเจ้าตัว Prefix หน้าตาประหลาดที่เป็น f0_ อยู่ทั้งสอง Columns ทำไมถึงเป็นอย่างนั้น ? คำตอบก็คือ เราไม่ได้สร้าง Alias ได้ใน Query นั่นเอง ลองเพิ่ม Alias เข้าไปแล้วลองดูผลลัพธ์ใหม่ พูดง่ายๆ คือการใส่ Alias เข้าไปหลัง Query เสมือนเป็นการตั้งชื่อ STRUCT นั่นเอง

ทีนี้ลองใส่ Array ของ Skills ลงไปใน Struct กันดูบ้าง
SELECT STRUCT(31 AS age, 'Yos' AS name, ['SQL', 'Google Sheet','Google Analytics'] AS skills) #เพิ่ม Array ลงไปเป็น Value ได้ AS customers
เราก็จะได้ Struct ที่มี Array อยู่ข้างในอีกที หรือมีเจ้า Nested field ใน Struct นั่นเอง เริ่มซับซ้อนไปกันใหญ่ 555+

แล้วในทางกลับกัน Array สามารถใส่ข้อมูลที่เป็น Struct ได้มั้ย ?? คำตอบคือ ได้ !! เรียกได้ว่าซับซ้อน แล้วก็ซ้อนเงื่อนกันไปอีก เรามาลองสร้าง Array ที่มี Struct ที่มี Array กันเถอะ งงมั้ย 5555+
SELECT [ STRUCT(31 AS age, 'Yos' AS name, ['SQL', 'Google Sheet','Google Analytics'] AS skills), STRUCT(29 AS age, 'Jane' AS name, ['SQL', 'MS Excel','Alteryx'] AS skills), STRUCT(25 AS age, 'Smith' AS name, ['JavaScript', 'Google Sheet','Google Tag Manager'] AS skills) ] AS members
และผลที่ได้ก็จะเป็นแบบนี้ คือข้อมูลทั้งหมดอยู่ภายใต้ 1 Row เท่านั้น !! ลองมาทวนกันอีกที
SELECT ARRAY[ STRUCT [ARRAY], STRUCT [ARRAY], STRUCT [ARRAY] ]

ยังๆ ยังไม่พอ เราลองมา Filter member ที่มี skill JavaScript กันดีกว่า
#สร้าง ARRAY ขึ้นมาจาก query ก่อนหน้านี้ WITH member_skills AS ( SELECT [ STRUCT(31 AS age, 'Yos' AS name, ['SQL', 'Google Sheet','Google Analytics'] AS skills), STRUCT(29 AS age, 'Jane' AS name, ['SQL', 'MS Excel','Alteryx'] AS skills), STRUCT(25 AS age, 'Smith' AS name, ['JavaScript', 'Google Sheet','Google Tag Manager'] AS skills) ] AS members ) SELECT members #เลือก Array FROM member_skills AS ms #จากตารางที่สร้างขึ้นมา CROSS JOIN UNNEST(ms.members) AS members #unpack Array ของ members ออก WHERE 'JavaScript' IN UNNEST(members.skills) #Filter struct ของ members ที่มีค่า skills = 'JavaScript'

Recap Arrays and Structs
- Nested records บน BigQuery คือ Arrays ของ Structs
- ใช้การ Join ภายในตาราง แทนที่จะใช้ Concept การ Join ทั่วไปของ SQL
- UNNESTing ARRAY ของ STRUCTs นั้นคล้ายๆ กับการ join ตาราง
Module 12 : More Visualization with Google Data Studio
หลังจากในบทความก่อนหน้านี้ ทุกคนคงรู้จักกับ Data Studio กันมาพอสมควรแล้ว หรือถ้ายังอยากทบทวน Data Studio กันอีก ก็สามารถย้อนกลับไปอ่านบทความที่แอดเคยสรุปไว้ทั้ง 2 EP. เพื่อจะได้เข้าใจกันมากขึ้น อีกสัก สอง สาม สี่ รอบ ก็ได้
- สรุปคอร์ส Data Studio จาก Google Analytics Academy EP.1
- สรุปคอร์ส Data Studio จาก Google Analytics Academy EP.2
เพื่อนๆ คนไหนที่อ่านจบแล้ว หรือเคยใช้ Data Studio กันมาบ้างแล้ว ก็มาลุยกันต่อกับแอดได้เลย
Case Statements
CASE
CASE คือ การตั้ง Custom rule based เพื่อกำหนดการแสดงค่าให้ตรงกับเงื่อนไขที่เราต้องการ มักใช้เพื่อสร้าง Field หรือ Column ใหม่ โดยใช้เงื่อนไขจากข้อมูลที่มีอยู่แล้ว ลองดู Syntax กันก่อน
CASE WHEN condition1 THEN result1 #ใส่เงื่อนไขตรง condition และใส่ค่าที่จะให้แสดงใน result WHEN condition2 THEN result2 WHEN conditionN THEN resultN ELSE result #หลัง ELSE เป็นการใส่ค่าที่จะให้แสดงหากไม่ตรงกับเงื่อนไขที่กำหนดเลย END [AS alias for column name]
สมมุติว่าเราต้องการสร้าง Field ที่เป็น Grade ของนักเรียน โดยแบ่งตามจำนวนคะแนนสอบ
CASE WHEN score > 80 THEN 'A' WHEN score > 70 THEN 'B' WHEN score > 60 THEN 'C' WHEN score > 50 THEN 'D' ELSE 'F' END AS Grade
ก่อนอื่นแอดจะสร้าง Data Studio ขึ้นมาใหม่เพื่อให้ทุกคนเห็นภาพมากขึ้น โดยจะ Connect กับ Connector ชื่อ BigQuery โดย Billing Project ของ GCP โดยผูกกับอีเมลที่เราใช้สร้าง Data Studio เลือก Public Dataset เป็นเจ้า baseball และเลือกตารางที่ชื่อ schedules

แอดสร้างตารางขึ้นมาโดยดึงเอา Dimension -> year (ปีที่แข่ง), homeTeamName (ชื่อทีมเหย้า), awayTeamName (ชื่อทีมเยือน) และ attendance (จำนวนผู้ชม) ทีนี้เราอยากจะสร้าง Dimension ใหม่ขึ้นมาชื่อ Density แล้วแบ่งเกณฑ์ของจำนวน attendance เป็น Highest, High, Medium, Low จากนั้นเข้าไปที่ ADD A FIELD

ต่อไปก็เขียน CASE ได้เลย และจะมี Syntax validator อยู่ข้างล่าง เพื่อตรวจสอบความถูกต้องของ Syntax ให้ด้วย

และนี่คือผลที่ได้ หลังจากเราเพิ่ม Dimension ที่เราสร้างมาในตาราง

Module 13 : Optimizing for Performance
Avoid BigQuery Performance Pitfalls
Input / Output
ต้องคำนึงถึงจำนวน bytes ที่ Read และจำนวน bytes ที่ Write ลง Storage บน BigQuery
- กฎเหล็กที่สำคัญของการทำงานบน BigQuery คือ ไม่ควรใช้
SELECT *
แต่ควรจะSELECT
เฉพาะ Field ที่ต้องการเท่านั้น - Denormalize Schema ด้วยการใช้ Nested และ Repeated fields แทนที่จะต้องแยก Table แล้วใช้ Primary Key มา Join กัน
- ใช้ Wildcard +
TABLE_SUFFIX
สามารถย้อนกลับไปทบทวนได้ในบทความก่อนหน้านี้ คลิก
Use BigQuery native storage for the best performance !!
- การต่อ External data connections จะไม่สามารถเก็บ cached ได้ ทำให้เมื่อเรา Run query เดิม ก็ยังคงเสียเวลาเท่าเดิม เพราะต้องดึงข้อมูลจาก External data source มาใหม่ทุกครั้ง ตรงกันข้ามกับการใช้ Dataset ที่อยู่ใน BigQuery จะมีการเก็บ cached ไว้ ถ้า Run query เดิมจะใช้เวลาน้อยลง
- หาก External data นั้นเป็น Spreadsheet ข้อมูลจะเปลี่ยนได้แบบ Real-time ทำให้ข้อมูลเปลี่ยนแปลงได้ง่าย BigQuery จะสับสนว่าอันไหนคือ Last version ของ Spreadsheet กันแน่
- BigQuery มีการใช้ Performance ที่ค่อนข้างหลากหลายวิธี เมื่อเราเขียน Query ยาวๆ สักอัน เรา Filter dataset หลายๆ แบบ เช่น ใช้
WHERE
ต่างๆ จากนั้น BigQuery จะเอา Filter เหล่านี้ ไปทำสิ่งที่เรียกว่า “Predicate pushdown” คือ การสั่งให้ data source กรองข้อมูลตามที่ต้องการก่อน แล้วจึงส่งเฉพาะข้อมูลที่ต้องการกลับมาให้เรา การ pushdown where filter แบบนี้จะช่วยให้ทำงานได้เร็วขึ้น และลด load บน data source ได้

Communication between slots (shuffle)
ต้องคำนึงถึงจำนวน bytes ที่ส่งต่อไปยัง stage ถัดไป หรือ Workers คนถัดไป สิ่งที่น่าสนใจ คือวิธีการที่ Workers ที่อยู่หลังบ้านคุยกันเพื่อแบ่งการทำงานในแต่ละส่วนของ Query และลงไปยัง slot ของแต่ละ Workers สิ่งที่ทำให้การทำงานง่ายขึ้นก็คือ
- เริ่ม Filter ตารางแต่ละตารางก่อนทำการ JOINs เช่น ถ้าจำนวน Records 80% ของตารางมีค่า Null ก็ควรจะ
WHERE
Null ออกก่อน เพื่อประหยัด Slots ของ Workers ในการทำงานกับ Records ที่มีจำนวนน้อยลง ทำให้ลดเวลาในการคุยกันเพื่อส่งงานไปมาระหว่าง Workers อีกด้วย
- หลายๆ Stage อาจเจอกับ Data skew ถ้ามีการทำงานคอขวดที่ Workers บางคน สามารถแก้ไขได้เบื้องต้นด้วยการ Optimize Query (มีอธิบายต่อด้านล่าง)

Prevent Hotspots in your Data
หลีกเลี่ยงการใช้ WITH clauses ด้วย Permanent table
เมื่อเราพูดถึงการใช้ WITH เรามักพูดกันว่าจะใช้เพื่อช่วยแบ่งส่วนของ Query ที่ซับซ้อนให้ใช้งานได้ง่ายขึ้น แต่มันก็มีอีกเหตุผล คือเราไม่ต้องการนำผลลัพธ์ที่ได้จากการ Query ไปเก็บบน Permanent Storage นั่นเอง เราลองมาดู Query กัน

จากภาพด้านบน เราใช้ WITH
เพื่อจะ Define การ Joins กันของ irs_990_2015 (เก็บข้อมูลทางบัญชีของแต่ละ Organization) กับ irs_990_ein (เก็บข้อมูล Organization details) หากเรารัน Query มากกว่า 1 ครั้ง ก็เหมือนกับเรา Join แบบนี้ใหม่ทุกครั้ง วิธีแก้ปัญหาคือ เราควรจะเก็บ Query ใน Line ที่ 6 ถึง Line 13 เป็น Permanent table เลย เพื่อดึงข้อมูลมาใช้งานทีหลังได้โดยไม่ต้องเสียเวลาในการ Join ซ้ำๆ ซึ่งจะช่วยลดความยาวของ Code และช่วยลเวลาในการรัน Query ได้อีกด้วย
ข้อควรระวังในการใช้ GROUP BY
ไม่ควร GROUP BY
ด้วยการใช้ Many distinct values ยกตัวอย่าง เช่น ID Card เพราะจะทำให้มีการรันโดยละเอียดทีละ ID ซึ่งทำให้เกิดสิ่งที่เรียกว่า Forced Shuffles หรือ Forced communication steps ระหว่าง Workers ทางดีที่ควร Group ใน Scale ที่ใหญ่ขึ้น เช่น สี, จังหวัด, อำเภอ, ขนาด เป็นต้น
ระวังการเกิด Data Skew ใน Dataset
สิ่งที่ควรระวังคือการเกิดขึ้นบ่อยๆ ของค่าบางค่าใน Dataset ยกตัวอย่างเช่น สมมุติว่าเรามีข้อมูล Transaction การซื้อของ เป็นล้านๆ Records แต่ข้อมูล 99% เป็นของ Customer เพียงบางกลุ่ม เมื่อเรา Run query สิ่งที่เกิดขึ้นภายใน BigQuery จะเป็นแบบนี้
Workers 1 คนจะรับ values ทั้งหมดของ customer ที่ 1 ที่มีข้อมูลอยู่ 99% ในขณะที่ Workers คนอื่นต้องรอที่จะใช้ Slot เพื่อเสร็จสิ้นการประมวลผลข้อมูล หลังจากนั้นถึงจะรวมผลลัพธ์เข้าด้วยกันและแสดงค่าให้เราได้เห็น สิ่งนี้เราสามารถสังเกตได้จาก Query Explanation Map ที่จะแสดงค่า complete task ของแต่ละ Workers เทียบกับค่าเฉลี่ยทั้งหมดกับ Workers อื่นๆ
Query Explanation Map
Query Explanation Map คือ Features ที่ใช้แจกแจงรายละเอียดของ Query ว่ามีการทำงานกี่ Stage แต่ละ Stage มี Task อะไรบ้างใช้เวลาในการทำงานเท่าไหร่ รวมถึง Output ที่ได้ในแต่ละ Stage ด้วย
ก่อนอื่นเราลองรัน Query นี้ดู
WITH Postsql_bq AS ( SELECT id AS pid FROMbigquery-public-data.stackoverflow.posts_questions
WHERE LOWER(title) LIKE '%sql%' OR LOWER(title) LIKE '%bigquery') #### SELECT * FROMbigquery-public-data.stackoverflow.posts_questions
AS a INNER JOIN Postsql_bq ON a.id = Postsql_bq.pid WHERE a.view_count > 1000
กดไปที่ Tab ชื่อ Execution details จะเจอ Query Explanation Map ตามรูปด้านล่าง

จะเห็นค่าที่น่าสนใจ 4 ค่า
- Elapsed time: total time for the query to process
- Slot time consumed: if the query were not processed in parallel on multiple machines, how long would it take to process
- Bytes shuffled: Automatic in-memory data shuffling for massive parallel processing
- Bytes spilled to disk: If data cannot be processed in memory, how much was spilled to persistent disk (usually data skew is to blame)
ซึ่งเราสามารถ Drill down ลงไปดูแต่ละ Stage อย่างละเอียดได้จากการกดเครื่องหมาย v ข้างๆ กับ Stages

จากภาพในกรอบสีแดง จะเห็นได้ว่าแต่ละ Stages มีการบอกว่า READ อะไรจาก Stage ไหน และ WRITE อะไรเพื่อส่งต่อไปยัง Stage ไหน และบอกเวลาที่ใช้รวมถึง Output ที่ได้
Module 14 : Data Access Roles
Access Roles จะแบ่งเป็น 2 ส่วน คือ BigQuery Roles และ IAM Project Roles
BigQuery Predefined roles

IAM Project Roles

Course Summary
จบกันไปแล้วสำหรับ การรีวิวคอร์ส Achieving Advanced Insights with BigQuery จาก Coursera ทีนี้เราลองมาดู Hands-on Labs ทั้งหมดในคอร์ส ที่ทำผ่าน Qwiklabs เหมือนเดิมเลย
- Lab1 : Deriving Insights from Advanced SQL Functions
- Lab2 : Practicing Working with Nested and Repeated Fields
- Lab3 : Optimizing and Troubleshooting Query Performance
- Lab4 : Run BigQuery in a Jupyter Notebook
ลองสรุปกันอีกทีว่ารอบนี้เราเรียนอะไรไปกันบ้าง
- Module 10 : Advanced Functions and Clauses
- Module 11 : Schema Design and Nested Data Structures
- Module 12 : More Visualization with Google Data Studio
- Module 13 : Optimizing for Performance
- Module 14 : Data Access Roles
แอดว่าคอร์สนี้เนื้อหาน่าสนใจมากโดยเฉพาะ Nested Data Structures เพราะตัวแอดเองทำงานบน BigQuery โดยใช้ Dataset ของ Google Analytics ซึ่งมันมีหน้าตาแบบ Nested Data Structures พอดี เลยรู้สึกสนุกปน มึนงงเป็นพิเศษ 555+
หวังว่าบทความนี้จะมีประโยชน์ต่อทุกคนที่เข้ามาอ่านไม่มากก็น้อยนะคร้าบ รอติดตามบทความต่อไปเร็วๆ นี้
พูดคุยกันได้ที่เพจ Malonglearn – มาลองเรียน หรือติชมผ่านคอมเม้นด้านล่างเลยจ้า
