PythonでシンプルなSQLパーサーを実装する(ステップバイステップ)
Takashi Yamamoto
Infrastructure Engineer · Leapcell

PythonでシンプルなSQLパーサーを実装する:原理から実践へ
データ処理の分野では、SQL(構造化クエリ言語)はデータベースとやり取りするための中心的なツールです。しかし、SELECT id, name FROM users WHERE age > 18
のようなステートメントを書くと、プログラムがどのように理解しているのか疑問に思ったことはありませんか?その背景にはSQLパーサーの力があります。これは、人間が読めるSQLテキストをプログラムが実行できる構造化データ(抽象構文木など)に変換します。
SQLパーサーは、ORMフレームワーク(例:Django ORM)、SQL監査ツール(例:Sqitch)、およびデータベースミドルウェア(例:Sharding-JDBC)で広く使用されています。この記事では、解析の原則から始めて、PythonでコアSQLステートメント(SELECT ... FROM ... WHERE
)をサポートするシンプルなSQLパーサーの実装をガイドし、パーサーのコアロジックを理解するのに役立ちます。
I. SQL解析の核心:2つの主要な段階
SQLを含むあらゆる言語の解析には、「字句解析」と「構文解析」という2つの重要な段階があります。このプロセスを「英語の文章を読む」ことに例えることができます。最初に個々の単語を識別し(字句解析)、次に文の文法構造を理解します(構文解析)。
1. 字句解析:「単語」に分解する
字句解析の目標は、連続するSQLテキストを明確な意味を持つ個々のトークン(字句素)に分割することです。トークンはSQLの「単語」のようなものです。例:
- SQLステートメント:
SELECT id, name FROM users WHERE age > 18
- 分割後のトークンシーケンス:
[SELECT, ID(id), COMMA, ID(name), FROM, ID(users), WHERE, ID(age), GT(>), INT(18)]
一般的なSQLトークンの種類は次のとおりです。
- キーワード:
SELECT
、FROM
、WHERE
(大文字と小文字を区別しない) - 識別子:テーブル名(例:
users
)、列名(例:id
) - リテラル:数値(
18
)、文字列('Alice'
) - 演算子:
=
(等価)、>
(より大きい)、<
(より小さい) - 句読点:
,
(コンマ)、*
(アスタリスク、すべての列を表す)
2. 構文解析:「文構造」を構築する
構文解析は、SQL文法規則(例えば、「SELECT
の後に列名または*
が続き、FROM
の後にテーブル名が続く必要がある」)を使用して、トークンシーケンスを**抽象構文木(AST)**に変換します。ASTは、SQLステートメントの論理的な階層を明確に表すツリー構造です。たとえば、上記のSQLステートメントのASTは、次のように単純化できます。
Query
├─ select_clause: [id, name]
├─ from_clause: users
└─ where_clause:
├─ column: age
├─ operator: >
└─ value: 18
ASTの値は、構造化されていないテキストを構造化データに変換できることにあります。ASTを走査することにより、プログラムは「どの列をクエリするか」、「どのテーブルからクエリするか」、「どのようなフィルタ条件を適用するか」などの重要な情報を簡単に取得できます。
II. 練習:PythonでシンプルなSQLパーサーを実装する
Pythonのply
ライブラリ(Python Lex-Yacc)を使用してパーサーを実装します。ply
は、従来のコンパイラ構築ツールであるlex
(字句解析用)およびyacc
(構文解析用)をシミュレートするライブラリです。使いやすく、パーサーのコアロジックに密接に合致しています。
1. 環境準備
まず、ply
ライブラリをインストールします。
pip install ply
2. ステップ1:字句アナライザー(Lexer)の実装
字句アナライザーの核心は、正規表現を使用してさまざまな種類のトークンを一致させ、スペースやコメントなどの無関係な文字を無視することです。
コード実装(Lexer)
import ply.lex as lex # 1. トークンタイプを定義する(エラーを避けるために最初に定義する必要がある) tokens = ( 'SELECT', 'FROM', 'WHERE', # キーワード 'ID', 'INT', 'STRING', # 識別子とリテラル 'EQ', 'GT', 'LT', # 演算子(等価、より大きい、より小さい) 'COMMA', 'STAR' # 句読点(コンマ、アスタリスク) ) # 2. キーワードのマッチングルールを定義する(キーワードも文字で構成されているため、識別子よりも優先度が高い) reserved = { 'select': 'SELECT', 'from': 'FROM', 'where': 'WHERE' } # 3. トークンの正規表現を定義する(優先度の高い順に並べる) # 文字列リテラル:シングルクォートで囲む。例:'Alice' def t_STRING(t): r"'[^']*'" # 正規表現:シングルクォート内の任意の文字(シングルクォートを除く)に一致する t.value = t.value[1:-1] # シングルクォートを削除して、実際の内容を保持する return t # 整数リテラル:数字のシーケンス def t_INT(t): r'\d+' t.value = int(t.value) # 整数型に変換する return t # 識別子(テーブル名、列名):文字で始まり、文字/数字/アンダースコアが続く def t_ID(t): r'[a-zA-Z_][a-zA-Z0-9_]*' # 識別子がキーワードであるかどうかを確認する(例:'select'はIDではなくSELECTとして認識される必要がある) t.type = reserved.get(t.value.lower(), 'ID') return t # 演算子 t_EQ = r'=' # 等価 t_GT = r'>' # より大きい t_LT = r'<' # より小さい # 句読点 t_COMMA = r',' # コンマ t_STAR = r'\*' # アスタリスク(*は正規表現で特別な意味を持つため、エスケープが必要) # 4. 無関係な文字を無視する(スペース、タブ、改行) t_ignore = ' \t\n' # 5. エラー処理(認識できない文字が発生した場合にトリガーされる) def t_error(t): print(f"Illegal character: '{t.value[0]}'" t.lexer.skip(1) # 無効な文字をスキップし、後続のコンテンツの解析を続行する # 6. Lexerインスタンスを作成する lexer = lex.lex() # Lexerをテストする:SQLテキストを入力し、トークンシーケンスを出力する def test_lexer(sql): lexer.input(sql) print("Lexical Analysis Result (Token Sequence):") while True: tok = lexer.token() if not tok: break print(f"Type: {tok.type:10}, Value: {tok.value}" # テストケース test_sql = "SELECT id, name FROM users WHERE age > 18 AND name = 'Alice'" test_lexer(test_sql)
実行結果
Lexical Analysis Result (Token Sequence):
Type: SELECT , Value: select
Type: ID , Value: id
Type: COMMA , Value: ,
Type: ID , Value: name
Type: FROM , Value: from
Type: ID , Value: users
Type: WHERE , Value: where
Type: ID , Value: age
Type: GT , Value: >
Type: INT , Value: 18
Type: ID , Value: AND # 注:ANDキーワードはまだ定義されていないため、一時的にIDとして認識されます
Type: ID , Value: name
Type: EQ , Value: =
Type: STRING , Value: Alice
3. ステップ2:構文アナライザー(Parser)の実装
構文アナライザーの核心は、SQL文法規則を定義し、トークンシーケンスをASTに変換することです。最もコアなクエリ構文をサポートします。
SELECT [column_list/*] FROM table_name [WHERE condition (column operator value)]
コード実装(Parser)
import ply.yacc as yacc from lexer import tokens # ステップ1で定義されたトークンタイプをインポートする # 1. ASTノードを定義する(単純さと明瞭さのために辞書で表す) def create_ast(node_type, **kwargs): return {'type': node_type, **kwargs} # 2. 文法規則を定義する(優先度の低い順から高い順に並べる。開始ルールは「query」) # 開始ルール:クエリステートメント= SELECT句+ FROM句+ [WHERE句] def p_query(p): '''query : select_clause from_clause where_clause_opt''' # p[0]は規則の戻り値。p[1]はselect_clause、p[2]はfrom_clause、p[3]はwhere_clause_opt p[0] = create_ast( 'Query', select=p[1], from_clause=p[2], where_clause=p[3] if p[3] else None # オプションの句。存在しない場合はNoneに設定する ) # オプションのWHERE句:存在するか存在しないかのどちらか def p_where_clause_opt(p): '''where_clause_opt : WHERE condition | empty''' if len(p) == 3: # 「WHERE condition」に一致する p[0] = p[2] else: # 「empty」に一致する(WHERE句がない) p[0] = None # SELECT句:SELECT +(アスタリスク/列リスト) def p_select_clause(p): '''select_clause : SELECT STAR | SELECT column_list''' if p[2] == '*': # 「SELECT *」に一致する p[0] = create_ast('SelectClause', columns=['*']) else: # 「SELECT column_list」に一致する p[0] = create_ast('SelectClause', columns=p[2]) # 列リスト:コンマで区切られた複数のID(例:id、name、age) def p_column_list(p): '''column_list : ID | column_list COMMA ID''' if len(p) == 2: # 単一の列(例:id) p[0] = [p[1]] else: # 複数の列(例:column_list、ID) p[0] = p[1] + [p[3]] # FROM句:FROM +テーブル名(例:FROM users) def p_from_clause(p): '''from_clause : FROM ID''' p[0] = create_ast('FromClause', table=p[2]) # 条件:列+演算子+値(例:age> 18またはname = 'Alice') def p_condition(p): '''condition : ID EQ INT | ID EQ STRING | ID GT INT | ID LT INT''' p[0] = create_ast( 'Condition', column=p[1], operator=p[2], value=p[3] ) # 空のルール(オプションの句に使用される) def p_empty(p): '''empty :''' p[0] = None # 構文エラー処理 def p_error(p): if p: print(f"Syntax error: Near Token {p.type} (Value: {p.value})" else: print("Syntax error: Unexpected end of input" # Parserインスタンスを作成する parser = yacc.yacc() # Parserをテストする:SQLテキストを入力し、ASTを出力する def parse_sql(sql): ast = parser.parse(sql) print("\nSyntactic Analysis Result (AST):") import json # JSONを使用して、出力の形式を整え、読みやすくする print(json.dumps(ast, indent=2)) # テストケース(アスタリスク、複数の列、およびINT / STRING条件をサポートする) test_sql1 = "SELECT id, name FROM users WHERE age > 18" test_sql2 = "SELECT * FROM orders WHERE product = 'phone'" parse_sql(test_sql1) parse_sql(test_sql2)
実行結果(AST)
最初のテストケースのAST(SELECT id, name FROM users WHERE age > 18
):
{ "type": "Query", "select": { "type": "SelectClause", "columns": ["id", "name"] }, "from_clause": { "type": "FromClause", "table": "users" }, "where_clause": { "type": "Condition", "column": "age", "operator": ">", "value": 18 } }
2番目のテストケースのAST(SELECT * FROM orders WHERE product = 'phone'
):
{ "type": "Query", "select": { "type": "SelectClause", "columns": ["*"] }, "from_clause": { "type": "FromClause", "table": "orders" }, "where_clause": { "type": "Condition", "column": "product", "operator": "=", "value": "phone" } }
III. 解析結果の適用:ASTの値
ASTを取得したら、それを使用して多くのことができます。たとえば、ASTを自然言語の説明に変換する簡単な「クエリインタープリター」を作成できます。
def interpret_ast(ast): if ast['type'] != 'Query': return "Unsupported statement type" # Parse the SELECT clause select_cols = ', '.join(ast['select']['columns']) select_desc = f"Query columns: {select_cols}" # Parse the FROM clause from_desc = f"From table: {ast['from_clause']['table']}" # Parse the WHERE clause where_desc = "" if ast['where_clause']: cond = ast['where_clause'] where_desc = f", Filter condition: {cond['column']} {cond['operator']} {cond['value']}" return f"Execution logic: {select_desc} {from_desc}{where_desc}" # Test the interpreter ast1 = parser.parse(test_sql1) print(interpret_ast(ast1)) # Output: Execution logic: Query columns: id, name From table: users, Filter condition: age > 18
IV. 制限事項と高度な方向性
この記事で実装されているパーサーは、最も基本的なSQL構文のみをサポートしており、明らかな制限があります。
- 複数テーブルの結合(
JOIN
)、集計関数(COUNT
、SUM
)、およびグループ化(GROUP BY
)などの複雑な構文はサポートされていません。 - セマンティック分析(例えば、テーブル/列が存在するかどうか、またはデータ型が一致するかどうかのチェック)はサポートされていません。
- SQLコメントや大文字と小文字が混在するケース(例えば、
Select
)などのエッジケースは処理されません。
より実用的なSQL解析機能については、次の高度な方向性を参照してください。
- 成熟したライブラリを使用する:産業用グレードのシナリオでは、
sqlparse
(Python)やantlr4
(クロス言語)などのライブラリの使用を優先します。sqlparse
は、複雑なSQLを直接解析してASTを生成できます。 - 文法規則を拡張する:
ply
に基づいて、JOIN
やGROUP BY
などのルールを追加します。文法の優先順位(例えば、AND
はOR
よりも優先度が高い)に注意してください。 - セマンティック分析:構文解析後に「テーブル構造チェック」ステップを追加して、
SELECT
句の列がFROM
句で指定されたテーブルに存在するかどうかを確認します。 - クエリの最適化:ASTに基づいてクエリを最適化します(例えば、フィルタ条件をプッシュダウンする、インデックスを選択する)—これはデータベースカーネルのコア機能です。
V. 結論
SQL解析の本質は、「テキストを構造化されたデータに変換する」ことであり、これには「字句解析によるトークンへの分割」と「構文解析によるASTの構築」という2つのコアステップがあります。この記事でply
を使用して実装された単純なパーサーは、本番環境には十分ではありませんが、パーサーの動作原理を理解するのに役立ちます。
Leapcell: 最高のサーバーレスWebホスティング
最後に、Pythonサービスをデプロイするための優れたプラットフォームをお勧めします:Leapcell
🚀 お気に入りの言語で構築
JavaScript、Python、Go、またはRustで簡単に開発できます。
🌍 無制限のプロジェクトを無料でデプロイ
使用量に対してのみ支払い—リクエストも料金もありません。
⚡ 従量課金制、隠れたコストなし
アイドル料金はなく、シームレスなスケーラビリティのみ。
🔹 Twitterでフォローしてください:@LeapcellHQ