Oracle การสอน PL/SQL Dynamic SQL: ดำเนินการทันที & DBMS_SQL

⚡ สรุปอย่างชาญฉลาด

ไดนามิก SQL ใน Oracle PL/SQL สร้างและรันคำสั่งในระหว่างการทำงาน โดยปรับเปลี่ยนคำสั่งค้นหาให้เข้ากับความต้องการที่เปลี่ยนแปลงไปผ่านสองแนวทาง ได้แก่ Native Dynamic SQL ที่มี EXECUTE IMMEDIATE และ OPEN-FOR และแพ็กเกจ DBMS_SQL ที่ยืดหยุ่นสำหรับกรณีที่ซับซ้อน

  • ⚙️ คำสั่ง SQL ขณะรันไทม์: Dynamic SQL สร้างและเรียกใช้คำสั่งเมื่อไม่ทราบชื่อตารางหรือชื่อคอลัมน์ล่วงหน้า
  • SQL แบบไดนามิกดั้งเดิม: EXECUTE IMMEDIATE สร้างและรันคำสั่ง SQL อย่างรวดเร็วด้วยโค้ดน้อยที่สุด
  • 🔁 เปิดรับสมัคร: จัดการกับคำสั่งค้นหาแบบไดนามิกหลายแถวที่คำสั่ง EXECUTE IMMEDIATE ไม่สามารถดึงข้อมูลได้เพียงอย่างเดียว
  • 🧩 DBMS_SQL: เหมาะสำหรับคำสั่งที่จำนวนคอลัมน์หรือประเภทไม่ทราบจนกว่าจะถึงเวลารันโปรแกรม
  • 🔐 ผูกตัวแปร: คำสั่ง USING จะส่งผ่านค่าตามตำแหน่งและป้องกันการโจมตีแบบ SQL injection
  • 🤖 ความช่วยเหลือจาก AI: เครื่องมือ AI ร่าง SQL แบบไดนามิกและแจ้งเตือนความเสี่ยงจากการโจมตีแบบ Injection ในระหว่างการตรวจสอบ

Oracle PL/SQL การสอน SQL ไดนามิก

ไดนามิก SQL คืออะไร?

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

วิธีการเขียน SQL แบบไดนามิก

PL/SQL มีวิธีการเขียน SQL แบบไดนามิกอยู่สองวิธี:

  1. NDS – Native Dynamic SQL (คำสั่ง EXECUTE IMMEDIATE และ OPEN-FOR)
  2. DBMS_SQL (ชุดอุปกรณ์ที่จัดส่ง)

หลักการทั่วไปนั้นง่ายมาก: หากทราบจำนวนและชนิดข้อมูลของตัวแปรอินพุตและเอาต์พุตในระหว่างการคอมไพล์ ให้ใช้ Native Dynamic SQL เพราะเร็วกว่าและใช้โค้ดน้อยกว่า แต่หากทราบข้อมูลนั้นเฉพาะในระหว่างการทำงาน ให้ใช้แพ็กเกจ DBMS_SQL

NDS (Native Dynamic SQL) – ดำเนินการทันที

Native Dynamic SQL เป็นวิธีที่ง่ายในการเขียน SQL แบบไดนามิก โดยใช้คำสั่ง EXECUTE IMMEDIATE เพื่อสร้างและเรียกใช้ SQL ในขณะรันไทม์ ในการใช้วิธีนี้ จำเป็นต้องทราบชนิดข้อมูลและจำนวนตัวแปรที่ใช้ในขณะรันไทม์ล่วงหน้า นอกจากนี้ยังให้ประสิทธิภาพที่ดีกว่าและมีความซับซ้อนน้อยกว่าเมื่อเทียบกับ DBMS_SQL

วากยสัมพันธ์

EXECUTE IMMEDIATE dynamic_sql_string
[INTO {variable[, variable]... | record}]
[USING [IN | OUT | IN OUT] bind_argument[, ...]]
[RETURNING INTO bind_argument[, ...]];
  • สตริง SQL แบบไดนามิก: นิพจน์สตริง (VARCHAR2 หรือ CHAR ไม่ใช่ NVARCHAR2/NCHAR) ที่บรรจุคำสั่ง SQL หรือบล็อก PL/SQL เพียงคำสั่งเดียว
  • เงื่อนไข INTO: ตัวเลือกเสริม ใช้เฉพาะเมื่อ SQL แบบไดนามิกเป็นการเลือกข้อมูลเพียงแถวเดียว โดยจะเก็บค่าที่ส่งคืนมาไว้ในตัวแปรหรือเรคอร์ด แต่ละคอลัมน์ที่เลือกจะต้องมีตัวแปรที่เข้ากันได้กับประเภทข้อมูล
  • เงื่อนไขการใช้งาน: ตัวเลือกเสริม ใช้สำหรับกำหนดตัวแปรผูกค่า โหมดเริ่มต้นคือ IN; ส่วน OUT และ IN OUT ใช้สำหรับรับค่ากลับมา
  • เงื่อนไขการกลับเข้าสู่ประโยค: ใช้ร่วมกับคำสั่ง DML ที่มีเงื่อนไข RETURNING เพื่อดึงค่าของแถวที่ได้รับผลกระทบมาใส่ในอาร์กิวเมนต์การผูกค่า

1 ตัวอย่าง: ในตัวอย่างนี้ เราดึงข้อมูลจากตาราง emp สำหรับ emp_no '1001' โดยใช้คำสั่ง NDS พร้อมตัวแปรผูกค่า

NDS - ดำเนินการทันที

DECLARE
   lv_sql       VARCHAR2(500);
   lv_emp_name  VARCHAR2(50);
   ln_emp_no    NUMBER;
   ln_salary    NUMBER;
   ln_manager   NUMBER;
BEGIN
   lv_sql := 'SELECT emp_name, emp_no, salary, manager FROM emp WHERE emp_no = :empno';
   EXECUTE IMMEDIATE lv_sql
      INTO lv_emp_name, ln_emp_no, ln_salary, ln_manager
      USING 1001;
   DBMS_OUTPUT.PUT_LINE('Employee Name: '   || lv_emp_name);
   DBMS_OUTPUT.PUT_LINE('Employee Number: ' || ln_emp_no);
   DBMS_OUTPUT.PUT_LINE('Salary: '          || ln_salary);
   DBMS_OUTPUT.PUT_LINE('Manager ID: '      || ln_manager);
END;
/

เอาท์พุต

Employee Name  : XXX
Employee Number: 1001
Salary         : 15000
Manager ID     : 1000

Code คำอธิบาย:

  • บรรทัดที่ 2-6: การประกาศตัวแปร
  • 8 แนว: การกำหนดโครงสร้าง SQL ในขณะรันไทม์ SQL นี้มีตัวแปรผูกค่า ':empno' อยู่ในเงื่อนไข WHERE
  • บรรทัดที่ 9-11: ดำเนินการคำสั่ง SQL ที่กำหนดไว้ด้วย EXECUTE IMMEDIATE ตัวแปรในส่วน INTO จะเก็บค่าที่ดึงมาได้ และส่วน USING จะส่งค่าให้กับตัวแปรผูกมัด :empno
  • บรรทัดที่ 12-15: แสดงค่าที่ดึงมาได้

การใช้ Dynamic SQL สำหรับ DDL

PL/SQL แบบคงที่ไม่สามารถรันคำสั่ง DDL เช่น CREATE, ALTER หรือ DROP ได้โดยตรง EXECUTE IMMEDIATE แก้ปัญหานี้โดยการสร้างคำสั่งเป็นสตริง ซึ่งสะดวกเมื่อมีการระบุชื่ออ็อบเจ็กต์ในระหว่างการรันไทม์ด้วย

DECLARE
   l_table_name VARCHAR2(30) := 'my_table';
   l_sql_stmt   VARCHAR2(200);
BEGIN
   l_sql_stmt := 'CREATE TABLE ' || l_table_name ||
                 ' (id NUMBER, name VARCHAR2(30))';
   EXECUTE IMMEDIATE l_sql_stmt;
END;
/

ชื่ออ็อบเจ็กต์ (ตาราง คอลัมน์ สคีมา) ไม่สามารถส่งผ่านเป็นตัวแปรผูกมัดได้ ดังนั้นจึงต้องนำมาต่อกันเป็นสตริง ตรวจสอบความถูกต้องของข้อมูลที่ป้อนเข้ามาเสมอ เช่น ด้วย DBMS_ASSERT.SIMPLE_SQL_NAME เพื่อป้องกันการโจมตีแบบ SQL injection

DBMS_SQL สำหรับไดนามิก SQL

PL/SQL มีแพ็กเกจ DBMS_SQL สำหรับทำงานกับ SQL แบบไดนามิก เมื่อโครงสร้างของคำสั่งไม่เป็นที่รู้จักจนกว่าจะถึงเวลาทำงาน กระบวนการสร้างและเรียกใช้ SQL แบบไดนามิกประกอบด้วยขั้นตอนต่อไปนี้:

  • เปิดเคอร์เซอร์: คำสั่ง SQL แบบไดนามิกทำงานเหมือนกับ... เคอร์เซอร์ในการเรียกใช้คำสั่ง SQL เราต้องเปิดเคอร์เซอร์ก่อน
  • วิเคราะห์คำสั่ง SQL: วิเคราะห์คำสั่ง SQL แบบไดนามิก ขั้นตอนนี้จะตรวจสอบไวยากรณ์และเตรียมคำสั่งให้พร้อมสำหรับการประมวลผล
  • ค่าที่ผูกกับตัวแปร: กำหนดค่าให้กับตัวแปรผูกมัด (ถ้ามี)
  • กำหนดคอลัมน์: กำหนดนิยามของแต่ละคอลัมน์โดยใช้ตำแหน่งสัมพัทธ์ในคำสั่ง SELECT
  • ดำเนินการ: ดำเนินการคำสั่งค้นหาที่แยกวิเคราะห์แล้ว
  • ดึงค่า: ดึงค่าที่ดำเนินการแล้ว
  • ปิดเคอร์เซอร์: เมื่อดึงผลลัพธ์เสร็จแล้ว ให้ปิดเคอร์เซอร์

1 ตัวอย่าง: ในตัวอย่างนี้ เราดึงข้อมูลจากตาราง emp สำหรับ emp_no '1001' โดยใช้คำสั่ง DBMS_SQL บล็อก EXCEPTION จะปิดเคอร์เซอร์แม้ว่าจะเกิดข้อผิดพลาดก็ตาม

DBMS_SQL สำหรับไดนามิก SQL

DECLARE
   lv_sql            VARCHAR2(500);
   lv_emp_name       VARCHAR2(50);
   ln_emp_no         NUMBER;
   ln_salary         NUMBER;
   ln_manager        NUMBER;
   ln_cursor_id      NUMBER;
   ln_rows_processed NUMBER;
BEGIN
   lv_sql := 'SELECT emp_name, emp_no, salary, manager FROM emp WHERE emp_no = :empno';
   ln_cursor_id := DBMS_SQL.OPEN_CURSOR;
   DBMS_SQL.PARSE(ln_cursor_id, lv_sql, DBMS_SQL.NATIVE);
   DBMS_SQL.BIND_VARIABLE(ln_cursor_id, ':empno', 1001);
   DBMS_SQL.DEFINE_COLUMN(ln_cursor_id, 1, lv_emp_name, 50);
   DBMS_SQL.DEFINE_COLUMN(ln_cursor_id, 2, ln_emp_no);
   DBMS_SQL.DEFINE_COLUMN(ln_cursor_id, 3, ln_salary);
   DBMS_SQL.DEFINE_COLUMN(ln_cursor_id, 4, ln_manager);
   ln_rows_processed := DBMS_SQL.EXECUTE(ln_cursor_id);
   LOOP
      IF DBMS_SQL.FETCH_ROWS(ln_cursor_id) = 0 THEN
         EXIT;
      ELSE
         DBMS_SQL.COLUMN_VALUE(ln_cursor_id, 1, lv_emp_name);
         DBMS_SQL.COLUMN_VALUE(ln_cursor_id, 2, ln_emp_no);
         DBMS_SQL.COLUMN_VALUE(ln_cursor_id, 3, ln_salary);
         DBMS_SQL.COLUMN_VALUE(ln_cursor_id, 4, ln_manager);
         DBMS_OUTPUT.PUT_LINE('Employee Name: '   || lv_emp_name);
         DBMS_OUTPUT.PUT_LINE('Employee Number: ' || ln_emp_no);
         DBMS_OUTPUT.PUT_LINE('Salary: '          || ln_salary);
         DBMS_OUTPUT.PUT_LINE('Manager ID: '      || ln_manager);
      END IF;
   END LOOP;
   DBMS_SQL.CLOSE_CURSOR(ln_cursor_id);
EXCEPTION
   WHEN OTHERS THEN
      DBMS_SQL.CLOSE_CURSOR(ln_cursor_id);
END;
/

เอาท์พุต

Employee Name  : XXX
Employee Number: 1001
Salary         : 15000
Manager ID     : 1000

Code คำอธิบาย:

  • บรรทัดที่ 1-8: การประกาศตัวแปร
  • 10 แนว: การจัดโครงสร้างคำสั่ง SQL
  • 11 แนว: การเปิดเคอร์เซอร์โดยใช้ DBMS_SQL.OPEN_CURSOR จะส่งคืน ID ของเคอร์เซอร์ที่เปิดอยู่
  • 12 แนว: หลังจากเปิดเคอร์เซอร์แล้ว ระบบจะทำการวิเคราะห์คำสั่ง SQL
  • 13 แนว: ค่าผูกมัด '1001' ถูกกำหนดแทนที่ ':empno'
  • บรรทัดที่ 14-17: กำหนดคอลัมน์ตามตำแหน่งสัมพัทธ์: (1) ชื่อพนักงาน, (2) หมายเลขพนักงาน, (3) เงินเดือน, (4) ผู้จัดการ
  • 18 แนว: เรียกใช้คำสั่ง SQL ด้วย DBMS_SQL.EXECUTE ซึ่งจะส่งคืนจำนวนระเบียนที่ประมวลผลแล้ว
  • บรรทัดที่ 19-32: ดึงข้อมูลในลูป ฟังก์ชัน FETCH_ROWS จะส่งค่า 0 กลับมาเมื่อไม่มีแถวเหลืออยู่ ซึ่งจะทำให้ลูปหยุดทำงาน
  • บล็อกข้อยกเว้น: ช่วยให้มั่นใจได้ว่าเคอร์เซอร์ถูกปิด เพื่อป้องกันไม่ให้เคอร์เซอร์ที่เปิดอยู่รั่วไหลหากเกิดข้อผิดพลาดขึ้น

NDS กับ DBMS_SQL: ควรใช้ตัวไหนในสถานการณ์ใด

ทั้งสองวิธีใช้คำสั่ง SQL ในระหว่างการทำงาน แต่เหมาะกับสถานการณ์ที่แตกต่างกัน:

  • ใช้ Native Dynamic SQL (EXECUTE IMMEDIATE / OPEN-FOR) เมื่อทราบจำนวนและชนิดข้อมูลของอินพุตและเอาต์พุตในระหว่างการคอมไพล์ จะทำให้เร็วขึ้น อ่านง่ายขึ้น และใช้โค้ดน้อยลง
  • ใช้ DBMS_SQL เมื่อโครงสร้างไม่เป็นที่รู้จักจนกว่าจะถึงเวลาทำงาน ตัวอย่างเช่น คำสั่ง SQL ที่จำนวนคอลัมน์ที่เลือกหรือตัวแปรผูกค่าเปลี่ยนแปลงไป ซึ่งเรียกว่า SQL แบบไดนามิกวิธีที่ 4 หรือคำสั่งที่มีขนาดใหญ่เกินกว่าจะใส่ลงในตัวแปร VARCHAR2 ขนาด 32K ตัวเดียวได้

คำถามที่พบบ่อย

ตัวแปรแบบ Bind จะส่งผ่านข้อมูลที่ผู้ใช้ป้อนเข้ามา ไม่ใช่โค้ดที่สามารถเรียกใช้งานได้ ส่วนคำสั่ง USING จะส่งค่าตามตำแหน่ง ดังนั้นข้อความที่เป็นอันตรายจึงไม่สามารถเปลี่ยนแปลงโครงสร้างของคำสั่งได้ ควรใช้ Bind กับข้อมูลที่รับเข้ามาที่ไม่น่าเชื่อถือเสมอ แทนที่จะต่อข้อมูลเข้าด้วยกัน

ลำดับ Oracle ผูกเฉพาะค่าข้อมูล ไม่ใช่ชื่อวัตถุ ต่อตัวระบุเข้ากับสตริง SQL และตรวจสอบความถูกต้องด้วย DBMS_ASSERT.SIMPLE_SQL_NAME เพื่อป้องกันการโจมตีแบบ Injection

คำสั่ง EXECUTE IMMEDIATE จะดึงข้อมูลเพียงแถวเดียว หากต้องการดึงข้อมูลหลายแถว ให้เปิด REF CURSOR ด้วยคำสั่ง OPEN-FOR จากนั้นวนลูปผ่านคำสั่ง FETCH จนกว่าจะพบค่า %NOTFOUND แล้วจึงปิดเคอร์เซอร์

เพิ่มส่วนคำสั่ง RETURNING ในคำสั่ง INSERT, UPDATE หรือ DELETE จากนั้นใช้ส่วนคำสั่ง RETURNING INTO ของ EXECUTE IMMEDIATE เพื่อดึงค่าของแถวที่ได้รับผลกระทบมาเก็บไว้ในอาร์กิวเมนต์การผูกข้อมูล

SQL แบบไดนามิกเพิ่มภาระในการวิเคราะห์เนื่องจากคำสั่งจะถูกคอมไพล์ในขณะรันไทม์ การนำตัวแปรผูกมัดกลับมาใช้ใหม่ช่วยให้ Oracle แชร์เคอร์เซอร์และลดการแยกวิเคราะห์ที่ซับซ้อน รักษาping ประสิทธิภาพใกล้เคียงกับ SQL แบบคงที่

สตริงต้องเป็นประเภท VARCHAR2 หรือ CHAR เท่านั้น ไม่อนุญาตให้ใช้ประเภทอักขระเฉพาะประเทศ เช่น NVARCHAR2 และ NCHAR สำหรับข้อความที่มีความยาวเกิน 32K นั้น DBMS_SQL จะยอมรับชุดข้อมูลที่ประกอบด้วยส่วนย่อยของ VARCHAR2

ใช่แล้ว ผู้ช่วย AI เช่น GitHub Copilot สามารถร่างบล็อก EXECUTE IMMEDIATE และ DBMS_SQL จากข้อความแจ้งทั่วไป แนะนำตัวยึดตำแหน่งตัวแปรผูก และอธิบายแต่ละส่วนได้ แต่ผู้พัฒนาควรตรวจสอบผลลัพธ์อีกครั้ง

เครื่องมือสแกนโค้ดที่ขับเคลื่อนด้วย AI จะตรวจจับข้อมูลที่ผู้ใช้ป้อนเข้ามาแบบต่อกัน และแนะนำตัวแปรผูก (bind variables) หรือการตรวจสอบ DBMS_ASSERT นอกจากนี้ยังเน้นรูปแบบที่มีความเสี่ยงในระหว่างการตรวจสอบ และช่วยให้...ping ทีมตรวจพบข้อบกพร่องในการฉีดโค้ดก่อนการใช้งานจริง

สรุปโพสต์นี้ด้วย: