การสร้างและตรวจสอบความถูกต้องของคำสั่ง SQL
รูปแบบพรอมต์สำหรับ SQL ที่ปลอดภัย: โหมด SELECT เท่านั้น และคำสั่งค้นหาแบบมีพารามิเตอร์
การสร้างและตรวจสอบความถูกต้องของคำสั่ง SQL เป็นบทเรียน AI Agents ฟรีบน CoddyKit นี่คือบทเรียนที่ 3 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน AI Agents และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส AI Agents มีบทเรียนทั้งหมด 4 บทเรียน
เป้าหมายของการสร้างคำสั่ง SQL
การสร้างคำสั่ง SQL เป็นเพียงครึ่งหนึ่งของงาน ก่อนดำเนินการกับฐานข้อมูลจริง คุณต้อง ตรวจสอบ ว่าคำสั่งนั้นปลอดภัย ถูกต้องตามไวยากรณ์ และทำงานได้ตรงตามที่ผู้ใช้ตั้งใจทุกประการ
บทเรียนนี้ครอบคลุมการบังคับใช้โหมด SELECT เท่านั้น การแยกวิเคราะห์ การดำเนินการอย่างปลอดภัย และการตรวจสอบแผนการทำงานด้วย EXPLAIN
การบังคับใช้โหมด SELECT เท่านั้น
สิ่งที่อันตรายที่สุดที่ตัวแทนแปลง NL เป็น SQL อาจทำได้คือการดำเนินการกับคำสั่งที่ทำลายข้อมูล ให้บังคับใช้ โหมด SELECT เท่านั้น เสมอ ไม่ว่า LLM จะส่งอะไรกลับมา
การตรวจสอบสตริงแบบง่าย ๆ ไม่เพียงพอ — ให้ใช้ตัวแยกวิเคราะห์ SQL ที่เหมาะสม
import sqlparse
def is_select_only(sql):
parsed = sqlparse.parse(sql)
if not parsed:
return False
for statement in parsed:
stmt_type = statement.get_type()
if stmt_type != 'SELECT':
print(f'Blocked statement type: {stmt_type}')
return False
return True
# Test
print(is_select_only('SELECT * FROM users')) # True
print(is_select_only('DROP TABLE users')) # False — Blockedบล็อกลิสต์คำสำคัญเพื่อการป้องกันหลายชั้น
แม้ใช้ตัวแยกวิเคราะห์ SQL แล้ว ก็ควรเพิ่มบล็อกลิสต์คำสำคัญเป็นการป้องกันชั้นที่สอง การแทรก SQL บางรูปแบบอาจหลอกตัวแยกวิเคราะห์ได้ การตรวจหาคำสำคัญอันตรายก่อนดำเนินการช่วยเพิ่มชั้นความปลอดภัยอีกชั้น
DANGEROUS_KEYWORDS = [
'INSERT', 'UPDATE', 'DELETE', 'DROP', 'CREATE',
'ALTER', 'TRUNCATE', 'GRANT', 'REVOKE', 'EXEC',
'EXECUTE', 'CALL', 'MERGE'
]
def passes_blocklist(sql):
sql_upper = sql.upper()
for keyword in DANGEROUS_KEYWORDS:
# Check as whole word to avoid false positives like 'CREATED_AT'
import re
if re.search(r'\b' + keyword + r'\b', sql_upper):
raise ValueError(f'Blocked keyword detected: {keyword}')
return True
def validate_sql(sql):
if not is_select_only(sql):
raise ValueError('Only SELECT statements are allowed')
passes_blocklist(sql)
return Trueการแยกวิเคราะห์ SQL ด้วยตัวแยกวิเคราะห์ SQL
sqlparse แบ่งสตริง SQL เป็นโทเค็นและแยกวิเคราะห์โดยไม่ดำเนินการกับสตริงเหล่านั้น คุณสามารถตรวจสอบโครงสร้างคำสั่งสอบถาม แยกชื่อ таблиาง และตรวจหาปัญหาด้านไวยากรณ์ได้
ติดตั้งด้วย pip install sqlparse
import sqlparse
from sqlparse.sql import IdentifierList, Identifier
from sqlparse.tokens import Keyword, DML
def extract_table_names(sql):
parsed = sqlparse.parse(sql)[0]
tables = []
from_seen = False
for token in parsed.tokens:
if token.ttype is DML and token.value.upper() == 'SELECT':
continue
if token.ttype is Keyword and token.value.upper() in ('FROM', 'JOIN'):
from_seen = True
continue
if from_seen:
if isinstance(token, Identifier):
tables.append(token.get_name())
elif isinstance(token, IdentifierList):
for item in token.get_identifiers():
tables.append(item.get_name())
from_seen = False
return tables
print(extract_table_names('SELECT u.name FROM users u JOIN orders o ON u.id = o.user_id'))
# ['users', 'orders']การตรวจสอบว่าตารางมีอยู่ในสคีมาหรือไม่
หลังจากแยกชื่อตารางจากคำสั่ง SQL ที่สร้างขึ้นแล้ว ให้ตรวจสอบเทียบกับสคีมาที่ทราบ หาก LLM สร้างชื่อตารางที่ไม่มีอยู่จริงขึ้นมา ให้ปฏิเสธคำสั่งก่อนดำเนินการ แทนที่จะปล่อยให้เกิดข้อผิดพลาดฐานข้อมูลที่เข้าใจยาก
def validate_tables_exist(sql, known_tables):
used_tables = extract_table_names(sql)
invalid = [t for t in used_tables if t and t not in known_tables]
if invalid:
raise ValueError(
f'Query references non-existent tables: {invalid}. '
f'Available tables: {list(known_tables)[:10]}...'
)
return True
# Usage
known = set(build_schema_dict(conn).keys())
try:
validate_tables_exist(generated_sql, known)
except ValueError as e:
# Send error back to LLM for correction
corrected_sql = llm_fix_sql(generated_sql, str(e))
print('Corrected SQL:', corrected_sql)การดำเนินการด้วยพารามิเตอร์
อย่าใช้การจัดรูปแบบสตริงเพื่อแทรกค่าที่ผู้ใช้ระบุลงใน SQL เด็ดขาด แม้ LLM จะเป็นผู้สร้างคำสั่งสอบถาม แต่ค่าตัวกรองที่ผู้ใช้ระบุควรส่งเป็นพารามิเตอร์ เพื่อป้องกันการแทรก SQL
import sqlite3
conn = sqlite3.connect(':memory:')
conn.execute('CREATE TABLE orders (status TEXT, user_id INTEGER)')
conn.execute("INSERT INTO orders VALUES ('pending', 42)")
def safe_execute(conn, sql_template, params=()):
"""Execute with parameterized values."""
cur = conn.cursor()
cur.execute(sql_template, params) # driver handles escaping
columns = [d[0] for d in cur.description]
rows = cur.fetchmany(200)
return {'columns': columns, 'rows': rows}
sql = 'SELECT * FROM orders WHERE status = ? AND user_id = ?'
result = safe_execute(conn, sql, params=('pending', 42))
print(result)แผน EXPLAIN ก่อนดำเนินการ
สำหรับคำสั่งสอบถามที่ใช้ทรัพยากรมากกับตารางขนาดใหญ่ ให้เรียกใช้ EXPLAIN ก่อนคำสั่งสอบถามจริง หากตัววางแผนแสดงว่ามีการสแกนทั้งตารางที่มีหนึ่งล้านแถว ให้เตือนผู้ใช้หรือปฏิเสธคำสั่งสอบถาม
def check_explain_plan(conn, sql):
explain_sql = f'EXPLAIN {sql}'
with conn.cursor() as cur:
cur.execute(explain_sql)
plan = '\n'.join(row[0] for row in cur.fetchall())
# Check for sequential scans on large tables
if 'Seq Scan' in plan:
print('WARNING: Query involves a sequential scan')
print(plan)
return {'safe': False, 'plan': plan, 'warning': 'Sequential scan detected'}
return {'safe': True, 'plan': plan}
# Use before executing
plan_result = check_explain_plan(conn, generated_sql)
if not plan_result['safe']:
print(f'Optimization hint: {plan_result["warning"]}')การบังคับใช้ขีดจำกัดแถว
LLM อาจสร้าง SELECT * FROM logs โดยไม่มี LIMIT ซึ่งอาจส่งคืนข้อมูลหลายล้านแถว ให้บังคับใช้จำนวนแถวสูงสุดเสมอ — โดยต่อท้าย LIMIT ให้คำสั่งสอบถาม หรือดึงชุดผลลัพธ์ที่มีขอบเขต
import re
MAX_ROWS = 500
def enforce_row_limit(sql, max_rows=MAX_ROWS):
sql_upper = sql.upper().rstrip().rstrip(';')
# Check if LIMIT already present
if re.search(r'\bLIMIT\b', sql_upper):
# Extract current limit and enforce maximum
match = re.search(r'LIMIT\s+(\d+)', sql_upper)
if match:
current = int(match.group(1))
if current > max_rows:
sql = re.sub(r'LIMIT\s+\d+', f'LIMIT {max_rows}', sql, flags=re.IGNORECASE)
else:
sql = sql.rstrip(';') + f' LIMIT {max_rows}'
return sql
print(enforce_row_limit('SELECT * FROM users'))
# SELECT * FROM users LIMIT 500การแยก SQL ที่สะอาดจากผลลัพธ์ของ LLM
LLM มักส่ง SQL ที่หุ้มอยู่ในบล็อกโค้ดมาร์กดาวน์ (```sql ... ```) หรือมีข้อความอธิบายประกอบ คุณต้องแยก SQL ดิบออกมาก่อนการแยกวิเคราะห์หรือดำเนินการ
import re
CODE_FENCE = chr(96) * 3 # three backticks, built at runtime to avoid template issues
def extract_sql(llm_response):
# Remove markdown code blocks ('''sql ... ''' or ''' ... ''')
pattern = CODE_FENCE + r'(?:sql)?\s*([\s\S]+?)' + CODE_FENCE
match = re.search(pattern, llm_response, re.IGNORECASE)
if match:
return match.group(1).strip()
# If no code block, look for SELECT statement
match = re.search(r'(SELECT\s+[\s\S]+?;)', llm_response, re.IGNORECASE)
if match:
return match.group(1).strip()
# Fallback: strip common preamble phrases
cleaned = re.sub(r'^(Here is|The SQL query is|Query:)[^\n]*\n', '',
llm_response, flags=re.IGNORECASE).strip()
return cleaned
if __name__ == '__main__':
demo_response = 'Here is the SQL query:\n' + CODE_FENCE + 'sql\nSELECT * FROM users;\n' + CODE_FENCE
print(extract_sql(demo_response))
กระบวนการตรวจสอบครบวงจร
เชื่อมขั้นตอนการตรวจสอบทั้งหมดเข้าด้วยกันเป็นฟังก์ชันเดียวที่รับผลลัพธ์ดิบจาก LLM และส่งคืนสตริง SQL ที่ปลอดภัยและดำเนินการได้ หรือแสดงข้อผิดพลาดพร้อมข้อความอธิบายชัดเจนเพื่อให้กู้คืนได้
def validate_and_prepare_sql(llm_output, known_tables, max_rows=500):
# Step 1: extract raw SQL
sql = extract_sql(llm_output)
if not sql:
raise ValueError('No SQL found in LLM response')
# Step 2: type check
if not is_select_only(sql):
raise ValueError('Only SELECT queries allowed')
# Step 3: keyword blocklist
passes_blocklist(sql)
# Step 4: table existence check
validate_tables_exist(sql, known_tables)
# Step 5: row limit
sql = enforce_row_limit(sql, max_rows)
return sql
# Full flow
try:
safe_sql = validate_and_prepare_sql(llm_output, known_tables)
result = safe_execute(conn, safe_sql)
except ValueError as e:
corrected = llm_fix_sql(llm_output, str(e))
safe_sql = validate_and_prepare_sql(corrected, known_tables)
result = safe_execute(conn, safe_sql)ผู้ใช้ฐานข้อมูลแบบอ่านอย่างเดียว
การตรวจสอบในระดับโค้ดมีความสำคัญแต่ยังไม่เพียงพอ เพื่อเป็นแนวป้องกันชั้นสุดท้าย ให้เชื่อมต่อฐานข้อมูลโดยใช้ บัญชีผู้ใช้แบบอ่านอย่างเดียว ที่มีสิทธิ์ SELECT เท่านั้น แม้คำสั่งอันตรายจะหลบเลี่ยงการตรวจสอบทั้งหมด ฐานข้อมูลก็จะปฏิเสธคำสั่งนั้น
# Create read-only user in PostgreSQL:
# CREATE USER nl_to_sql_reader WITH PASSWORD 'secure_password';
# GRANT CONNECT ON DATABASE yourdb TO nl_to_sql_reader;
# GRANT USAGE ON SCHEMA public TO nl_to_sql_reader;
# GRANT SELECT ON ALL TABLES IN SCHEMA public TO nl_to_sql_reader;
import os
import psycopg2
def get_readonly_connection():
return psycopg2.connect(
host=os.getenv('DB_HOST'),
database=os.getenv('DB_NAME'),
user='nl_to_sql_reader', # read-only account
password=os.getenv('DB_READER_PASS')
)ตรวจสอบความรู้
แนวทางป้องกันหลายชั้นที่ถูกต้องสำหรับการตรวจสอบ SQL ในตัวแทนแปลง NL เป็น SQL คืออะไร
สรุป: การสร้างและตรวจสอบ SQL
การสร้าง SQL อย่างปลอดภัยต้องใช้กระบวนการตรวจสอบครบวงจร: แยก SQL ที่สะอาดจากผลลัพธ์ของ LLM บังคับใช้ SELECT เท่านั้นด้วยตัวแยกวิเคราะห์ SQL ใช้บล็อกลิสต์คำสำคัญ ตรวจสอบชื่อตารางเทียบกับสคีมาจริง บังคับใช้ขีดจำกัดแถว และใช้ผู้ใช้ฐานข้อมูลแบบอ่านอย่างเดียวเป็นมาตรการป้องกันขั้นสุดท้าย
คำสั่งสอบถามแบบใช้พารามิเตอร์ช่วยป้องกันการแทรก SQL เมื่อมีค่าที่ผู้ใช้ระบุ การตรวจสอบแผน EXPLAIN ช่วยป้องกันไม่ให้คำสั่งสอบถามที่ใช้ทรัพยากรมากเกินคาดทำงานกับข้อมูลจริง
คำถามที่พบบ่อย
บทเรียน “การสร้างและตรวจสอบความถูกต้องของคำสั่ง SQL” ฟรีหรือไม่
ใช่ — ข้อความเต็มของ “การสร้างและตรวจสอบความถูกต้องของคำสั่ง SQL” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส AI Agents ให้อัปเกรดเป็น CoddyKit PRO คอร์ส AI Agents มีบทเรียนทั้งหมด 4 บทเรียน
คุณจะเรียนรู้อะไรในบทเรียน “การสร้างและตรวจสอบความถูกต้องของคำสั่ง SQL”
รูปแบบพรอมต์สำหรับ SQL ที่ปลอดภัย: โหมด SELECT เท่านั้น และคำสั่งค้นหาแบบมีพารามิเตอร์ คุณปฏิบัติ AI Agents ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน
คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน AI Agents หรือไม่
ไม่จำเป็นต้องมีประสบการณ์มาก่อน AI Agents บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 3 จากทั้งหมด 4 บทเรียน
บทเรียน “การสร้างและตรวจสอบความถูกต้องของคำสั่ง SQL” ใช้เวลานานแค่ไหน
บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย
ฉันเขียนและรันโค้ดในบทเรียน AI Agents นี้ได้ไหม
ได้ บทเรียน AI Agents ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- การทำงานของตัวแทน NL-to-SQL
- การทำความเข้าใจและแทรกโครงร่าง
- การสร้างและตรวจสอบความถูกต้องของคำสั่ง SQL
- การจัดการคำถามฐานข้อมูลที่กำกวม