구문#
어느 언어로 쓰였든 모든 문장이 지키는 어휘 층입니다. 이 레퍼런스가 구문에 쓰는 표기법, 그리고 식별자·리터럴·연산자·주석·null의 규칙입니다.
Cypher와 GQL과 SQL이 파서 하나를 공유하므로, 이 규칙들은 셋 모두에서 같습니다. 규칙이 셋 사이에서 다른 곳에서는 그 차이를 따로 적었습니다.
구문 표기 규약#
이 레퍼런스의 모든 개요는 같은 방식으로 적혀 있습니다:
개요에서 그 밖의 것은 표기법이 아닙니다. 쉼표·괄호·화살표는 문장의 일부이며, 보인 대로 입력합니다.
- 굵게
키워드입니다. 보이는 대로 입력하십시오. 대소문자는 상관없습니다.
- 기울임
직접 채우는 자리입니다. 각각 같은 페이지의 매개변수 아래에 정의돼 있습니다.
[ ]감싼 부분은 생략할 수 있습니다.
{ ... | ... }세로 막대로 구분한 선택지를 중괄호로 감싼 것입니다. 정확히 하나를 고르십시오.
...그 앞의 항목을 반복할 수 있습니다.
[, ... ]그 앞의 항목을 쉼표로 구분해 반복할 수 있습니다.
- 개요가 두 줄 이상
같은 문장의 다른 형태들입니다. 그중 하나를 쓰십시오.
whereplaceholderis one of:한 줄에 담기에 너무 큰 자리표를 뒤따르는 줄에서 펼쳐 보입니다.
대괄호와 중괄호는 결코 글자 그대로가 아닙니다. 어떤 문장에는 실제로 대괄호가 들어갑니다. 리스트
리터럴, 맵, 간선 패턴이 그렇습니다. 그런 경우에는 개요가 산문으로 그 사실을 적거나,
-[ variable ]->처럼 대괄호를 다른 자리에 보여 줍니다.
식별자#
식별자는 그래프·레이블·변수·속성 키·컬럼·인덱스·제약의 이름입니다.
대소문자 접힘#
따옴표 없는 식별자는 소문자로 접힙니다. PostgreSQL이 따옴표 없는 SQL 식별자를 접는 것과
정확히 같습니다. 그래서 MOVIE, Movie, movie가 모두 같은 레이블을 가리킵니다:
MATCH (n:MOVIE {title: 'The Matrix'}) RETURN n.title AS title;
title
--------------
"The Matrix"
큰따옴표로 감싼 식별자는 적힌 대로 받아들여집니다. "MOVIE"는 movie와 다른 이름입니다.
MOVIE라는 레이블은 없으므로, 그 패턴은 실패하는 것이 아니라 아무것에도 일치하지 않습니다:
MATCH (n:"MOVIE" {title: 'The Matrix'}) RETURN n.title AS title;
title
-------
(0 rows)
따옴표는 변수에도 통하며, 따옴표를 붙인 변수는 언급할 때마다 따옴표를 붙여야 합니다:
MATCH ("Film":movie {title: 'The Matrix'}) RETURN "Film".title AS title;
title
--------------
"The Matrix"
속성 키도 같은 방식으로 접힙니다. {MixedKey: 1}은 키 mixedkey를 저장하고,
{"MixedKey": 1}은 MixedKey를 저장합니다. 둘은 서로 다른 키이고 한 정점이 둘 다 가질 수 있으므로,
키에 따옴표를 붙일 때는 일관되게 붙이거나 아예 붙이지 마십시오.
백틱은 인용 문자가 아닙니다. `movie` 는 인용된 식별자가 아니라 구문 오류입니다:
MATCH (n:`movie`) RETURN n.title;
ERROR: syntax error at or near "`"
LINE 1: MATCH (n:`movie`) RETURN n.title;
^
문자#
따옴표 없는 식별자는 글자나 밑줄로 시작하고, 그 뒤로 글자·숫자·밑줄·달러 기호가 이어집니다.
“글자”에는 ASCII가 아닌 글자도 포함되므로 café도 유효한 식별자입니다. 숫자로 시작할 수는
없습니다. 1abc는 뒤에 군더더기가 붙은 숫자로 읽힙니다. 큰따옴표로 감싼 식별자에는 null 바이트를
뺀 어떤 문자든 들어갈 수 있고, 그 안의 큰따옴표는 두 번 적습니다("say ""hi""").
길이 제한은 PostgreSQL의 것입니다. 63바이트이고, 그보다 긴 이름은 그 길이로 잘립니다. 이 제한은 글자가 아니라 바이트를 세므로, 여러 바이트 글자로 된 이름은 더 일찍 제한에 닿습니다.
예약어#
어떤 키워드는 예약돼 있습니다. 문법이 그 단어를 이름이 아니라 키워드로 읽습니다. both,
let, finish, insert, offset, xor가 모두 예약어입니다. 일부는 PostgreSQL에서 물려받은
것이고 일부는 그래프 절을 위해 더한 것입니다. 각각이 어느 자리에서 여전히 동작하는지는 단어마다
다르므로, 자리별 표는 키워드에 있습니다. 패턴 변수는 그중 어느 것도
살아남지 못하는 자리입니다:
MATCH (both:movie {title: 'The Matrix'}) RETURN both.title;
ERROR: syntax error at or near "both"
LINE 1: MATCH (both:movie {title: 'The Matrix'}) RETURN both.title;
^
따옴표를 붙이면 그 제한이 풀립니다. 인용된 식별자는 결코 키워드로 읽히지 않기 때문입니다:
MATCH ("both":movie {title: 'The Matrix'}) RETURN "both".title AS title;
title
--------------
"The Matrix"
예약어는 RETURN 별칭과 속성 키로는 여전히 허용됩니다. 그 두 자리는 키워드와 혼동될 수 없기
때문입니다:
MATCH (m:movie {title: 'The Matrix'}) RETURN m.title AS both;
both
--------------
"The Matrix"
리터럴#
문자열#
문자열 리터럴은 작은따옴표로 감쌉니다. 큰따옴표는 문자열이 아니라 식별자를 만들므로,
"plain"은 변수 참조이고 그런 변수가 유효 범위에 없으면 실패합니다.
리터럴 안의 작은따옴표는 두 번 적습니다. 역슬래시 이스케이프는 JSON의 규칙을 따릅니다. Cypher의
문자열이 jsonb 문자열이 되기 때문입니다:
이스케이프 |
뜻 |
|---|---|
|
작은따옴표입니다. |
|
큰따옴표입니다. |
|
역슬래시입니다. |
|
슬래시입니다. |
|
백스페이스, 폼 피드, 줄바꿈, 캐리지 리턴, 탭입니다. |
|
그 코드 포인트의 문자입니다. |
그 밖의 역슬래시 조합은 거부됩니다. 'a\qb'는 Escape sequence "\q" is invalid를 냅니다.
RETURN 'plain', 'it''s', 'a\tb', 'a\\tb', 'say \" hi';
?column? | ?column? | ?column? | ?column? | ?column?
----------+----------+----------+----------+-------------
"plain" | "it's" | "a\tb" | "a\\tb" | "say \" hi"
출력은 각 값의 jsonb 표현이고, 그것은 출력하는 것을 다시 이스케이프합니다. 세 번째 컬럼은 문자
세 개, a와 탭과 b를 담고 있습니다.
숫자#
숫자 리터럴은 SQL에서와 같이 적으며, PostgreSQL의 비십진 표기와 구분자 표기도 받아들여집니다.
16진 0x, 8진 0o, 2진 0b, 그리고 자릿수 구분자로 쓰는 밑줄입니다.
RETURN 1, -1, 1.5, 1e3, 1E-3, 0x1F, 0o17, 0b101, 1_000;
?column? | ?column? | ?column? | ?column? | ?column? | ?column? | ?column? | ?column? | ?column?
----------+----------+----------+----------+----------+----------+----------+----------+----------
1 | -1 | 1.5 | 1000 | 0.001 | 31 | 15 | 5 | 1000
소수점 없이 적은 숫자는 JSON 정수이고, 그 둘을 나누면 버림이 일어납니다. 7 / 2는 3입니다.
3.5를 원하면 7.0 / 2로 쓰십시오.
불리언, null, 리스트, 맵#
true, false, null은 대소문자와 무관한 키워드입니다. 리스트는 대괄호로, 맵은 중괄호로 적습니다.
둘 다 중첩되고, 둘 다 값의 타입을 섞을 수 있습니다. 둘 다 jsonb가 되기 때문입니다.
RETURN true, false, null, [1, 'two', true, null], {a: 1, b: [2], c: {d: 3}};
?column? | ?column? | ?column? | ?column? | ?column?
----------+----------+----------+------------------------+-----------------------------------
t | f | | [1, "two", true, null] | {"a": 1, "b": [2], "c": {"d": 3}}
맵의 키는 식별자이고 식별자처럼 접힙니다. []와 {}는 빈 리스트와 빈 맵입니다. 리터럴이 저장되는
jsonb 값과 어떤 관계인지는 데이터 타입을 참고하십시오.
연산자#
우선순위#
연산자는 이 순서로 결합하며, 느슨한 것이 먼저입니다. 같은 행의 두 연산자는 우선순위가 같고 그 행이 적은 대로 결합합니다.
우선순위 |
연산자 |
결합 방향 |
|---|---|---|
1 (가장 느슨) |
|
왼쪽 |
2 |
|
왼쪽 |
3 |
|
왼쪽 |
4 |
|
왼쪽 |
5 |
|
오른쪽 |
6 |
|
없음 |
7 |
|
없음 |
8 |
|
없음 |
9 |
|
왼쪽 |
10 |
|
왼쪽 |
11 |
|
왼쪽 |
12 |
|
왼쪽 |
13 |
|
오른쪽 |
14 |
|
없음 |
15 |
|
왼쪽 |
16 |
|
왼쪽 |
17 (가장 단단) |
|
왼쪽 |
이 표는 Cypher 표현식이 담을 수 있는 연산자를 나열합니다. SQL은 자기만의 단계에 몇 개를 더합니다.
AT TIME ZONE, COLLATE, LIKE, ILIKE, SIMILAR TO입니다. Cypher의 표현식 문법은 그중 어느
것도 받지 않습니다.
이 표에서 흔한 기대와 다른 것이 세 가지 있습니다.
^는 왼쪽 결합이고 단항 마이너스보다 느슨하게 결합합니다. 산술의 규칙이 아니라 PostgreSQL의
규칙입니다. 그래서 2 ^ 3 ^ 2는 (2 ^ 3) ^ 2이고 -2 ^ 2는 (-2) ^ 2입니다:
RETURN 2 ^ 3 ^ 2 AS pow, -2 ^ 2 AS neg;
pow | neg
-----+-----
64 | 4
문자열 일치 연산자는 첨자·호출·형 변환·속성 읽기를 뺀 모든 것보다 단단히 결합합니다. 이어
붙이기보다도, 산술보다도 단단히 결합합니다. 그래서 아래 ||의 오른쪽 피연산자는 'c'만이 아니라
STARTS WITH 검사 전체입니다:
RETURN 'ab' || 'c' STARTS WITH 'a';
?column?
-----------
"abfalse"
다른 쪽으로 읽히길 원한다면 괄호를 쓰십시오. ('ab' || 'c') STARTS WITH 'a'는 true입니다.
비교는 이어지지 않습니다. 1 < 2 < 3은 두 개의 비교가 아니라 구문 오류입니다.
논리 연산자#
AND, OR, NOT, XOR는 불리언을 받아 불리언을 반환하며, 세 개의 값을 다룹니다. 진리표는
null과 삼치 논리를 참고하십시오. XOR는 SQL의 집합에
AgensGraph가 더한 것이고, 우선순위는 OR와 같습니다.
비교와 조건#
=, <>(또는 !=), <, <=, >, >=는 두 값을 비교합니다. jsonb 타입이 서로 다른 값을
비교해도 실패하지 않습니다. jsonb 자신의 타입 순서로 그것들을 정렬합니다.
IN은 리스트에 속하는지 검사합니다. IS NULL과 IS NOT NULL은 어떤 값이든 비교하지 않고 null인지
검사합니다. IS UNKNOWN과 IS NOT UNKNOWN은 불리언에 대한 그 검사를 표준이 표기하는 방식이며,
여기서도 받아들여집니다:
RETURN 2 IN [1, 2, 3] AS present, null IS NULL AS nul, null IS UNKNOWN AS unk;
present | nul | unk
---------+-----+-----
t | t | t
문자열 일치#
문자열을 일치시키는 연산자가 네 개 있습니다. LIKE, ILIKE, SIMILAR TO는 SQL 전용입니다.
Cypher의 표현식 문법의 일부가 아니고, 거기서는 구문 오류입니다. PostgreSQL의 정규 표현식
연산자(~, ~*, !~, !~*)는 Cypher에서 동작합니다.
연산자 |
검사 |
|---|---|
string |
문자열이 그 접두 문자열로 시작합니다. |
string |
문자열이 그 접미 문자열로 끝납니다. |
string |
그 부분 문자열이 문자열 어디에든 나타납니다. |
string |
문자열 전체가 POSIX 정규 표현식과 일치합니다. |
RETURN 'Matrix' STARTS WITH 'Mat' AS sw, 'Matrix' ENDS WITH 'rix' AS ew,
'Matrix' CONTAINS 'atr' AS c, 'Matrix' =~ '^M.*x$' AS re;
sw | ew | c | re
----+----+---+----
t | t | t | t
넷 중 어느 것도 인덱스로 답할 수 없습니다. 인덱스로 처리돼야 하는 조건에는 =이나 범위가
필요합니다. 속성의, 인덱스에 보이는 형태는 데이터 타입이 다룹니다.
산술#
+, -, *, /, %, ^는 숫자에 대해 동작하고, 단항 +와 -는 부호를 바꿉니다. 속성에
대해서는 SQL의 수치 타입이 아니라 jsonb를 상대로 동작하며, 그래서 +에 일이 둘 더 생깁니다.
문자열 둘을 이어 붙이고, 리스트에 덧붙입니다.
MATCH (p:person {name: 'Keanu Reeves'})
RETURN p.born + 1 AS plus, p.born % 7 AS mod, -p.born AS neg,
p.name + '!' AS concat, [1, 2] + 3 AS appended;
plus | mod | neg | concat | appended
------+-----+-------+-----------------+-----------
1965 | 4 | -1964 | "Keanu Reeves!" | [1, 2, 3]
맵 둘에 대한 +는 invalid expression: {"a": 1} + {"b": 2}로 거부됩니다. 맵은 대신 jsonb의
이어 붙이기 연산자로 병합하십시오:
RETURN {a: 1} || {b: 2} AS merged;
merged
------------------
{"a": 1, "b": 2}
값이 아니라 요소에 병합하려면 SET n += map을 쓰십시오.
jsonb 연산자#
속성은 jsonb이므로 PostgreSQL의 jsonb 연산자가 속성에 곧바로 적용됩니다. 알아 둘 만한 것들은
이렇습니다:
연산자 |
결과 |
|---|---|
json |
그 멤버를 |
json |
그 멤버를 |
json |
텍스트 배열 경로에 있는 값을 |
json |
텍스트 배열 경로에 있는 값을 |
json |
그 키가 있는지 |
json |
왼쪽 값이 오른쪽을 포함하는지 |
json |
왼쪽 값이 오른쪽에 포함되는지 |
json |
이어 붙이기. 객체 둘은 병합되고, 그 밖의 조합은 배열을 만듭니다. |
MATCH (m:movie {title: 'The Matrix'})
RETURN properties(m) -> 'title' AS as_json, properties(m) ->> 'title' AS as_text,
properties(m) ? 'genre' AS has_genre, properties(m) @> '{"genre": "Action"}' AS is_action;
as_json | as_text | has_genre | is_action
--------------+--------------+-----------+-----------
"The Matrix" | "The Matrix" | t | t
->>은 text를 반환했지만, Cypher의 RETURN이 스칼라 컬럼을 다시 jsonb로 감싸므로 따옴표가
그대로 남습니다. 평범한 문자열로 꺼내려면 그 쿼리를 SQL로 감싸십시오.
데이터 타입을 참고하십시오.
Cypher 안에서 PostgreSQL의 나머지 jsonb 연산자를 지배하는 규칙이 두 가지 있고, 둘 다 산술 표기를
Cypher가 차지했다는 점에서 따라옵니다.
jsonb 값 둘 사이의 -는 뺄셈이므로, jsonb의 키 삭제 연산자는 Cypher에서 닿을 수 없습니다.
properties(m) - 'plot'은 invalid expression을 냅니다. 키는 SQL로 감싸서 지우거나 REMOVE로
지우십시오.
네이티브 jsonb 연산자 옆에 문자열 리터럴만 두면 그것은 JSON 문자열이 아니라 JSON 문서 전체로
파싱됩니다. 그래서 m.title || '!'은 Token "!" is invalid로 실패합니다.
m.title || to_jsonb('!'::text)처럼 그 연산자에 타입이 있는 피연산자를 주십시오. 또는 Cypher 쪽
해석을 취하는 +를 쓰십시오.
주석#
주석은 --부터 그 줄의 끝까지, 또는 /*부터 짝이 되는 */까지입니다. 블록 주석은 중첩되므로,
주석이 들어 있는 영역을 주석 처리해도 짝이 맞습니다. //는 주석이 아니고 구문 오류입니다.
MATCH (m:movie {title: 'The Matrix'}) -- one film
/* a block comment
over two lines */
RETURN m.title AS title;
title
--------------
"The Matrix"
null과 삼치 논리#
null은 알 수 없음을 뜻합니다. 비교에 쓸 수 있는 값이 아니고, 속성 값도 아닙니다. 속성에 null을
할당하면 그 속성이 지워집니다. allow_null_properties를 켜 둔
경우는 예외입니다.
진리표#
AND, OR, XOR는 세 개의 값으로 다음과 같이 확장됩니다. 아래 표는 모든 조합을 실행한
출력입니다:
SELECT a #>> '{}' AS a, b #>> '{}' AS b,
andv::text AS "a AND b", orv::text AS "a OR b", xorv::text AS "a XOR b"
FROM (UNWIND [true, false, null] AS a
UNWIND [true, false, null] AS b
RETURN a, b, a AND b AS andv, a OR b AS orv, a XOR b AS xorv) q;
a | b | a AND b | a OR b | a XOR b
-------+-------+---------+--------+---------
true | true | true | true | false
true | false | false | true | true
true | null | null | true | null
false | true | false | true | true
false | false | false | false | false
false | null | false | null | null
null | true | null | true | null
null | false | false | null | null
null | null | null | null | null
NOT은 참을 거짓으로, 거짓을 참으로, null을 null로 바꿉니다:
SELECT a #>> '{}' AS a, notv::text AS "NOT a"
FROM (UNWIND [true, false, null] AS a RETURN a, NOT a AS notv) q;
a | NOT a
-------+-------
true | false
false | true
null | null
그래서 AND는 한쪽이 거짓이면 곧바로 거짓이고, OR는 한쪽이 참이면 곧바로 참이며, 다른 쪽이
무엇이든 그렇습니다. null이 얽힌 그 밖의 모든 것은 null입니다.
IN도 같은 규칙에서 따라옵니다. null을 담은 리스트에서 찾지 못한 값이라도 실은 거기 있었을 수
있으므로, 답은 거짓이 아니라 알 수 없음입니다.
SELECT expr, res::text AS result FROM (
RETURN '2 IN [1, 2, 3]' AS expr, 2 IN [1,2,3] AS res
UNION ALL RETURN '2 IN [1, null, 3]', 2 IN [1,null,3]
UNION ALL RETURN '2 IN [1, 2, null]', 2 IN [1,2,null]
UNION ALL RETURN '2 IN [1]', 2 IN [1]
UNION ALL RETURN '2 IN []', 2 IN []
UNION ALL RETURN 'null IN [1, 2, 3]', null IN [1,2,3]
UNION ALL RETURN 'null IN [1, null, 3]', null IN [1,null,3]
UNION ALL RETURN 'null IN []', null IN []
) q;
expr | result
------------------------+--------
"2 IN [1, 2, 3]" | true
"2 IN [1, null, 3]" | null
"2 IN [1, 2, null]" | true
"2 IN [1]" | false
"2 IN []" | false
"null IN [1, 2, 3]" | null
"null IN [1, null, 3]" | null
"null IN []" | false
빈 리스트가 예외입니다. 그 안에는 아무것도 없으므로 null에 대해서도 답이 거짓입니다.
null을 내놓는 표현식#
오류나 값이 아니라 null을 내놓는 표현식들입니다:
표현식 |
이유 |
|---|---|
|
그 요소에 그런 속성이 없습니다. |
|
그 리스트에 그런 요소가 없습니다. |
|
알 수 없는 피연산자와의 산술입니다. |
|
알 수 없는 값과의 비교입니다. |
|
null을 주고 호출한 함수입니다. |
MATCH (m:movie {title: 'The Matrix'})
RETURN m.no_such_property AS missing_property, [][0] AS missing_element,
head([]) AS head_of_empty, 1 + null AS arithmetic, 1 < null AS comparison,
sin(null) AS function, null = null AS equality;
missing_property | missing_element | head_of_empty | arithmetic | comparison | function | equality
------------------+-----------------+---------------+------------+------------+----------+----------
null | null | null | null | null | null | null
IS NULL이 있는 것은 null = null이 null이기 때문입니다. null을 검사하는 유일한 방법입니다.
조건은 불리언만 받는다#
WHERE, FILTER, CASE WHEN과 그 밖의 모든 조건은 불리언만 받고 그 밖의 것은 받지 않습니다.
jsonb 문자열·숫자·리스트·맵으로 평가되는 조건은 오류입니다:
CREATE GRAPH demo;
SET graph_path = demo;
CREATE VLABEL t;
CREATE (:t {name: 'Jack'});
MATCH (n:t) WHERE n.name RETURN n.name;
ERROR: cannot cast "Jack" (jsonb type string) to boolean
그 오류는 조건을 평가하는 데서 나오므로, 오류가 이름으로 부르는 값은 실행기가 먼저 도달한 행의
값입니다. 애초에 jsonb가 아닌 조건은 그보다 앞서, 문장이 파싱될 때 거부됩니다:
MATCH (n:t) WHERE 1 RETURN n.name;
ERROR: argument of WHERE must be type boolean, not type integer
LINE 1: MATCH (n:t) WHERE 1 RETURN n.name;
^
뜻하는 검사를 그대로 쓰십시오. WHERE n.name IS NOT NULL은 그 속성이 있는지 묻습니다:
MATCH (n:t) WHERE n.name IS NOT NULL RETURN n.name AS name;
name
--------
"Jack"
WHERE n.name <> ''은 그것이 비어 있지 않은지 묻고, WHERE n.active는 그 속성이 실제로 불리언을
담고 있을 때 동작합니다. jsonb 불리언이 불리언으로 형 변환되기 때문입니다.
null은 여전히 받아들여지고 SQL에서와 같이 동작합니다. 조건이 null인 행은 반환되지 않고 아무 오류도
나지 않습니다. 그래서 WHERE n.no_such_property는 아무 행도 반환하지 않습니다.
참고
이것은 비어 있지 않은 값을 조건으로 받아들였던 이전 릴리스와의 의도적인 호환성 단절입니다. 그런 방식으로 쓴 쿼리는 뜻하지 않은 것을 기준으로 걸러 내는 대신 실패합니다. 무엇을 고쳐야 하는지는 Cypher 호환성이 나열합니다.
호환성#
openCypher#
다른 곳에서 Cypher를 알고 온 독자가 걸릴 어휘상의 차이가 세 가지 있습니다.
식별자는 소문자로 접히고, 백틱은 인용 문자가 아닙니다. 큰따옴표를 쓰십시오.
`My Label`은 구문 오류입니다.문자열 리터럴은 작은따옴표만 받습니다. 큰따옴표는 식별자를 만듭니다.
//는 주석이 아닙니다.--나/* */를 쓰십시오.
XOR, STARTS WITH, ENDS WITH, CONTAINS, =~는 모두 있고, openCypher에서와 같은
뜻입니다.
ISO/IEC 39075#
이 레퍼런스의 표기법은 표준의 구문 생성 규칙과 같은 규약을 따르지만, 표준의 BNF는 아닙니다.
IS [NOT] UNKNOWN은 불리언에 대한 null 검사를 표준이 표기하는 방식이고 IS [NOT] NULL과 함께
받아들여집니다. XOR도 그렇습니다. 둘 다 표준의
<boolean value expression>(ISO/IEC 39075:2024 §20.20)에 속합니다. UNKNOWN을 값으로 쓰면 구문
오류입니다. 나머지는 GQL 적합성을 참고하십시오.
관련 항목#
데이터 타입 —
graphid,vertex,edge,graphpath, 그리고jsonb속성서버 설정 — 모든 세션에 필요한
graph_path를 포함키워드 — 전체 키워드 목록과 단어별 범주
Cypher, GQL, SQL — 어느 언어를 꺼내 쓸까
PostgreSQL: SQL 구문 — 같은 렉서의 SQL 쪽 절반