Spark SQL และ ML Pipeline
บทนี้คือจุดที่ทฤษฎีทั้งหมดจากบทที่ 4-6 กลายเป็นเครื่องมือที่ใช้งานจริงในอุตสาหกรรม — Spark คือผู้สืบทอด MapReduce ที่แก้ปัญหาสำคัญที่สุดของมัน: การต้องเขียน/อ่านดิสก์ซ้ำทุกขั้นตอน
1RDD: รากฐานของทุกอย่างใน Spark
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;
(product, amount) ออกไป Shuffle จัดข้อมูลสินค้าเดียวกันให้อยู่กลุ่มเดียว และ Reduce รวมยอดขาย ส่วน SQL ซ่อนขั้นตอนเหล่านี้ไว้หลังภาษาที่บอกเพียงผลลัพธ์ที่ต้องการRelational Algebra: ภาษากลางระหว่าง SQL กับการทำงานจริง
ก่อนที่ SQL จะกลายเป็น task บนคลัสเตอร์ ระบบจะแปลงคำสั่งให้เป็นชุด operator ทาง Relational Algebra แต่ละ operator รับตารางหนึ่งหรือหลายตาราง แล้วคืนตารางใหม่ จึงต่อกันเป็นสายการคำนวณได้
| SQL | Relational Algebra | ภาพของการคำนวณแบบกระจาย |
|---|---|---|
WHERE | Selection (σ) | filter แต่ละ partition ได้อย่างอิสระ และควรทำใกล้แหล่งข้อมูล |
SELECT col | Projection (π) | เก็บเฉพาะคอลัมน์ที่ใช้ ลดข้อมูลที่ต้องอ่านและส่ง |
GROUP BY | Grouping/Aggregation (γ) | รวมบางส่วนในเครื่อง แล้ว Shuffle ตาม key เพื่อรวมขั้นสุดท้าย |
JOIN | Join (⋈) | เลือก Broadcast Join, Hash Join หรือ Sort-Merge Join ตามขนาดและรูปแบบข้อมูล |
ORDER BY | Sort (τ) | แบ่งช่วง key, Shuffle แล้วเรียงข้อมูลภายในแต่ละ partition |
WHERE ลงไปกรองตั้งแต่ตอนอ่านไฟล์ ตัดคอลัมน์ที่ไม่ใช้ออกก่อน Shuffle หรือเปลี่ยนลำดับ Join เพื่อให้ข้อมูลระหว่างทางเล็กลง นี่คือการใช้ Algebra เพื่อประหยัดทั้งงานและการเคลื่อนย้ายข้อมูลเส้นทางจาก Hadoop มาถึง Spark SQL
| ช่วง | สิ่งที่โปรแกรมเมอร์เขียน | ใครวางแผนการคำนวณ |
|---|---|---|
| MapReduce | Map, key, Shuffle และ Reduce | โปรแกรมเมอร์กำหนดโครงหลักเอง |
| Pig / Hive | Dataflow script หรือ SQL-like query | ระบบแปลงเป็น MapReduce หลาย job |
| Spark RDD | สายของ transformation | Spark วาง stage แต่ยังไม่รู้ความหมายของคอลัมน์มากนัก |
| DataFrame / Spark SQL | Relational operations หรือ SQL | Catalyst ใช้ schema และ Algebra ช่วยปรับ logical/physical plan |
3Spark SQL และ DataFrame
// อ่านข้อมูลแบบ 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, groupBy | count, collect, show, head, take |
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: รับ 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)
5เมื่อยุคเปลี่ยน: Spark SQL/ML ในปี 2026
SQLContext หรือ HiveContext แยกกัน ขึ้นกับว่าต้องการ feature ของ Hive หรือไม่SparkSession เดียวตั้งแต่ Spark 2.0 เป็นต้นมา ลดความสับสนของ entry point หลายตัวDataset[T] (Scala) รวมข้อดีของ RDD (type safety) กับ DataFrame (Catalyst optimization) เข้าด้วยกัน — Adaptive Query Execution (AQE) ปรับแผนการ query ระหว่างรันจริงได้ ไม่ใช่แค่ตอน compile- อธิบายว่าทำไม RDD ถึงเรียกว่า "resilient" และกลไก lineage ทำงานอย่างไรเมื่อ partition หายไป
- อธิบายความแตกต่างระหว่าง Transformation และ Action พร้อมยกตัวอย่างแต่ละแบบอย่างน้อย 3 ตัว
- อธิบายว่าทำไม DataFrame API และ SQL string ถึงให้ประสิทธิภาพเท่ากันใน Spark
- แปลงคำสั่ง SQL พื้นฐานเป็น Selection, Projection, Join และ Aggregation พร้อมอธิบายว่าระบบกระจายงานอย่างไร
- อธิบายความต่างระหว่าง logical plan กับ physical plan และยกตัวอย่าง selection pushdown หรือ projection pruning
- อธิบายความสัมพันธ์ระหว่าง Transformer, Estimator, และ Pipeline ใน Spark ML
- อธิบายว่าทำไมงาน deep learning สมัยใหม่มักใช้ Spark ร่วมกับ PyTorch/TensorFlow แทนที่จะใช้ MLlib ล้วน ๆ