Big Data Analytics · บทที่ 9 จาก 13

Spark SQL และ ML Pipeline

บทนี้คือจุดที่ทฤษฎีทั้งหมดจากบทที่ 4-6 กลายเป็นเครื่องมือที่ใช้งานจริงในอุตสาหกรรม — Spark คือผู้สืบทอด MapReduce ที่แก้ปัญหาสำคัญที่สุดของมัน: การต้องเขียน/อ่านดิสก์ซ้ำทุกขั้นตอน

📚
สังเคราะห์จากเอกสาร Spark SQL/DataFrame/ML API ของ Jimmy Lin ที่ใช้ประกอบวิชา (อ้างอิงเอกสารทางการของ Apache Spark และ Databricks)

1RDD: รากฐานของทุกอย่างใน Spark

Resilient Distributed Dataset
โครงสร้างข้อมูลหลักของ Spark — เป็น collection ของ record ที่กระจายอยู่บนหลายเครื่อง immutable (แก้ไขไม่ได้ ตามหลักการ functional programming ในบทที่ 5) และ "resilient" เพราะ Spark จำ lineage (ประวัติการแปลงข้อมูล) ไว้ ถ้า partition ไหนหายไปเพราะเครื่องพัง Spark คำนวณมันขึ้นมาใหม่ได้จาก lineage โดยไม่ต้องมี replication เหมือน HDFS

RDD รองรับสอง operation หลัก: Transformation (เช่น map, filter, join — คืนค่าเป็น RDD ใหม่ และขี้เกียจ (lazy) คือยังไม่รันจริงจนกว่าจะมี action) และ Action (เช่น count, collect, show — บังคับให้ Spark รัน transformation ทั้งหมดที่ค้างอยู่และคืนผลลัพธ์จริง)

2จาก MapReduce สู่ SQL: เมื่อเราเลิกบอกทุกขั้นตอน

MapReduce เปิดทางให้ประมวลผลข้อมูลขนาดใหญ่ได้ แต่โปรแกรมเมอร์ต้องระบุเองว่าจะ Map อะไร ใช้ key ใด Shuffle ตอนไหน และ Reduce อย่างไร ต่อให้โจทย์เป็นเพียง “เลือกยอดขายของกรุงเทพฯ แล้วรวมตามสินค้า” ก็ยังต้องเขียนเส้นทางการคำนวณค่อนข้างละเอียด

SQL เปลี่ยนบทบาทของเรา จากคนบอกเส้นทางทุกแยก มาเป็นคนบอกจุดหมาย เราเขียนว่า ต้องการผลลัพธ์อะไร ส่วนระบบเลือกเองว่า ควรคำนวณอย่างไร จุดเปลี่ยนสำคัญจึงไม่ใช่เพียงไวยากรณ์ที่สั้นลง แต่คือการแยก logical plan ออกจาก physical plan

SELECT product, SUM(amount)
FROM sales
WHERE city = 'Bangkok'
GROUP BY product;
ถ้าเขียนเป็น MapReduce
Map อ่านแต่ละ record แล้วกรองเฉพาะกรุงเทพฯ จากนั้นส่ง (product, amount) ออกไป Shuffle จัดข้อมูลสินค้าเดียวกันให้อยู่กลุ่มเดียว และ Reduce รวมยอดขาย ส่วน SQL ซ่อนขั้นตอนเหล่านี้ไว้หลังภาษาที่บอกเพียงผลลัพธ์ที่ต้องการ

Relational Algebra: ภาษากลางระหว่าง SQL กับการทำงานจริง

ก่อนที่ SQL จะกลายเป็น task บนคลัสเตอร์ ระบบจะแปลงคำสั่งให้เป็นชุด operator ทาง Relational Algebra แต่ละ operator รับตารางหนึ่งหรือหลายตาราง แล้วคืนตารางใหม่ จึงต่อกันเป็นสายการคำนวณได้

SQLRelational Algebraภาพของการคำนวณแบบกระจาย
WHERESelection (σ)filter แต่ละ partition ได้อย่างอิสระ และควรทำใกล้แหล่งข้อมูล
SELECT colProjection (π)เก็บเฉพาะคอลัมน์ที่ใช้ ลดข้อมูลที่ต้องอ่านและส่ง
GROUP BYGrouping/Aggregation (γ)รวมบางส่วนในเครื่อง แล้ว Shuffle ตาม key เพื่อรวมขั้นสุดท้าย
JOINJoin (⋈)เลือก Broadcast Join, Hash Join หรือ Sort-Merge Join ตามขนาดและรูปแบบข้อมูล
ORDER BYSort (τ)แบ่งช่วง key, Shuffle แล้วเรียงข้อมูลภายในแต่ละ partition
Algebra สำคัญอย่างไร
เมื่อ operator ต่อกันได้และความหมายไม่เปลี่ยน ระบบจึงจัดรูปแผนใหม่ได้ เช่น ดัน WHERE ลงไปกรองตั้งแต่ตอนอ่านไฟล์ ตัดคอลัมน์ที่ไม่ใช้ออกก่อน Shuffle หรือเปลี่ยนลำดับ Join เพื่อให้ข้อมูลระหว่างทางเล็กลง นี่คือการใช้ Algebra เพื่อประหยัดทั้งงานและการเคลื่อนย้ายข้อมูล

เส้นทางจาก Hadoop มาถึง Spark SQL

ช่วงสิ่งที่โปรแกรมเมอร์เขียนใครวางแผนการคำนวณ
MapReduceMap, key, Shuffle และ Reduceโปรแกรมเมอร์กำหนดโครงหลักเอง
Pig / HiveDataflow script หรือ SQL-like queryระบบแปลงเป็น MapReduce หลาย job
Spark RDDสายของ transformationSpark วาง stage แต่ยังไม่รู้ความหมายของคอลัมน์มากนัก
DataFrame / Spark SQLRelational operations หรือ SQLCatalyst ใช้ schema และ Algebra ช่วยปรับ logical/physical plan
SQL ไม่ได้ลบ MapReduce ทิ้ง
SQL ซ่อนรายละเอียดการกระจายงานไว้ด้านล่าง ใน physical plan เรายังพบการกรองแบบขนาน การแบ่ง key การ Shuffle และการรวมผลอยู่ เพียงแต่ optimizer เป็นผู้ประกอบชิ้นส่วนเหล่านี้แทนเรา

3Spark SQL และ DataFrame

DataFrame คืออะไร
RDD ที่มี schema กำกับ (คล้ายตารางใน relational database) — DataFrame และ SQL ใน Spark ใช้ execution/optimization pipeline เดียวกัน (Catalyst optimizer) ดังนั้นเขียนด้วย DataFrame API หรือเขียนเป็น SQL string ก็ได้ประสิทธิภาพเท่ากัน เพราะสุดท้ายถูก compile เป็นแผนการทำงานเดียวกัน
// อ่านข้อมูลแบบ Python (PySpark)
df = spark.read \
     .format("json") \
     .option("samplingRatio", "0.1") \
     .load("/data/stuff.json")

df.write \
     .format("parquet") \
     .mode("append") \
     .partitionBy("year") \
     .saveAsTable("faster_stuff")
// เขียนแบบ Scala — เทียบเท่ากัน
val df = spark.read.format("json").load("/data/stuff.json")
val young = df.filter($"age" < 30)
young.createOrReplaceTempView("young")
spark.sql("SELECT count(*) FROM young").show()

Transformation vs Action ใน DataFrame

Transformation (lazy)Action (บังคับให้รันจริง)
filter, select, drop, join, groupBycount, collect, show, head, take
ทำไม lazy evaluation ถึงสำคัญ
เพราะ transformation ไม่รันทันที Spark จึงมองเห็น "แผนทั้งหมด" ก่อนตัดสินใจว่าจะ optimize อย่างไร (เช่น รวมหลาย filter เข้าด้วยกัน, จัดลำดับ join ใหม่ให้มีประสิทธิภาพสูงสุด) — นี่คือข้อได้เปรียบที่ MapReduce แบบดั้งเดิมไม่มี เพราะแต่ละ job แยกกันโดยสิ้นเชิง

ตัวอย่างการทำงานกับ Schema

// ให้ Spark เดา schema จาก case class
case class Person(firstName: String, lastName: String, age: Int)
val rdd = sc.textFile("people.csv")
val peopleRDD = rdd.map { line =>
  val cols = line.split(",")
  Person(cols(0), cols(1), cols(3).toInt)
}
val df = peopleRDD.toDF

// หรือกำหนด schema เอง (แม่นยำกว่า infer อัตโนมัติ)
val schema = StructType(
  StructField("firstName", StringType, false) ::
  StructField("age", IntegerType, false) :: Nil
)
val df = spark.read.format("csv").schema(schema).load("people.csv")

df.printSchema()
// root
// |-- firstName: string (nullable = true)
// |-- age: integer (nullable = false)

4Spark ML Pipeline API

แนวคิดหลัก: Transformer, Estimator, Pipeline
  • Transformer: รับ DataFrame แล้วคืน DataFrame ใหม่ (มีเมธอด transform()) เช่น Tokenizer แปลงข้อความเป็นคำ
  • Estimator: รับ DataFrame แล้ว "เรียนรู้" (fit) เพื่อคืน Transformer เช่น LogisticRegression.fit(df) คืน LogisticRegressionModel
  • Pipeline: เรียง Transformer/Estimator หลายตัวต่อกันเป็น workflow เดียว
import org.apache.spark.ml.Pipeline
import org.apache.spark.ml.feature.{HashingTF, Tokenizer}
import org.apache.spark.ml.classification.LogisticRegression

val tokenizer = new Tokenizer()
  .setInputCol("text").setOutputCol("words")

val hashingTF = new HashingTF()
  .setNumFeatures(1000)
  .setInputCol(tokenizer.getOutputCol)
  .setOutputCol("features")

val lr = new LogisticRegression()
  .setMaxIter(10).setRegParam(0.01)

val pipeline = new Pipeline()
  .setStages(Array(tokenizer, hashingTF, lr))

val model = pipeline.fit(trainingData)
val predictions = model.transform(testData)
ทำไมโครงสร้าง Pipeline ถึงสำคัญ
งาน ML จริงไม่ได้มีแค่ "train โมเดล" ขั้นตอนเดียว — ต้องแปลงข้อความเป็นตัวเลขก่อน (feature engineering) แล้วค่อย train Pipeline API ทำให้ขั้นตอนทั้งหมดถูก serialize เป็นก้อนเดียวได้ (บันทึกและโหลดกลับมาใช้ตอน inference ได้ทั้งชุด ไม่ต้องแยกเขียนโค้ด preprocessing ซ้ำสำหรับ production)

5เมื่อยุคเปลี่ยน: Spark SQL/ML ในปี 2026

แล้ว · ~2016-2018 (Spark 1.6-2.x)
ตอนนี้ · 2026 (Spark 3.x/4.x)
ต้องเลือกใช้ SQLContext หรือ HiveContext แยกกัน ขึ้นกับว่าต้องการ feature ของ Hive หรือไม่
รวมเป็น SparkSession เดียวตั้งแต่ Spark 2.0 เป็นต้นมา ลดความสับสนของ entry point หลายตัว
DataFrame เป็น API หลักสำหรับข้อมูลมี schema, RDD เป็น low-level API
Dataset[T] (Scala) รวมข้อดีของ RDD (type safety) กับ DataFrame (Catalyst optimization) เข้าด้วยกัน — Adaptive Query Execution (AQE) ปรับแผนการ query ระหว่างรันจริงได้ ไม่ใช่แค่ตอน compile
Spark ML Pipeline เป็นเครื่องมือหลักสำหรับ classical ML (logistic regression, tree-based model)
สำหรับ deep learning งานส่วนใหญ่ใช้ PyTorch/TensorFlow ร่วมกับ Spark ผ่าน library เชื่อมต่อ (เช่น Petastorm, TorchDistributor) แทนที่จะพึ่งพา MLlib ล้วน ๆ — Spark ทำหน้าที่เตรียมข้อมูลขนาดใหญ่ (feature engineering ระดับ TB) แล้วส่งต่อให้ framework ML เฉพาะทางจัดการ training จริง
คำถามซ้อมสอบ
  1. อธิบายว่าทำไม RDD ถึงเรียกว่า "resilient" และกลไก lineage ทำงานอย่างไรเมื่อ partition หายไป
  2. อธิบายความแตกต่างระหว่าง Transformation และ Action พร้อมยกตัวอย่างแต่ละแบบอย่างน้อย 3 ตัว
  3. อธิบายว่าทำไม DataFrame API และ SQL string ถึงให้ประสิทธิภาพเท่ากันใน Spark
  4. แปลงคำสั่ง SQL พื้นฐานเป็น Selection, Projection, Join และ Aggregation พร้อมอธิบายว่าระบบกระจายงานอย่างไร
  5. อธิบายความต่างระหว่าง logical plan กับ physical plan และยกตัวอย่าง selection pushdown หรือ projection pruning
  6. อธิบายความสัมพันธ์ระหว่าง Transformer, Estimator, และ Pipeline ใน Spark ML
  7. อธิบายว่าทำไมงาน deep learning สมัยใหม่มักใช้ Spark ร่วมกับ PyTorch/TensorFlow แทนที่จะใช้ MLlib ล้วน ๆ