SQL과 JSON 사이 직렬화·역직렬화

SQL과 JSON 사이 직렬화·역직렬화

DuckDB는 SELECT 문을 SQL과 JSON 사이에서 직렬화(serialize)·역직렬화(deserialize)하는 함수와, JSON으로 직렬화된 문장을 그대로 실행하는 함수를 제공해요. 쿼리를 데이터처럼 다뤄야 하는 자동화·동적 실행 시나리오에 유용하죠.

출처: 공식문서

함수 목록

함수 타입 설명
json_deserialize_sql(json) Scalar 하나 또는 여러 개의 JSON 직렬화된 문장을 원래의 SQL 문자열로 다시 역직렬화해요.
json_execute_serialized_sql(varchar) Table JSON으로 직렬화된 문장을 실행하고 그 결과 행을 반환해요. 지금은 한 번에 하나의 문장만 지원해요.
json_serialize_sql(varchar, skip_default := boolean, skip_empty := boolean, skip_null := boolean, format := boolean) Scalar 세미콜론(;)으로 구분된 SELECT 문들을 동등한 JSON 직렬화 문장 목록으로 바꿔요.
PRAGMA json_execute_serialized_sql(varchar) Pragma json_execute_serialized_sql 함수의 PRAGMA 버전이에요.

json_serialize_sql(varchar) 함수는 출력을 제어하는 선택 파라미터 세 개 — skip_empty, skip_null, format — 를 받아요.

주의할 점이 하나 있어요. json_execute_serialized_sql(varchar) 테이블 함수는 직렬화된 문장을 별도의 쿼리 컨텍스트에서 실행하기 때문에, 트랜잭션 안에서 호출하면 트랜잭션의 로컬 변경 사항을 보지 못해요. 트랜잭션 컨텍스트를 그대로 쓰고 싶다면 PRAGMA json_execute_serialized_sql(varchar) 버전을 쓰면 되는데, 이때는 JSON을 상수 문자열로 넘겨야 해요. 즉 PRAGMA json_execute_serialized_sql(json_serialize_sql(...))처럼 함수 호출을 중첩해서 쓸 수는 없어요.

그리고 이 함수들은 FROM * SELECT ... 같은 문법적 달콤함(syntactic sugar)을 보존하지 않아요. 그래서 json_deserialize_sql(json_serialize_sql(...))로 왕복(round-trip)시킨 문장은 원문과 100% 동일하진 않지만, 항상 의미상 동등하고 같은 결과를 내요.

예시

가장 간단한 예시부터 볼게요.

SELECT json_serialize_sql('SELECT 2');
{"error":false,"statements":[{"node":{"type":"SELECT_NODE","modifiers":[],"cte_map":{"map":[]},"select_list":[{"class":"CONSTANT","type":"VALUE_CONSTANT","alias":"","query_location":7,"value":{"type":{"id":"INTEGER","type_info":null},"is_null":false,"value":2}}],"from_table":{"type":"EMPTY","alias":"","sample":null,"query_location":18446744073709551615},"where_clause":null,"group_expressions":[],"group_sets":[],"aggregate_handling":"STANDARD_HANDLING","having":null,"sample":null,"qualify":null},"named_param_map":[]}]}

여러 문장과 skip 옵션을 함께 쓰는 예시예요.

SELECT json_serialize_sql('SELECT 1 + 2; SELECT a + b FROM tbl1', skip_empty := true, skip_null := true);
{"error":false,"statements":[{"node":{"type":"SELECT_NODE","select_list":[{"class":"FUNCTION","type":"FUNCTION","query_location":9,"function_name":"+","children":[{"class":"CONSTANT","type":"VALUE_CONSTANT","query_location":7,"value":{"type":{"id":"INTEGER"},"is_null":false,"value":1}},{"class":"CONSTANT","type":"VALUE_CONSTANT","query_location":11,"value":{"type":{"id":"INTEGER"},"is_null":false,"value":2}}],"order_bys":{"type":"ORDER_MODIFIER"},"distinct":false,"is_operator":true,"export_state":false}],"from_table":{"type":"EMPTY","query_location":18446744073709551615},"aggregate_handling":"STANDARD_HANDLING"}},{"node":{"type":"SELECT_NODE","select_list":[{"class":"FUNCTION","type":"FUNCTION","query_location":23,"function_name":"+","children":[{"class":"COLUMN_REF","type":"COLUMN_REF","query_location":21,"column_names":["a"]},{"class":"COLUMN_REF","type":"COLUMN_REF","query_location":25,"column_names":["b"]}],"order_bys":{"type":"ORDER_MODIFIER"},"distinct":false,"is_operator":true,"export_state":false}],"from_table":{"type":"BASE_TABLE","query_location":32,"table_name":"tbl1"},"aggregate_handling":"STANDARD_HANDLING"}}]}

skip_default := true를 주면 AST에서 기본값("distinct":false 같은)을 생략할 수 있어요.

SELECT json_serialize_sql('SELECT 1 + 2; SELECT a + b FROM tbl1', skip_default := true, skip_empty := true, skip_null := true);
{"error":false,"statements":[{"node":{"type":"SELECT_NODE","select_list":[{"class":"FUNCTION","type":"FUNCTION","query_location":9,"function_name":"+","children":[{"class":"CONSTANT","type":"VALUE_CONSTANT","query_location":7,"value":{"type":{"id":"INTEGER"},"is_null":false,"value":1}},{"class":"CONSTANT","type":"VALUE_CONSTANT","query_location":11,"value":{"type":{"id":"INTEGER"},"is_null":false,"value":2}}],"order_bys":{"type":"ORDER_MODIFIER"},"is_operator":true}],"from_table":{"type":"EMPTY"},"aggregate_handling":"STANDARD_HANDLING"}},{"node":{"type":"SELECT_NODE","select_list":[{"class":"FUNCTION","type":"FUNCTION","query_location":23,"function_name":"+","children":[{"class":"COLUMN_REF","type":"COLUMN_REF","query_location":21,"column_names":["a"]},{"class":"COLUMN_REF","type":"COLUMN_REF","query_location":25,"column_names":["b"]}],"order_bys":{"type":"ORDER_MODIFIER"},"is_operator":true}],"from_table":{"type":"BASE_TABLE","query_location":32,"table_name":"tbl1"},"aggregate_handling":"STANDARD_HANDLING"}}]}

문법 오류가 있는 문장은 error 플래그와 함께 오류 정보를 돌려줘요.

SELECT json_serialize_sql('TOTALLY NOT VALID SQL');
{"error":true,"error_type":"parser","error_message":"syntax error at or near \"TOTALLY\"","error_subtype":"SYNTAX_ERROR","position":"0"}

역직렬화 예시예요.

SELECT json_deserialize_sql(json_serialize_sql('SELECT 1 + 2'));
SELECT (1 + 2)

syntactic sugar가 변환 과정에서 사라지는 걸 보여주는 역직렬화 예시예요.

SELECT json_deserialize_sql(json_serialize_sql('FROM x SELECT 1 + 2'));
SELECT (1 + 2) FROM x

직렬화한 문장을 바로 실행하는 예시예요.

SELECT * FROM json_execute_serialized_sql(json_serialize_sql('SELECT 1 + 2'));
3

오류가 있는 문장을 실행하면 이런 오류가 나요.

SELECT * FROM json_execute_serialized_sql(json_serialize_sql('TOTALLY NOT VALID SQL'));
Parser Error:
Error parsing json: parser: syntax error at or near "TOTALLY"

더 알아보기 (Learn more)

  • JSON 개요 — JSON 데이터 로딩·쿼리·생성 전반.
  • JSON 함수 — JSON을 다루는 스칼라·테이블 함수 목록.