1. Home
  2. Learn
  3. BigQuery
  4. BigQuery สำหรับงานจริง: ออกแบบตาราง เขียน Query ให้คุ้ม และเชื่อมต่อแอป
BigQuery

BigQuery สำหรับงานจริง: ออกแบบตาราง เขียน Query ให้คุ้ม และเชื่อมต่อแอป

การใช้ BigQuery การออกแบบ schema, partition, clustering, query optimization, cost control ไปจนถึงตัวอย่าง Node.js และ SQL

BigQuery สำหรับงานจริง: ออกแบบตาราง เขียน Query ให้คุ้ม และเชื่อมต่อแอปอย่างมีวินัย

BigQuery เป็นคลังข้อมูลแบบ serverless ที่เหมาะกับงาน analytics, reporting, event analysis, dashboard backend และระบบที่ต้อง query ข้อมูลจำนวนมากโดยไม่อยากดูแล infrastructure เอง แต่การใช้ให้ “เร็วและคุ้ม” ไม่ได้จบแค่เขียน SQL เป็น เพราะถ้าออกแบบตารางไม่ดีหรือ query แบบไม่ระวัง ค่าใช้จ่ายและ latency จะโตเร็วกว่าที่คิด

บทความนี้จะโฟกัสการใช้งานแบบลงมือทำจริง โดยเน้นเรื่องที่ทีมมักเจอเมื่อเริ่มใช้ BigQuery กับ production system:

  • จะออกแบบ schema แบบไหนดี
  • ควร partition และ cluster เมื่อไร
  • เขียน query ยังไงให้ scan น้อยลง
  • จะคุม cost ยังไงไม่ให้บานปลาย
  • จะเรียกใช้งานจาก Node.js ยังไงให้ปลอดภัยและ maintain ได้

BigQuery เหมาะกับงานแบบไหน

BigQuery เหมาะกับงานที่ต้องอ่านข้อมูลจำนวนมากเพื่อสรุปผล มากกว่างาน transaction แบบ row-by-row ที่ต้อง update ถี่มาก ๆ เช่น

  • dashboard สำหรับผู้บริหาร
  • event analytics จาก web/app
  • รายงานธุรกิจประจำวัน
  • aggregation จาก transaction log
  • fraud/risk analysis
  • data mart สำหรับ machine learning หรือ BI

ถ้าระบบของคุณต้องการ query ข้อมูลตามเวลา, แยกตาม customer, product, source หรือ campaign อยู่บ่อย ๆ BigQuery มักตอบโจทย์ได้ดีมาก

โครงสร้างที่ควรเข้าใจก่อนเริ่ม

โดยทั่วไป BigQuery จะมีลำดับประมาณนี้:

  • Project
  • Dataset
  • Table / View / Materialized View

ตัวอย่างชื่อเต็มของตาราง:

`my-project.analytics.orders`

แนวทางตั้งชื่อที่ดีคือทำให้ทีมเดาได้ทันทีว่าอะไรอยู่ตรงไหน เช่น

  • raw_* สำหรับข้อมูลดิบ
  • stg_* สำหรับข้อมูลที่เริ่มแปลงแล้ว
  • mart_* สำหรับตารางที่พร้อมให้ dashboard หรือ service ใช้งาน

ตัวอย่าง:

analytics.raw_events
analytics.stg_orders
analytics.mart_daily_sales

แนวทางนี้ช่วยลดความสับสนมากกว่าการโยนทุกอย่างไว้ใน dataset เดียวโดยไม่มีชั้นข้อมูล

เริ่มจาก schema ที่ตอบโจทย์ query ไม่ใช่แค่เก็บข้อมูลได้

เวลาสร้างตาราง หลายทีมเริ่มจาก “มี field อะไรบ้าง” แต่สิ่งที่สำคัญกว่าคือ “เราจะ query แบบไหนบ่อยที่สุด”

ตัวอย่างตาราง order analytics:

CREATE TABLE `my-project.analytics.orders` (
  order_id STRING NOT NULL,
  customer_id STRING NOT NULL,
  order_status STRING,
  payment_status STRING,
  channel STRING,
  total_amount NUMERIC,
  currency STRING,
  created_at TIMESTAMP NOT NULL,
  updated_at TIMESTAMP,
  completed_at TIMESTAMP,
  items ARRAY<STRUCT<
    sku STRING,
    product_name STRING,
    quantity INT64,
    unit_price NUMERIC,
    line_total NUMERIC
  >>,
  shipping STRUCT<
    province STRING,
    district STRING,
    postcode STRING
  >
)
PARTITION BY DATE(created_at)
CLUSTER BY customer_id, order_status, channel;

จุดสำคัญของตัวอย่างนี้มี 3 เรื่อง

  1. ใช้ PARTITION BY DATE(created_at) เพราะคำถามทางธุรกิจมักอิงตามช่วงเวลา
  2. ใช้ CLUSTER BY กับ field ที่ถูก filter บ่อย
  3. ใช้ nested/repeated field กับข้อมูลที่เป็นส่วนย่อยของ order เพื่อไม่ต้องกระจายเป็นหลายตารางเสมอไป

เมื่อไรควรใช้ Partition

Partition คือการแยกข้อมูลตามช่วง เช่น รายวัน รายเดือน เพื่อให้ query scan เฉพาะส่วนที่ต้องใช้

ถ้าตารางมีข้อมูลต่อเนื่องตามเวลาและ query มักมีเงื่อนไขวันที่ ควร partition แทบจะทันที

ตัวอย่าง query ที่ดี:

SELECT
  DATE(created_at) AS order_date,
  COUNT(*) AS total_orders,
  SUM(total_amount) AS revenue
FROM `my-project.analytics.orders`
WHERE DATE(created_at) BETWEEN '2026-04-01' AND '2026-04-15'
GROUP BY order_date
ORDER BY order_date;

ถ้าไม่มี partition หรือ query ไม่แตะ partition key เลย BigQuery อาจต้อง scan ตารางก้อนใหญ่มากโดยไม่จำเป็น

ตัวอย่างที่ควรหลีกเลี่ยง:

SELECT *
FROM `my-project.analytics.orders`
WHERE customer_id = 'CUST-1001';

query นี้อาจอ่านข้อมูลกว้างเกินไปถ้าตารางใหญ่มาก เพราะไม่ได้จำกัดช่วงเวลาเลย

เวอร์ชันที่ดีกว่า:

SELECT order_id, customer_id, order_status, total_amount, created_at
FROM `my-project.analytics.orders`
WHERE DATE(created_at) >= '2026-04-01'
  AND customer_id = 'CUST-1001';

เมื่อไรควรใช้ Clustering

Clustering ช่วยจัดเรียงข้อมูลตามคอลัมน์ที่ query/filter บ่อย ทำให้ BigQuery อ่านข้อมูลได้มีประสิทธิภาพขึ้น โดยเฉพาะเมื่อตารางใหญ่และมี partition อยู่แล้ว

field ที่มักเหมาะกับ clustering เช่น

  • customer_id
  • status
  • channel
  • country
  • tenant_id

แต่อย่าใส่ clustering มั่ว ๆ ทุก field เพราะจะทำให้โครงสร้างซับซ้อนเกินจำเป็น ควรเลือกเฉพาะ field ที่มีผลต่อรูปแบบ query จริง

อย่าใช้ SELECT * ถ้าไม่จำเป็น

หนึ่งในสาเหตุที่ค่า BigQuery บานคือ SELECT * กับตารางใหญ่

ตัวอย่างที่ไม่ควรใช้ใน production route:

SELECT *
FROM `my-project.analytics.orders`
WHERE DATE(created_at) = '2026-04-17';

ถึงแม้จะ filter date แล้ว แต่ถ้าตารางมี column ใหญ่ ๆ หรือ nested field เยอะ ก็ยัง scan ข้อมูลมากเกินจำเป็น

ควรเลือกเฉพาะ field ที่ใช้จริง:

SELECT
  order_id,
  customer_id,
  order_status,
  total_amount,
  created_at
FROM `my-project.analytics.orders`
WHERE DATE(created_at) = '2026-04-17';

แนวคิดนี้สำคัญมากถ้าคุณเอา BigQuery ไปเป็น backend ของ API

ใช้ Approximate Aggregation เมื่อคำตอบไม่ต้องเป๊ะ 100%

ถ้างานเป็น dashboard ที่ไม่ต้องการ exact distinct count ทุกครั้ง สามารถใช้ approximate function เพื่อให้ query เร็วและเบาลง

SELECT
  APPROX_COUNT_DISTINCT(customer_id) AS active_customers
FROM `my-project.analytics.orders`
WHERE DATE(created_at) BETWEEN '2026-04-01' AND '2026-04-15';

นี่เหมาะกับงาน monitoring, trend analysis และ exploratory analytics มากกว่างานการเงินที่ต้องตรงทุกหน่วย

ใช้ Materialized View หรือ Summary Table เมื่อ query เดิมถูกเรียกซ้ำบ่อย

ถ้า dashboard เรียก query aggregate เดิมทั้งวัน การยิง query ตรงจาก raw table ทุกครั้งอาจแพงและช้าเกินไป

ตัวอย่าง summary table รายวัน:

CREATE OR REPLACE TABLE `my-project.analytics.mart_daily_sales` AS
SELECT
  DATE(created_at) AS order_date,
  channel,
  order_status,
  COUNT(*) AS total_orders,
  SUM(total_amount) AS total_revenue,
  AVG(total_amount) AS avg_order_value
FROM `my-project.analytics.orders`
GROUP BY order_date, channel, order_status;

จากนั้น API หรือ dashboard ไปอ่านจาก mart_daily_sales แทน

ข้อดี:

  • query ง่ายขึ้น
  • เร็วขึ้น
  • คุมต้นทุนได้ง่ายขึ้น
  • ลดภาระฝั่ง frontend/backend

รูปแบบ query ที่ควรมีในทีม

1) รายงานยอดขายรายวัน

SELECT
  DATE(created_at) AS day,
  COUNT(*) AS orders,
  SUM(total_amount) AS revenue
FROM `my-project.analytics.orders`
WHERE DATE(created_at) BETWEEN @start_date AND @end_date
  AND payment_status = 'paid'
GROUP BY day
ORDER BY day;

2) ดู conversion ตาม channel

SELECT
  channel,
  COUNTIF(order_status = 'completed') AS completed_orders,
  COUNT(*) AS total_orders,
  SAFE_DIVIDE(COUNTIF(order_status = 'completed'), COUNT(*)) AS conversion_rate
FROM `my-project.analytics.orders`
WHERE DATE(created_at) BETWEEN @start_date AND @end_date
GROUP BY channel
ORDER BY conversion_rate DESC;

3) แตก array items เพื่อดูยอดขายต่อสินค้า

SELECT
  item.sku,
  item.product_name,
  SUM(item.quantity) AS total_qty,
  SUM(item.line_total) AS revenue
FROM `my-project.analytics.orders`,
UNNEST(items) AS item
WHERE DATE(created_at) BETWEEN '2026-04-01' AND '2026-04-15'
GROUP BY item.sku, item.product_name
ORDER BY revenue DESC
LIMIT 20;

4) หาลูกค้าที่ซื้อซ้ำ

SELECT
  customer_id,
  COUNT(*) AS order_count,
  SUM(total_amount) AS lifetime_value
FROM `my-project.analytics.orders`
WHERE payment_status = 'paid'
GROUP BY customer_id
HAVING COUNT(*) >= 2
ORDER BY lifetime_value DESC;

ตัวอย่าง Node.js: query BigQuery แบบ parameterized

เวลาเชื่อม BigQuery จาก backend อย่าต่อ string query แบบตรง ๆ ถ้ามี input จากผู้ใช้ ควรใช้ parameter แทน

ติดตั้ง package:

npm install @google-cloud/bigquery

สร้าง client:

import { BigQuery } from "@google-cloud/bigquery";

const bigquery = new BigQuery({
  projectId: process.env.GCP_PROJECT_ID,
});

export default bigquery;

ตัวอย่าง query รายงานยอดขาย:

import bigquery from "./bigquery-client.js";

export async function getDailySales(startDate, endDate) {
  const query = `
    SELECT
      DATE(created_at) AS day,
      COUNT(*) AS orders,
      SUM(total_amount) AS revenue
    FROM \`my-project.analytics.orders\`
    WHERE DATE(created_at) BETWEEN @startDate AND @endDate
      AND payment_status = 'paid'
    GROUP BY day
    ORDER BY day
  `;

  const options = {
    query,
    location: "US",
    params: {
      startDate,
      endDate,
    },
  };

  try {
    const [rows] = await bigquery.query(options);
    return rows;
  } catch (error) {
    console.error("BigQuery getDailySales error:", error);
    throw error;
  }
}

ตัวอย่าง route Express ที่เรียก BigQuery

import express from "express";
import { getDailySales } from "./services/report.service.js";

const router = express.Router();

router.get("/reports/daily-sales", async (req, res, next) => {
  try {
    const { startDate, endDate } = req.query;

    if (!startDate || !endDate) {
      return res.status(400).json({
        message: "startDate and endDate are required",
      });
    }

    const rows = await getDailySales(startDate, endDate);

    res.json({
      data: rows,
    });
  } catch (error) {
    next(error);
  }
});

export default router;

จุดสำคัญคือ route ไม่ควรรู้ logic SQL ลึกเกินไป ให้แยก query ไปไว้ใน service layer เพื่อให้ทดสอบและ refactor ง่าย

ตัวอย่าง query สำหรับค้นหาข้อมูลแบบ dynamic อย่างระวัง

สมมติผู้ใช้กรองด้วย channel และ order_status

import bigquery from "./bigquery-client.js";

export async function searchOrders({ startDate, endDate, channel, orderStatus }) {
  const conditions = ["DATE(created_at) BETWEEN @startDate AND @endDate"];
  const params = { startDate, endDate };

  if (channel) {
    conditions.push("channel = @channel");
    params.channel = channel;
  }

  if (orderStatus) {
    conditions.push("order_status = @orderStatus");
    params.orderStatus = orderStatus;
  }

  const query = `
    SELECT
      order_id,
      customer_id,
      channel,
      order_status,
      total_amount,
      created_at
    FROM \`my-project.analytics.orders\`
    WHERE ${conditions.join(" AND ")}
    ORDER BY created_at DESC
    LIMIT 100
  `;

  try {
    const [rows] = await bigquery.query({
      query,
      location: "US",
      params,
    });

    return rows;
  } catch (error) {
    console.error("BigQuery searchOrders error:", error);
    throw error;
  }
}

วิธีนี้ปลอดภัยและยืดหยุ่นกว่าการประกอบ SQL ด้วย string จาก input ตรง ๆ

คุมต้นทุนอย่างไรในงานจริง

นี่คือหลักพื้นฐานที่ช่วยประหยัดได้จริง

1) Filter ด้วย partition key ให้เป็นนิสัย

อย่าปล่อย query วิ่งข้ามหลายปีถ้าโจทย์ต้องการแค่ 7 วันล่าสุด

2) เลือกเฉพาะ column ที่ใช้

ย้ำอีกครั้งว่า SELECT * เป็นศัตรูของทั้ง cost และ latency

3) สร้าง summary table สำหรับ dashboard

อย่าให้หน้า dashboard ทุกคน query raw event table ตลอดเวลา

4) จำกัดผลลัพธ์ใน API

ถ้า endpoint ใช้แค่ 100 รายการแรก ก็ใส่ LIMIT และ sorting ที่ชัดเจน

5) ระวัง cross join และ unnest ที่ไม่จำเป็น

ถ้า UNNEST(items) บนตารางใหญ่โดยไม่ filter date ก่อน query จะหนักทันที

ตัวอย่างที่ควรทำ:

SELECT
  order_id,
  item.sku,
  item.quantity
FROM `my-project.analytics.orders`,
UNNEST(items) AS item
WHERE DATE(created_at) >= '2026-04-10';

แทนที่จะ unnest ทั้งตารางก่อนแล้วค่อยคัดทีหลัง

หลุมพรางที่เจอบ่อย

เขียน query ใช้งานได้ แต่ไม่พร้อม production

หลาย query รันผ่านใน console แต่ไม่เหมาะเอาไปไว้หลัง API เพราะ

  • scan เยอะเกินไป
  • ไม่มี parameter
  • ไม่มี limit
  • ไม่มี timeout strategy ฝั่ง application
  • ไม่รู้ว่าจะ cache ได้ตรงไหน

ใช้ BigQuery แทน database transaction ทุกอย่าง

BigQuery ไม่ได้ออกแบบมาแทน OLTP database หลัก เช่น PostgreSQL หรือ MySQL ในทุกเคส

รูปแบบที่ดีคือ

  • database หลัก เก็บ transaction และ state ปัจจุบัน
  • BigQuery ใช้สำหรับ analytics, reporting, historical analysis, wide scan

schema เปลี่ยนบ่อยโดยไม่มีชั้น staging

ถ้ารับข้อมูลจากหลาย source แล้วโยนเข้า mart table ตรง ๆ เวลา source เปลี่ยน field จะพังทั้ง downstream ได้ง่าย ควรมีชั้น raw/staging คั่นไว้

ตัวอย่างการโหลดข้อมูลจากแอปเข้า BigQuery

ถ้าคุณต้องการส่ง event analytics จาก Node.js ไป BigQuery แบบ streaming insert:

import { BigQuery } from "@google-cloud/bigquery";

const bigquery = new BigQuery({
  projectId: process.env.GCP_PROJECT_ID,
});

export async function insertEvent(event) {
  try {
    await bigquery
      .dataset("analytics")
      .table("raw_events")
      .insert([
        {
          event_name: event.eventName,
          user_id: event.userId,
          session_id: event.sessionId,
          page: event.page,
          created_at: new Date().toISOString(),
          metadata: JSON.stringify(event.metadata ?? {}),
        },
      ]);
  } catch (error) {
    console.error("BigQuery insertEvent error:", error);
    throw error;
  }
}

แต่ในระบบที่ event เยอะมาก ควรคิดต่อเรื่อง batching, retry, deduplication และ ingestion architecture ให้จริงจังกว่านี้

ตัวอย่าง SQL สำหรับสร้าง event table

CREATE TABLE `my-project.analytics.raw_events` (
  event_name STRING NOT NULL,
  user_id STRING,
  session_id STRING,
  page STRING,
  metadata STRING,
  created_at TIMESTAMP NOT NULL
)
PARTITION BY DATE(created_at)
CLUSTER BY event_name, user_id;

แนวทาง review query ในทีม

ก่อน merge query หรือ service ใหม่ที่ใช้ BigQuery ควรถามคำถามพวกนี้

  • query นี้ filter partition key หรือยัง
  • เลือก column เท่าที่จำเป็นหรือยัง
  • มี summary table ที่เหมาะกว่าไหม
  • query นี้ถูกเรียกบ่อยแค่ไหน
  • input จาก user ใช้ parameterized query หรือยัง
  • ถ้าตารางโต 10 เท่า query นี้ยังไหวไหม

คำถามพวกนี้ช่วยให้ทีมไม่จบที่ “มันรันผ่าน” แต่ไปถึง “มันอยู่ใน production ได้นานแค่ไหน”

สรุป

BigQuery ใช้งานง่ายมากในวันแรก แต่ความต่างระหว่างระบบที่ “query ได้” กับระบบที่ “คุม cost ได้และโตต่อได้” อยู่ที่วินัยในการออกแบบ

หัวใจสำคัญมีไม่กี่ข้อ:

  • ออกแบบ schema ตามรูปแบบ query จริง
  • partition ตามเวลาเมื่อข้อมูลโตต่อเนื่อง
  • cluster ตาม field ที่ filter บ่อย
  • เลี่ยง SELECT *
  • ใช้ summary table เมื่อ query เดิมถูกเรียกซ้ำ
  • แยก BigQuery ออกจากงาน transaction หลัก
  • เขียน backend ให้ใช้ parameterized query และจัดการ error ให้ครบ

ถ้าทีมวางพื้นฐาน 6–7 เรื่องนี้ได้ดี BigQuery จะเป็นเครื่องมือที่ทรงพลังมากทั้งในมุม data, product และ business reporting โดยไม่ต้องแบกภาระ infrastructure หนักเกินจำเป็น

💬 Chat (ตอบเร็ว)