דילוג לתוכן

תרגול SQL ב-BigQuery 🙋

·11 דקות

כולנו מכירים את הסיטואציה הזו: בעבודה יש לנו את הכלים הכי מתקדמים, נוח לנו לעבוד והכל מעולה. אבל כשזה מגיע לפרוייקטים אישיים יש משהו אחר שמפריע; אם זה המחשב שלא עובד, סביבת העבודה או התוכנות במחשב. היום, אדבר על איך נוכל לקחת Schema ולממש Dataset פיקטיבי, להריץ SQL ב-Google BigQuery ולענות על שאלות. השאלות עליהן נענה נלקחו מתוך Arena Games - Data Analysis שבנה רם קדם ופורסמו לתרגול, וכל הקרדיט לשאלות שייך ל-UpScale Analytics.

מבנה מסד הנתונים כולל נתונים על משחקים ועונות של שחקנים:

graph TD; A[game_sessions] -->|game_id| B[games]; A -->|player_id| C[players]; A -->|session_id| D[session_details]; C -->|player_id| E[paying_method];

Datacamp #

לפני שנתחיל, אמליץ על “SQL Fundamentals” באתר Datacamp. אוסף קורסים שמלמדים לכדי ביצוע שאילתות מורכבות, עם סרטונים קצרים ותרגול לכול אורכם. השתמשתי באתר Workflowy על מנת לסכם את הקורסים.

datacamp-track

Google BigQuery #

Google BigQuery הינו בין הכלים המתקדמים בשוק לביצוע שאילתות על גבי מסדי נתונים גדולים, בצורה מהירה ונוחה. בואו נגדיר Dataset ונתחיל לבנות אותו בשלבים:

  1. ניכנס לאתר של Google Cloud, נירשם עם חשבון ה-Google שלנו.
  2. נעבור למוצר BigQuery ונגדיר Dataset חדש וריק. אני קראתי לו ״Arena״.
  3. בתוך הלינק שצירפתי מעלה, מצורף ה-Schema של מסד הנתונים. היא מותאמת למסד נתונים מסוג MSSQL, ו-BigQuery תומכים ב-״BigQuery Standard SQL״, שזו גרסה מעט שונה עם שינויים ושיפורים. לכן, נצטרך להמיר את המבנה לפורמט ש-BigQuery יכירו.
  4. העתקתי את ה-Schema שכתובה שם ל-ChatGPT (למודל GPT-4), וביקשתי ממנו לבצע את ההמרה הנדרשת לפורמט ש-BigQuery יכיר. צירפתי מטה את התוצאה, שימו לב שלפני Arena מצויין project ID בו נמצא Dataset אותו אנחנו בונים.
-- Structure for game_sessions table
CREATE TABLE `teak-ellipse-401213.Arena.game_sessions` (
    session_id INT64 NOT NULL,
    session_begin_date TIMESTAMP,
    session_end_date TIMESTAMP,
    player_id INT64,
    game_id INT64
);

-- Structure for games table
CREATE TABLE `teak-ellipse-401213.Arena.games` (
    id INT64 NOT NULL,
    game_name STRING
);

-- Structure for paying_method table
CREATE TABLE `teak-ellipse-401213.Arena.paying_method` (
    player_id INT64 NOT NULL,
    credit_card_type STRING NOT NULL,
    credit_card_number STRING NOT NULL
);

-- Structure for players table
CREATE TABLE `teak-ellipse-401213.Arena.players` (
    player_id INT64 NOT NULL,
    first_name STRING,
    last_name STRING,
    email_address STRING,
    gender STRING,
    age_group STRING,
    country STRING,
    city STRING,
    street_address STRING
);

-- Structure for session_details table
CREATE TABLE `teak-ellipse-401213.Arena.session_details` (
    session_id INT64 NOT NULL,
    action_id INT64 NOT NULL,
    action_type STRING NOT NULL,
    amount NUMERIC
);
  1. אחרי שבנינו את תבנית הטבלאות, מה שנשאר לנו זה ליצר Data Synthetic בעזרת ChatGPT. מתפיסתי חשוב שניצמד ככל הניתן לפורמט של מסד הנתונים במקור, ולכן צירפתי ל-Prompt דוגמאות, גם מהקובץ של מסד הנתונים. לאחר מכן בניתי פונקציות Python שעזרו ליצר שאילתות להכנסת המידע לטבלאות שלנו בעזרת ספריית Faker:
    1. generate_game_sessions - פונקציה שמיצרת לנו את ה-Sessions. יש לנו תאריך התחלה וסיום, שחקן ומשחק.
    2. generate_games - פונקציה שמיצרת לנו את ה-Games. יש לנו את ה-ID שלו ושמו של המשחק.
    3. generate_paying_method - פונקציה שמיצרת לנו את שיטת התשלום עבור המשחק. Faker` אפילו תומכת ביצירת מספר אשראי פיקטיבי.
    4. generate_players - פונקציה שמיצרת את השחקנים והפרטים המזהים שלהם.
    5. generate_session_details - פונקציה שמיצרת את הפרטים עבור כל Session ואת נתוני רווח-הפסד שלה.
from faker import Faker
import random

fake = Faker()

def generate_game_sessions(num_rows=100):
    query = "INSERT INTO `teak-ellipse-401213.Arena.game_sessions` (session_id, session_begin_date, session_end_date, player_id, game_id) VALUES "
    values = []
    for i in range(1, num_rows + 1):
        session_id = i
        begin_date = fake.date_time_this_decade()
        end_date = fake.date_time_between_dates(begin_date)
        player_id = random.randint(1, 100)
        game_id = random.randint(1, 4)
        values.append(f"({session_id}, TIMESTAMP('{begin_date}'), TIMESTAMP('{end_date}'), {player_id}, {game_id})")
    query += ", ".join(values)
    return query

def generate_games(num_rows=100):
    query = "INSERT INTO `teak-ellipse-401213.Arena.games` (id, game_name) VALUES "
    values = []
    for i in range(1, num_rows + 1):
        game_name = fake.word()
        values.append(f"({i}, '{game_name}')")
    query += ", ".join(values)
    return query

def generate_paying_method(num_rows=100):
    query = "INSERT INTO teak-ellipse-401213.Arena.paying_method (player_id, credit_card_type, credit_card_number) VALUES "
    values = []
    card_types = ['visa', 'mastercard', 'amex']
    for i in range(1, num_rows + 1):
        player_id = random.randint(1, 100)
        card_type = random.choice(card_types)
        card_number = fake.credit_card_number(card_type=card_type)
        values.append(f"({player_id}, '{card_type}', '{card_number}')")
    query += ", ".join(values)
    return query

def generate_players(num_rows=100):
    query = "INSERT INTO `teak-ellipse-401213.Arena.players` (player_id, first_name, last_name, email_address, gender, age_group, country, city, street_address) VALUES "
    values = []
    for i in range(1, num_rows + 1):
        first_name = fake.first_name()
        last_name = fake.last_name()
        email = fake.email()
        gender = random.choice(['Male', 'Female'])
        age_group = random.choice(['10-21', '21-30', '31-40', '41-50', '51-60'])
        country = fake.country()
        city = fake.city()
        address = fake.street_address()
        values.append(f"({i}, '{first_name}', '{last_name}', '{email}', '{gender}', '{age_group}', '{country}', '{city}', '{address}')")
    query += ", ".join(values)
    return query

def generate_session_details(num_rows=100):
    query = "INSERT INTO `teak-ellipse-401213.Arena.session_details` (session_id, action_id, action_type, amount) VALUES "
    values = []
    action_types = ['gain', 'loss']
    for i in range(1, num_rows + 1):
        session_id = random.randint(1, 100)
        action_id = i
        action_type = random.choice(action_types)
        amount = round(random.uniform(100, 1000), 2)
        values.append(f"({session_id}, {action_id}, '{action_type}', {amount})")
    query += ", ".join(values)
    return query

# Function to get all the insert queries
def get_insert_queries(num_rows=100):
    return {
        'game_sessions': generate_game_sessions(num_rows),
        'games': generate_games(num_rows),
        'paying_method': generate_paying_method(num_rows),
        'players': generate_players(num_rows),
        'session_details': generate_session_details(num_rows)
    }

# Get all insert queries
insert_queries = get_insert_queries(100)

# File path where you want to save the queries
file_path = "insert_queries.sql"

# Open the file in write mode
with open(file_path, 'w') as file:
    # Iterate over each table and its corresponding query
    for table, query in insert_queries.items():
        # Write the comment and the query to the file
        file.write(f'-- Insert data into {table}\n')
        file.write(query)
        file.write('\n\n')
  1. אחרי שנריץ את הקוד הזה, נקבל 5 שאילתות שיישמרו תחת קובץ בשם ״insert_queries.sql״. את השאילתות נריץ ב-BigQuery ונהיה מוכנים להתחיל לענות על השאלות.
-- Insert data into game_sessions
INSERT INTO `teak-ellipse-401213.Arena.game_sessions` (session_id, session_begin_date, session_end_date, player_id, game_id) VALUES (1, TIMESTAMP('2022-04-18 07:48:12')

-- Insert data into games
INSERT INTO `teak-ellipse-401213.Arena.games` (id, game_name) VALUES (1, 'letter')

-- Insert data into players
INSERT INTO `teak-ellipse-401213.Arena.players` (player_id, first_name, last_name, email_address, gender, age_group, country, city, street_address) VALUES (1, 'Misty', 'Harper', 'melissawarner@example.net', 'Female', '41-50', 'Western Sahara', 'Howardstad', '53600 Craig Key')

-- Insert data into paying_method
INSERT INTO `teak-ellipse-401213.Arena.paying_method` (player_id, credit_card_type, credit_card_number) VALUES (2, 'visa', '4812645610428841')

-- Insert data into session_details
INSERT INTO `teak-ellipse-401213.Arena.session_details` (session_id, action_id, action_type, amount) VALUES (36, 1, 'gain', 890.87)

אחרי שהכנסנו והגדרנו את מסד הנתונים, נוכל להתחיל לתשאל שאילתות.

bigquery-overview

שאלות #

בפתרון השאלות אכתוב הערות מרחיבות על דרך הפתרון שלי כשארגיש צורך. במידה ותרצו לראות תפיסה נוספת לפתרונן והסבר מעמיק יותר, אמליץ לכם לצפות ב-Livestream של חברי הטוב Amit Grinson שפתר את אותן השאלות לפני מספר חודשים.

שאלה ראשונה - Payment Ranking #

Create a report that displays for each player a single payment method, according to the following preference: American Express, Mastercard, Visa. That is, if the player has an American Express payment method, we will display it. Otherwise, we will display Mastercard, and if none of the above, we will display Visa.

בפתרון שאלה זו השתמשתי ב-()ROW_NUMBER שעזר לי למספר עבור כל PARTITION של השחקנים את סדר כרטיסי האשראי. באמצעות המיון, יכלתי בשאילתה עצמה לסנן על המיקום הראשון של המיון payment_index, ובכך לקבל את אמצעי התשלום של השחקן על פי הסדר שהתבקש.

WITH PAYMENT_RANK AS (
  SELECT 
    player_id,
    credit_card_number,
    credit_card_type,
    ROW_NUMBER() OVER (PARTITION BY player_id ORDER BY CASE credit_card_type 
                                              WHEN "visa" THEN 1
                                              WHEN "mastercard" THEN 2
                                              WHEN "amex" THEN 3
                                              ELSE 4 END) AS payment_index
  FROM `teak-ellipse-401213.Arena.paying_method`
)

SELECT
  payment_rank.player_id,
  players.email_address,
  payment_rank.credit_card_type,
  payment_rank.credit_card_number
FROM PAYMENT_RANK payment_rank
JOIN `teak-ellipse-401213.Arena.players` players
  ON payment_rank.player_id = players.player_id
WHERE payment_rank.payment_index = 1;

Q1
#

שאלה שנייה - Payment Grouping #

Create a report that displays the number of players who carry each type of card. Break it down by each gender and age group.
מה שהיה מעניין בשאלה הזו היא יצירת ה-`PIVOT`, כאשר לפני ה-`FOR` נכתוב איזה פעולה מסכמת נרצה לעשות, ולאחריו נכתוב עבור מה.
-- Question Number 2: Payment Grouping
WITH players_payment AS (
  SELECT
    players.player_id AS player_id,
    gender,
    credit_card_type,
    age_group
  FROM `teak-ellipse-401213.Arena.paying_method` AS payment
  JOIN `teak-ellipse-401213.Arena.players` as players
    ON payment.player_id = players.player_id
)

SELECT * FROM players_payment
PIVOT (
  COUNT(DISTINCT player_id) 
  FOR credit_card_type IN ('visa', 'mastercard', 'amex')
)

Q2

שאלה שלישית - Games per Session #

Display the number of sessions for each game. Rank the output according to the number of game sessions, from highest to lowest.
-- Question Number 3: Games per Session
WITH session_rank AS (
  SELECT 
    game_name, 
    COUNT(game_id) AS num_sessions
  FROM `teak-ellipse-401213.Arena.game_sessions` as game_sessions
  JOIN `teak-ellipse-401213.Arena.games` as games
  ON game_sessions.game_id = games.ID
  GROUP BY game_name
)

SELECT
  game_name,
  num_sessions,
  RANK() OVER (ORDER BY num_sessions DESC) AS row_n
FROM session_rank
ORDER BY num_sessions DESC

Q3

שאלה רביעית - Total Game Duration #

Rank the games according to the total amount of minutes played in each one.
-- Question Number 4: Total Game Duration
SELECT
  game_name,
  duration,
  RANK() OVER (ORDER BY duration DESC) AS row_n
FROM (
  SELECT 
    game_id,
    SUM(TIMESTAMP_DIFF(session_end_date, session_begin_date, MINUTE)) AS duration
  FROM `teak-ellipse-401213.Arena.game_sessions` as game_sessions
  GROUP BY game_id
) AS games_durations
JOIN `teak-ellipse-401213.Arena.games` as games
ON games_durations.game_id = games.ID
ORDER BY duration DESC

Q4

שאלה חמישית - Duration per Age Group #

For each age-group display the game in which most time was spent
-- Question Number 5: Duration per Age Group
WITH games_durations AS (
  SELECT 
    age_group,
    game_name,
    SUM(TIMESTAMP_DIFF(session_end_date, session_begin_date, MINUTE)) AS duration
  FROM `teak-ellipse-401213.Arena.game_sessions` as game_sessions
  JOIN `teak-ellipse-401213.Arena.players` as players
    ON game_sessions.player_id = players.player_id
  JOIN `teak-ellipse-401213.Arena.games` as games
    ON game_sessions.game_id = games.ID 
  GROUP BY age_group, game_name
)

SELECT
  age_group,
  game_name,
  duration
FROM (
  SELECT
    age_group,
    game_name,
    duration,
    ROW_NUMBER() OVER (PARTITION BY game_name ORDER BY duration DESC) AS rank
  FROM games_durations
)
WHERE rank = 1;

Q5

שאלה שישית - Balance per Game #

Display the balance throughout each game session.
-- Question Number 6: Balance per Game
SELECT
  session_id,
  action_id,
  action_type,
  amount,
  SUM(
    CASE 
      WHEN action_type = "gain" THEN amount
      ELSE -amount
    END
  ) OVER (ORDER BY session_id, action_id) AS total_amount
FROM `teak-ellipse-401213.Arena.session_details` AS session_details
ORDER BY session_id, action_id

Q6

שאלה שביעית - Action Type Stats #

How many game sessions ended with a profit, how many game sessions ended with a loss, and how many ended in a draw?
האמת שאני מעדיף להימנע בכתיבה בגוף השאילתה ערכים של עמודות. ייתכן שיש פה מקרה קלאסי לצורך של חיבור Python על מנת לוודא את ערכי עמודת `action_type`. לשם העניין שלנו, עשיתי בדיקה בשאילתה נפרדת.
-- Question Number 7: Action Type Stats
-- Check distinct values of 'action_type'
SELECT DISTINCT action_type
FROM `teak-ellipse-401213.Arena.session_details` AS session_details;

SELECT 
  COUNTIF(action_type = 'gain') AS profit_total,
  COUNTIF(action_type = 'loss') AS loss_total
FROM `teak-ellipse-401213.Arena.session_details` AS session_details;

Q7

שאלה שמינית - Game Sessions Stats #

How many game sessions ended with a profit, how many game sessions ended with a loss, and how many ended in a draw. Break down the result for each gender and age group.
-- Question Number 8: Game Sessions Stats
WITH game_stats AS (
  SELECT
    gender,
    age_group,
    action_type,
    game_id
  FROM`teak-ellipse-401213.Arena.session_details` AS session_details
  JOIN`teak-ellipse-401213.Arena.game_sessions` AS game_sessions
    ON session_details.session_id = game_sessions.session_id
  JOIN`teak-ellipse-401213.Arena.players` AS players
    ON game_sessions.player_id = players.player_id
)

SELECT * FROM game_stats
PIVOT (
  COUNT(DISTINCT game_id)
  FOR action_type IN ('gain', 'loss')
)
ORDER BY gender, age_group

Q8

שאלה תשיעית - Total Profit/Loss for each player #

What is the total profit/loss amount for each player?
-- Question Number 9: Total Profit/Loss for each player
SELECT 
  player_id,
  SUM(profit) AS total_gain_loss
FROM (
  SELECT
    players.player_id,
    IF(action_type = 'gain', amount, -amount) AS profit,
  FROM`teak-ellipse-401213.Arena.session_details` AS session_details
  JOIN`teak-ellipse-401213.Arena.game_sessions` AS game_sessions
    ON session_details.session_id = game_sessions.session_id
  JOIN`teak-ellipse-401213.Arena.players` AS players
    ON game_sessions.player_id = players.player_id
)
GROUP BY player_id

Q9

שאלה עשירית - House Profit #

When a player wins, the house loses, and when a player loses, the house wins. Based on the available information, is the company currently in profit or loss?
בהתחלה לא הבנתי איך אעשה את השאלה הזו, והתחלתי לבנות תתי-שאילתות. אחרי כמה זמן הבנתי שמדובר על מקרה קלאסי של פעולות מסכמות עם `CASE` בתוכן.
-- Question Number 10: House Profit
SELECT
  SUM(CASE WHEN action_type = 'loss' THEN amount ELSE 0 END) AS house_gains,
  -SUM(CASE WHEN action_type = 'gain' THEN amount ELSE 0 END) AS house_losses,
  SUM(CASE WHEN action_type = 'loss' THEN amount ELSE 0 END) - 
  SUM(CASE WHEN action_type = 'gain' THEN amount ELSE 0 END) AS overall_gain_loss
FROM `teak-ellipse-401213.Arena.session_details`;

Q10

שאלה אחת עשר - House Earnings by Quarters #

Present the company's profits/losses by year and quarter.
בשאילתה הזו לראשונה שמתי לב ל-Feature ייחודי ב-BigQuery, אפשרות לא לחזור בתוך ה-`GROUP BY` על הפעולות המסכמות מתוך ה-`SELECT`. מדובר על מאפיין חכם שלא הכרתי שקיים, והופך את הקוד לקריא יותר.
-- Question Number 11: House Earnings by Quarters
SELECT
  EXTRACT(YEAR FROM session_begin_date) as year,
  EXTRACT(QUARTER FROM session_begin_date) as quarter,
  SUM(CASE WHEN action_type = 'loss' THEN amount ELSE 0 END) AS house_gains,
  -SUM(CASE WHEN action_type = 'gain' THEN amount ELSE 0 END) AS house_losses,
  SUM(CASE WHEN action_type = 'loss' THEN amount ELSE 0 END) - 
  SUM(CASE WHEN action_type = 'gain' THEN amount ELSE 0 END) AS overall_gain_loss
FROM `teak-ellipse-401213.Arena.session_details` AS session_details
JOIN `teak-ellipse-401213.Arena.game_sessions` AS game_sessions
  ON session_details.session_id = game_sessions.session_id
GROUP BY year, quarter -- using aliases in GROUP BY, unique to BigQuery
ORDER BY year, quarter

Q11

שאלה שניים עשר - Best / Worst 3 Months #

Present the company's top 3 best and worst months (in terms of profit and loss).
החלטתי לפצל את השאילתה לשלושה חלקים; בחלק הראשון לשלוף את הנתונים כמו בשאלה הקודמת. לאחר מכן, להוסיף מספור, ובחלק השלישי לשלוף את המספורים הרלוונטיים.
-- Question Number 12: Best / Worst 3 Months
  WITH MonthEarnings AS (
  SELECT
        EXTRACT(YEAR FROM session_begin_date) as year,
        EXTRACT(MONTH FROM session_begin_date) as month,
        SUM(CASE WHEN action_type = 'loss' THEN amount ELSE 0 END) AS house_gains,
        -SUM(CASE WHEN action_type = 'gain' THEN amount ELSE 0 END) AS house_losses,
        SUM(CASE WHEN action_type = 'loss' THEN amount ELSE 0 END) - 
        SUM(CASE WHEN action_type = 'gain' THEN amount ELSE 0 END) AS overall_gain_loss
      FROM `teak-ellipse-401213.Arena.session_details` AS session_details
      JOIN `teak-ellipse-401213.Arena.game_sessions` AS game_sessions
        ON session_details.session_id = game_sessions.session_id
      GROUP BY year, MONTH
      ORDER BY overall_gain_loss
  ),

  RankedResults AS (
    SELECT
      year,
      month,
      house_gains,
      house_losses,
      overall_gain_loss,
      ROW_NUMBER() OVER (ORDER BY overall_gain_loss ASC) as asc_row_num,
      ROW_NUMBER() OVER (ORDER BY overall_gain_loss DESC) as desc_row_num
    FROM MonthEarnings
  )

SELECT
  year,
  month,
  house_gains,
  house_losses,
  overall_gain_loss,
  CASE 
    WHEN asc_row_num <= 3 THEN CONCAT('Loss bottom ', CAST(asc_row_num AS STRING))
    WHEN desc_row_num <= 3 THEN CONCAT('Gain bottom ', CAST(desc_row_num AS STRING))
    ELSE NULL
  END as indication
FROM RankedResults
WHERE asc_row_num <= 3 OR desc_row_num <= 3

Q12

סיכום #

אני ממליץ מאוד להשתמש בכלי BigQuery, לפרוייקטים אישיים וללמידה. אישית למדתי מהפרוייקט הזה לא מעט, ואני מקווה שהצלחתי להעביר לכם את הייחודיות ויכולות כלי זה.