psql의 스크립트 언어는 튜링 완전(Turing complete)할까? 혹은: psql로 피보나치 수열 구하기
필 이튼(Phil Eaton) 2025년 11월 26일
저는 psql이 변수 설정(리터럴은 \set, 쿼리 결과는 \gset)과 조건부 블록(\if)을 지원한다는 것을 알게 되었습니다. 또한 루프(\watch 사용)와 일종의 재귀(\include 사용)도 지원합니다. 그렇다면 psql의 스크립트 언어로 모든 계산을 표현할 수 있을까요? 왠지 그럴 수 있을 것 같네요.
이 글은 그냥 재미로 쓴 것입니다. 절대 따라 하지 마세요. 하지만 psql의 중급 사용법을 익히기에는 꽤 괜찮은 입문서가 될 수도 있습니다.
자, 피보나치 수열 계산기를 만들어 봅시다. 먼저 파이썬으로 작성한 fib.py입니다.
def fib(n):
if n < 2:
return n
a = 0
b = 1
c = 0
while n > 1:
c = a + b
a = b
b = c
n -= 1
return b
if __name__ == "__main__":
print("nth fib to calculate: ")
n = int(input())
print(fib(n))
여기에 1을 입력하면 1이 나오고, 9를 입력하면 34가 나옵니다. 좋습니다!
psql, 프로그래밍 언어
psql의 구성 요소들을 살펴봅시다.
변수 (Variables)
\set을 사용하여 변수에 리터럴 값을 할당할 수 있습니다.
postgres=# \set a 1
\echo로 변수를 출력할 수 있습니다.
postgres=# \echo :a
1
\gset을 사용하여 쿼리 결과를 변수에 할당할 수 있습니다.
postgres=# SELECT 1 + 2 AS x \gset
postgres=# \echo :x
3
그리고 SQL 쿼리 내에서 psql 변수를 참조할 수 있습니다.
postgres=# SELECT :x * 2;
?column?
----------
6
(1 row)
조건부 실행 (Conditional execution)
\if를 사용하여 psql 문을 조건부로 실행할 수 있습니다.
postgres=# \if 'true'
postgres=# \echo 'here'
here
postgres=# \else
postgres@# \echo 'there'
\echo command ignored; use \endif or Ctrl-C to exit current \if block
postgres@# \endif
“command ignored(명령 무시됨)” 메시지가 약간 혼란스러울 수 있습니다. 분기(branch)가 \endif에 도달할 때까지 기다리지 않고 즉시 평가되기 때문입니다. 이 메시지는 해당 코드가 절대 실행되지 않을 것임을 의미합니다. psql -f로 스크립트를 실행할 때는 이 메시지가 나타나지 않습니다.
\if가 반대로 작동하는 경우도 살펴봅시다.
postgres=# \if 'false'
postgres@# \echo 'here'
\echo command ignored; use \endif or Ctrl-C to exit current \if block
postgres@# \else
postgres=# \echo 'there'
there
postgres=# \endif
\if (또는 \elif)에 전달되는 “표현식”은 반드시 참(truthy)인 문자열이어야 하는 것으로 보입니다:
유효한 값은 true, false, 1, 0, on, off, yes, no 중 대소문자를 구분하지 않고 명확하게 일치하는 값입니다. 예를 들어 t, T, tR은 모두 true로 간주됩니다.
불리언(boolean) 비교를 하려면 쿼리 내에서 실행한 뒤, 그 결과를 변수에 할당하여 확인해야 합니다.
postgres=# SELECT 1 = 2 AS ok \gset
postgres=# \if :ok
postgres@# \echo 'here'
\echo command ignored; use \endif or Ctrl-C to exit current \if block
postgres@# \else
postgres=# \echo 'there'
there
postgres=# \endif
루프 (Loops)
\watch를 사용하면 쿼리를 N번 실행할 수 있습니다.
postgres=# SELECT 1 \watch count=3 interval=0
Wed Nov 26 11:42:00 2025 (every 0s)
?column?
----------
1
(1 row)
Wed Nov 26 11:42:00 2025 (every 0s)
?column?
----------
1
(1 row)
Wed Nov 26 11:42:00 2025 (every 0s)
?column?
----------
1
(1 row)
하지만 안타깝게도 \gset으로 변수를 설정하는 것과 \watch를 결합할 수는 없습니다. 예를 들어 \watch와 \gset을 이용해 카운터를 증가시킬 수 없습니다. 적어도 저는 방법을 찾지 못했습니다.
postgres=# \set counter 0
postgres=# SELECT :counter + 1 AS counter \gset \watch interval=0 count=3
Wed Nov 26 11:43:27 2025 (every 0s)
counter
---------
1
(1 row)
Wed Nov 26 11:43:27 2025 (every 0s)
counter
---------
1
(1 row)
Wed Nov 26 11:43:27 2025 (every 0s)
counter
---------
1
(1 row)
postgres=# \echo :counter
1
\gset 명령은 한 번만 실행되고 쿼리 자체만 3번 실행되는 것으로 보입니다.
여기서 좀 막혔습니다!
재귀 (Recursion)
그러다 \include를 사용하여 파일의 리터럴 텍스트를 붙여넣을 수 있다는 점을 깨달았습니다. 그리고 그 붙여넣는 파일이 실행 중인 파일 자신일 수도 있습니다. 바로 재귀(Recursion)입니다.
counter.psql 파일에 위의 모든 것을 결합하여 다음과 같이 만들 수 있습니다:
\if :{?counter}
\else
\set counter 3
\endif
\echo :counter
SELECT :counter = 0 AS done \gset
SELECT :counter - 1 AS counter \gset
\if :done
\else
\include counter.psql
\endif
그리고 터미널에서 스크립트를 실행해 봅니다.
$ psql postgres -f counter.psql
3
2
1
0
나쁘지 않네요.
psql로 만든 피보나치 계산기
이 모든 것을 다시 종합하여 fib.psql에 파이썬 코드를 재귀적(그리고 약간 더 장황한) 방식으로 구현해 보겠습니다.
\if :{?n}
\else
\prompt 'nth fib to calculate: ' n
\set a 0
\set b 1
\set c 0
\endif
SELECT :a + :b AS c \gset
\set a :b
\set b :c
SELECT :n - 1 AS n \gset
SELECT :n < 0 AS zero, :n = 0 AS one, :n = 1 AS two, :n > 1 AS continue \gset
\if :zero
\echo nth = 0
\elif :one
\echo nth = 1
\elif :two
\echo nth = :b
\elif :continue
\include fib.psql
\endif
한번 해봅시다! 1을 입력하면 1이 나옵니다. 9를 입력하면 34가 나옵니다.
$ psql postgres -f fib.psql
nth fib to calculate: 1
nth = 1
$ psql postgres -f fib.psql
nth fib to calculate: 9
nth = 34
40을 입력하면 파이썬과 똑같이 102334155가 나옵니다.
$ psql postgres -f fib.psql
nth fib to calculate: 40
nth = 102334155
$ python3 fib.py
nth fib to calculate: 40
102334155
50을 입력하면 충돌(crash)합니다.
$ psql postgres -f fib.psql
nth fib to calculate: 50
psql:fib.psql:10: ERROR: integer out of range
psql:fib.psql:10: ERROR: integer out of range
psql:fib.psql:10: ERROR: integer out of range
psql:fib.psql:10: ERROR: integer out of range
nth = 1836311903
간단한 수정법이 있습니다. Postgres INT에서 덧셈을 하는 대신 BIGINT로 형변환(cast)을 해주는 겁니다.
\if :{?n}
\else
\prompt 'nth fib to calculate: ' n
\set a 0
\set b 1
\set c 0
\endif
SELECT :a::BIGINT + :b::BIGINT AS c \gset
\set a :b
\set b :c
SELECT :n - 1 AS n \gset
SELECT :n < 0 AS zero, :n = 0 AS one, :n = 1 AS two, :n > 1 AS continue \gset
\if :zero
\echo nth = 0
\elif :one
\echo nth = 1
\elif :two
\echo nth = :b
\elif :continue
\include fib.psql
\endif
이제 50번째 피보나치 수를 찾을 수 있습니다.
$ psql postgres -f fib.psql
nth fib to calculate: 50
nth = 12586269025
$ python3 fib.py
nth fib to calculate: 50
12586269025
하지만 100번째쯤 가면 다시 오버플로우가 발생합니다.
$ psql postgres -f fib.psql
nth fib to calculate: 100
psql:fib.psql:10: ERROR: bigint out of range
...
nth = 7540113804746346429
문제없습니다. NUMERIC(65, 0)으로 캐스팅하면 됩니다. 이제 100번째와 200번째 피보나치 수도 구할 수 있습니다.
$ psql postgres -f fib.psql
nth fib to calculate: 100
nth = 354224848179261915075
$ psql postgres -f fib.psql
nth fib to calculate: 200
nth = 280571172992510140037611932413038677189525
$ python3 fib.py
nth fib to calculate: 100
354224848179261915075
$ python3 fib.py
nth fib to calculate: 200
280571172992510140037611932413038677189525
1000번째를 시도해 봅시다.
$ psql postgres -f fib.psql
nth fib to calculate: 1000
psql:fib.psql:23: error: fib.psql: Too many open files
음. 어쩌면 우리의 재귀에는 결국 한계가 있었나 봅니다.
메일: salesinquiry@enterprisedb.com

