PostgreSQL 6.3.1 - 멀티바이트 환경의 기본 제공 PostgreSQL 6.3.1의 새로운 버전이 발표될 때마다 기다려지는 바이지만, 이번만큼은 너무 정신없이 버전이 올라간 것이 아닐까 생각한다. 3월 1일, 6.3 버전이 나온지가 채 한 달도 되지 않아서 버그 패치버전이 또 다시 나왔다는 소식을 주변의 잘 아는 동료로부터 듣고, 회사 설립에 정신이 없는 필자에게 또 하나의 일거리가 된 느낌이었다. 일단 새로운 버전이 나오면 설치한 다음 몇 가지 테스트를 하는 것이 습관처럼 되어 버렸다. 버전 6.3.1에서 달라진 내용 중 가장 중요한 사항은 그 동안 패치 파일로 별도로 제공되던 멀티바이트(한글, 일어, 중국어 등의 2바이트, 서양권 1바이트) 지원이 PostgreSQL 내에 자체 포함되었다는 것이다. 설치시에 자국의 사용언어를 위한 선택만 해주면 된다. PostgreSQL 소스 배포판을 푼 디렉토리를 중심으로 본다면, 한글 사용을 위한 멀티바이트 선택은 다음과같이 하면 된다.
SPI 와 트리거란 무엇인가? SPI는 서버 프로그래밍 인터페이스(Server Programming Interface)이다. psql은 postmaster 서버에 접속하여 질의를 처리하는 클라이언트 유틸리티라고 볼 수 있다. SPI는 SQL 질의를 사용자 정의 C함수 내부에서 실행할 수 있는 능력을 사용자에게 제공한다. 현재 버전의 PostgreSQL에서 서버측에 저장되는 프로시저와 트리거를 작성할 수 있는 유일한 방법이 SPI이다. 사실 SPI는 데이터베이스 서버 내의 분석기(Parser), 계획기(Planner), 최적화기(Optimizer), 실행기(Executor)에 간편하게 접군할 수 있는 인터페이스 함수의 집합일 뿐이다. SPI 에서는 또한 약간의 메모리 관리를 함께 수행한다.
먼저 SPI를 배우자! - SPI 인터페이스 함수 SPI를 익히는 것은LIBPQ를 익힐 때와 유사하다. 다만, LIBPQ는 프론트엔드 인터페이스(또는 클라이언트의 서버 접속 인터페이스)이고,SPI 는 서버내에서의 프로그래밍 인터페이스라는 점에 유의하면 된다.SPI를 작성하는 언어는 단연 C이다. 작성하는 방법은 SPI제공 함수를 사용하여C함수로 작성한 다음, 이 함수를 PostgreSQL 내부에 등록하기 위해서 psql에서 함수 등록 질의를 사용하면 된다. 원하는 기능을 SPI함수와 트리거 데이터구조를 사용하여 C 함수로 작성한 다음,PSQL에서 함수 등록 절차를 거친다. 다음 트리거 생성 질의문을 사용하여 등록한다. 먼저, SPI 용으로 준비되어 있는 함수를 살펴보자. 이들 함수는 서버측에서 구동된다는 점을 빼면,LIBPQ와 외양이 유사하므로 익히기 쉬울 것이다. SPI_connect int SPI_connect(void) ; 이 함수는 사용자의 프로시저를 SPI 관리자에 연결시킨다. SPI_connect는 Postgres 백엔드와 연결을 성립한다. 질의를 실행하기 위해서는 먼저 이 함수를 호출하여야 한다. SPI 함수 중에는 접속이 되지 않은 상태에서 호출할 수 있는 것이 더러 있다. 이미 접속이 되어있는 상태에서 SPI_connect를 다시 호출하면 SPI_ERROR_CONNECT를 반환한다. 연결이 성공적일 때에는, SPI_OK_CONNECT 가 반환된다. 내부적으로 이 함수는 질의 실행과 메모리 관리를 위한 내부적인 구조체를 최기화 한다. SPI_finish int SPI_finish(void) ; 이 함수는 SPI 관리자와 사용자의 프로시저를 분리시킨다. 사용자는 SPI 관리자를 통해 작업을 마쳤다면 이 함수를 호출하여야 한다. 현재 접속이 되어있지 않은 상태에서 이 함수를 호출한다면, SPI_ERROR_UNCONNECTED를 반환한다. 그 외에는 SPI_OK_UNCONNECTED를 반환한다. 내부적으로는 SPI_connect에서 palloc를 통해 할당된 모든 메모리를 해제한다. SPI_exec int SPI_exec(char *query, int tcount) ; 이 함수는 질의실행 계획을 수립하여, 질의를 수행한다. 서버의 분석기, 계획기, 최적화기가 이 함수의 계획수립시에 관여한다. char *query 는 질의 계획을 포함하는 문자열이고, int tcount 는 반환될 튜플의 최대 갯수이다. 질의가 성공적으로 수행되면 다음의 양수가 반환된다. SPI_OK_UTILITY : 유틸리티 질의(CREATE TABLE 등) 가 실행되었을 때 SPI_OK_SELECT : SELECT 가 실행되었을 때 SPI_OK_SELINTO : SELINTO ... INTO 가 실행되었을 때 SPI_OK_INSERT : INSERT 가 실행되었을 때 SPI_OK_DELETE : DELETE 가 실행되었을 때 SPI_OK_UPDATE :UPDATE 가 실행되었을 때
에러가 발생하면 다음 중 하나의 음수를 반환한다. SPI_ERROR_ARGUMENT :query 가 NULL이거나 tcount <0 SPI_ERROR_UNCOMMECTED : 프로시저가 접속이 되지 않았을 때 SPI_ERROR_COPY : COPY TO/FROM stdin가 실행될 때 SPI_ERROR_CURSOR : DECLARE/CLOSE CURSOR, FETCH가 실행될 때 SPI_ERROR_TRANSACTION : BEGIN/ABORT/END 가 실행될 때 SPI_ERROR_OPUNKNOWN : 질의 타입이 불명확할 때
SPI_exec는 연결된 프로시저에서 호출되어야 한다. tcount 가 0 이면, 질의 검색에서 해당하는 모든 튜플을 반환한다. tcount >0 이면, tcount 만큼만 반환한다. 예를 들어보자. SPI-exec ("insert into table select * from table", 5) ;
위 함수는 적어도 5개의 튜플을 해당 테이블에 삽입할 것이다. 주의 : 하나의 query 안에 여러개의 복합 질의를 전달할 수 있다. 이 경우에 반환값은 마지막 질의가 실행된 결과이다. 질의 수행결과로 실제 반환된 튜플의 갯수는 SPI_processed 전역 변수에 저장된다. SPI_OK_SELECT가 반환되고, SPI_processed 가 0보다 크다면, 전역 포인터 SPITupleTable *SPI_tuptable를 사용하여 선택된 튜플에 접근할 수 있다. SPI_finish 는 SPITupleTable에 할당된 메모리를 모두 해제하기 때문에 사용불가능 해진다는 점에 주의하자! SPI-exec 가 가장 중요하게 취급되므로 눈여겨 봐두는 것이 좋다. 참고 SPITupletable 구조체는 매우 중요하다. 왜냐하면 실제로 회수된 튜플에 접근할 수 있는 방법을 제공하고 있기 때문이다. 나중에 예제에서도 이 구조체 이야기가 나오므로 자세히 알아보도록 한다. SPITupletable에 대한 구조체 정의는 PostgreSQL이 설치된 디렉토리를 기준으로 include/executor/spi.h에 있다. typedef struct { uint32 alloced ; /* 할당된 값의 개수 */ uint32 free ; /* 자유로운 값의 개수*/ TupleDesc tupdesc ; /* 튜플 기술자*/ HeadTuple * vals ; /* 실제 튜플을 담고 있는 포인터*/ } SPITupleTable ;
나중의 예제에 나오는 SPI_tuptable 은 SPITupleTable에 대한 포인터이다. 로 미리 SPI관리자에서 정의되어 있다. 이 구조체는 SPI_tuptable ->vals |
SPI_prepare void *SPI_prepare(char *query, int nargs, argtypes) ; SPI_ prepare는 실행계획을 수립하여 반환한다. 하지만 질의를 실행하지는 않는다. query는 질의할 문자열이고, nargs는 매개인자(SQL함수에서 $1... $nargs와 같음)의 개수이며, 질의 안에 매개 인자가 없을 경우에 nargs는 0 이 될 수 있다. SPI_prepare에서 반환된 질의 계획은 현재 세션에서만 사용할 수 있다. SPI_finish를 사용하면 질의 계획에 할당된 메모리를 해제하기 때문에, 이후에도 사용하려면 SPI_saveplan을 사용하여 별도로 저장하여야 한다. SPI_prepare 함수는 잘 사용하지는 않지만, 특정한 경우에 유용할 수 있다. 가령 준비된 실행계획을 수행하는 것이 이따금 그냥 실행하는 것보다 빠를 수 있다. 같은 질의를 여러번 수행할 때가 좋은 예이다. SPI_prepare가 성공적으로 수행되면 널이 아닌 포인터가 반환되고, 실패하면 널이 반환된다. 이 함수의 수행 상태 결과는 SPI_result 전역 함수에 설정된다. 내용은 SPI_exec에서 반환하는 값과 동일하며, query가 NULL이거나 nargs < 0 이거나, nargs >0 && argtypes 가 NULL일 경우에는 SPI_ERROR_ARGUMENT가 세트된다는 점만 다르다. SPI_saveplan void *SPI_saveplan (void *plan) ; 이 함수는 SPI_prepare에서 수립된 계획을 안전한 영역에 저장한다. 계획이 백업되는 영역은 SPI_finish 나 트랜잭션 관리자의 영향을 받지 않는 보호 구역이다. 이후의 SPI 버전에서는 수립된 계획을 시스템 카탈로그(데이터 사전)에 저장하거나 불러오는 것이 가능해질 것으로 보인다. 수립된 계획은 SPI_execp 함수를 사용하여 수행할 수 있다. 반환값을 실패시에는 NULL을 반환하고, 성공시에는 저장된 메모리 위치를 돌려준다. 전역 변수 SPI_result에 세트되는 값은 다음과 같다. 주의 : 준비된 계획이 참조하는 테이블이나 함수 등의 객체중 하나가, 사용자의 실행 세션 동안에 사라진다면 SPI_execp 의 결과는 예측할 수 없게 될 것이다. SPI_execp int SPI_execp (void *plan, Datum *values, char *nulls, int tcount) ; 이 함수는 준비된 계획이나 SPI_saveplan 이 반환한 계획을 실행한다. 함수의 인자중 plan은 실행할 계획이며, values는 실제 매개 인자의 값이며, nulls는 매개인자가 NULL을 가지는 것을 허용할 것인가를 설명하는 배열이다. nulls가 'n' 이면 NULL 이 허용되고, ' ' 이면 NULL 이 허용되지 않는다. tcount 는 계획이 실행된 결과로 돌려받을 튜플의 갯수를 지정한다. plan이 NULL 이거나 tcount <0 이면 SPI_ERROR_ARGUMENT를 돌려주고, values 가 NULL 이고 plan의 매개인자와 실제 매개인자인 values 가 맞지 않는다면 SPI_ERROR_ARGUMENT를 돌려준다. 그 외에는 SPI_exec와 동일하다. 그 외에 SPI_execp 의 결과에 따라 SPI_tuptable 은 반환된 실제 튜플의 값으로 설정되고, SPI_processed는 반환된 튜플의 갯수를 저장하게 된다. nulls가 NULL 이면 SPI_execp 는 실제 매개인자의 모든 값들이 NOT NULL이라고 가정한다. 주의 : 준비된 계획이 참조하는 테이블이나 함수등의 객체 중 하나가, 사용자의 실행 세션동안에 사라진다면 SPI_execp의 결과는 예측할 수 없게 될 것이다. 다음 함수부터는 실제로 튜플을 다루는데 사용하는 유틸리티이다. [index]의 형태로 사용하는데, 주로 회수된 튜플의 값을 얻기 위해 많이 사용된다. HeapTuple 은 하나의 튜플을 설명하는 자료형이다. 여기에서 또 중요한 구조체가 TupleDesc 이다. 이 구조체는 PostgreSQL 의 소스를 푼 디렉토리를 기준으로 src/include/access/tupdesc.h 에 정의되어 있다. typedef struct tupleDesc { /* 튜플에서 속성의 개수 */ int natts ; /* attrs [N]은 속성 번호 N+1의 기술자에 대한 포인터이다. */ AttributeTupleForm *attrs ; TupleConstr *constr ; } *TupleDesc ;
TupleDesc 구조체는 SPI_getvalue 등의 함수에서 튜플 기술자의 인자로 사용되며, TupleDesc->natts 는 해당 튜플의 속성의 개수를 확인하는데 사용된다. |
SPI_copytuple HeapTuple SPI_copytuple (HeapTuple tuple) ; 이 함수는 상위 실행기 수준에서튜플을 복사한다. 입력 값은 복사할 튜플이고 반환 값은 복사된 튜플이다. 입력 값인 tuple이 NULL일 경우 반환 값은 NULL이고, 그 외의 경우에는 NOT NULL 이다. SPI_modifytuple HeapTuple SPI_modifytuple (Relation rel, HeapTuple tuple, int nattrs,int *attnum, >> *values, chr *nulls) ; 이 함수는 상위 실행기 수준에서 테이블의 튜플을 수정한다. rel과 tuple은 수정될 입력 튜플을 지정하고, nattrs 는 attnum에 포함되어 있는 속성번호의 개수이다. attnum은 수정할 속성번호로 이루어진 배열이며, values는 이전의 값을 대체할 새로운 값이며, nulls는 속성이 NULL 일 수 있는가를 설명한다. 반환값은 NULL이 되고, tuple 이 NULL 이 아니고 수정이 성공하였을 경우에는 NOT NULL 이 된다. 이 함수의 자세한 결과는 전역 변수인 SPI_result 에 저장된다. rel 이 NULL 이거나 tuple이 NULL이거나 natts<=0 이거나 attnum 이 NULL이거나 values 가 NULL 이면 SPI_result 의 값은 SPI_ERROR_ARGUMENT 가 된다. attnum에서 유효하지 않은 속성 번호가 있을 경우(attnum이 0보다 작거나 튜플의 속성 개수보다 클 경우) 에는 SPI_ERROR_NOAT_TRIBUTE 로 세트된다. 애초에 전달된 튜플은 이 함수 수행이후에도 변화하지 않는다. SPI_fnumber int SPI_fnumber (TupleDesc tupdesc, char *fname) ; 이 함수는 지정한 속성의 속성번호를 돌려준다. fname은 찾을 속성의 이름이고, tupdesc는 입력 튜플 기술자이다. 반환값은 해당 속성의 번호이다. fname 으로 지정한 속성이 없을 경우에는 SPI_ERROR_NOATTRIBUTE를 돌려주며, 속성번호는 1부터 시작한다. SPI_fname char *SPI_fname (TupleDesc tupdesc, int fnumber) ; 이 함수는 지정한 속성의 속성 이름을 돌려준다. fnumber는 찾을 속성의 번호이고, tupdesc는 입력 튜플 기술자이다. 반환 값은 해당 속성의 이름이다. fnumber가 범위를 벗어나면 NULL이 반환되고,에러시에는 SPI_result전역 변수가 SPI_ERROR_NOATTRIBUTE 로 설정된다. 성공시에 반환되는 속성 이름은 새로운 메모리에 할당된 복사본이다. SPI_getvalue char *SPI_getvalue (HeapTuple tuple, TupleDesc tupdesc, int fnumber) ; 이 함수는 지정한 속성의 값을 문자열로 반환한다. tuple 은 값을 얻어낼 입력 튜플이고, tupdesc는 입력 튜플의 기술자이다. fnumber는 속성번호이다. 반환 값은 성공적일 경우에는 속성 값이며, 그 외의 경우에는 NULL이다. 이 경우에 자세한 상태 값은 SPI_result 에 설정된다. fnumber 가 범위를 벗어나면 SPI_ERROR_NOATTRIBUTE 로 설정되며, 출력 함수가 유효하지 않다면 SPI_ERROR_NOOUTFUNC 로 설정된다. 성공시에 반환된 속성 값은 새로운 메모리에 할당된 복사본이다. SPI_getbinval Datum SPI_getbinval (HeapTuple tuple, TupleDesc tupdesc, int fnumber, bool *isnull) ; 이 함수는 지정한 속성의 값을 이진값으로 돌려준다. tuple은 값을 얻어낼 입력 튜플이고, tupdesc는 입력 튜플의 기술자이다. fnumber는 속성 번호이다. 반환값은 바이너리 값으로, 반환 값을 위해서 새로운 공간을 할당하지 않는다는 점에 주의해야 한다. 만일 속성값이 NULL 일 경우에 isnull 이 1로 설정된다. fnumber 가 범위를 벗어나면 SPI_result 는 SPI_ERROR_NOATTRIBUTE로 설정된다. SPI_gettype OID *SPI_gettype (TupleDesc tupdesc, int fnumber) ; 이 함수는 지정한 속성의 타입 이름의 복사본을 반환한다. tupdesc는 튜플 기술자이고, fnumber 는 속성 번호이다. 반환값은 번호로 지정한 속성의 타입 이름이다. 속성 번호가 알맞지 않을 경우에 SPI_result 는 SPI_ERROR_NOATTRIBUTE로 설정된다. SPI_gettypeid OID SPI_gettypeid (TupleDesc tupdesc, int fnumber) ; 이 함수는 지정한 속성이 타입 아이디를 반환한다. 속성 번호가 알맞지 않을 경우에 SPI_result 값이 상기와 같이 설정된다. SPI_getrelname char *SPI_getrelname (Relation rel) ; 이 함수는 지정한 테이블의 이름을 한부 복사해서 돌려준다. rel은 입력 테이블을 나타낸다. SPI_palloc void *SPI_palloc (Size size) ; 이 함수는 사위 실행기 수준에서 메모리를 할당한다. size는 할당할 공간을 지정하는 8 진수이어야 한다. 물론 반환값은 새롭게 할당한 메모리의 주소이다. SPI_repalloc void *SPI_repalloc (void *pointer, Size size) ; 이 함수는 상위 실행기 수준에서 메모리를 다시 할당한다. pointer는 이미 존재하는 메모리 주소이고, Size는 새롭게 할당할 메모리의 크기를 8 진수로 지정한 것이어야 한다. 반환 값은 당연히 이미 존재하는 메모리에 담긴 내용을 복사한 내용을 담고 있는 지정한 크기의 공간의 주소이다. SPI_pfree void SPI_pfree (void *pointer) ; 이 함수는 상위 실행기 수준에서 메모리를 해제한다. pointer는 해제할 메모리 주소이다.
SPI에서 메모리 관리 데이터베이스 서버는 메모리를 할당할 때 상호 문맥이 영향을 받지 않도록 독립적으로 할당한다. 즉, 하나의 문맥에서 메모리를 해제되는 것은 다름 문맥에 영향을 주지 않는다는 이야기다. 따라서 현재의 문맥이 아닌 다른 문맥에 대한 메모리 해제는 예측할 수 없는 결과를 야기할 수 있다. SPI 프로시저는 두 개의 메모리 문맥과 관련이 있다. 상위 실행기 메모리 문맥과 프로시저 메모리 문맥이 그것이다. 이러한 두 개의 메모리 문맥에서 변환은 SPI 관리자의 메모리 관리에 의해 이루어진다. SPI 관리자에 접속하기 이전은 상위 실행기 문맥에 놓여지게 되고, 모든 메모리 할당은 프로시저 그 자신에 의해 palloc/repalloc를 통하거나 SPI 유틸리티 함수를 통해서 이루어진다. SPI_connect 가 호출된 이후에는 프로시저 문맥에 놓여지게 되고, 이후의 모든 메모리 할당은 palloc/repalloc 또는 SPI 유틸리티 함수(SPI_copytuple, SPImodifytuple, SPI_palloc, SPI_repalloc 는 제외된다.)를 통해 이루어진다. SPI 관리자와 연결을 끊게 되면 현재의 문맥은 상위 실행기 문맥으로 복귀하게 되며, 프로시저 메모리 문맥에서 할당된 모든 메모리는 해제되고 더 이상 사용할 수 없게 된다. 상위 실행기로 어떠한 것을 되돌릴려면 상위 문맥에서 이를 위한 메모리를 할당해야 한다. SPI는 상위 실행기 문맥에서 할당된 메모리를 자동적으로 해제 해주지는 않는다. 하지만 SPI는 질의의 실행 도중에 할당된 메모리는 질의가 완료되면 자동으로 해제해준다.
데이터변화의 가시성 규칙 PostgreSQL 에서의 가시성 규칙은상식적인 수준을 크게 벗어나지 않는다. 가시성 규칙은 프로시저 내부에서 질의가 처리될 때에 중요한 의미를 지닌다. 중요한 의미라기 보다는 작업하기에 따라서 사용자의 생각과는 다르게 작동할 수 있다는 것을 의미한다. 하지만 매우 규칙적이기 때문에 논리적으로 잘 추론하면 아무 이상이 없다. PostgreSQL에서 질의가 실행되는 도중에 질의 그 자체에 의해 이루어진 변화는 질의 검색기에 나타나지 않는다. 이러한 변화는 주로 SQL 함수나 SPI 함수, 또는 트리거에 의해 주로 이루어진다. 가령 예를 들어보면, "INSERT INTO a SELECT * FROM a" 질의에 의해 삽입된 튜플은 SELECT 검색시에 나타나지 않는다. 잠시 설명을 하자면, 이 질의는 자신의 테이블의 내용을 그대로 다시 복사한다. 물론 재귀는 하지 않으며, unique 인덱스 규칙이 있다면 이에 따른다. 질의 A의 실행 도중에 생성되는 변화는 질의 A 이후에 시작하는 질의에서는 나타난다. 하지만 질의 A가 실행되고 있는 도중에는 아무런 일도 없는 것처럼 보인다.
SPI의 예제 이제 SPI 인터페이스 함수를 사용하여 테스트를 위한 함수를 만들어 보자. 잘 이해하려면 꼼꼼히 챙겨봐야 할 것이다. SPI 가 아주 유용하긴 하지만 관련되는 데이터 타입과 전역 변수, SPI 함수, SPI 상수들을 주의깊게 살펴봐야 할 것이며, 가시성 규칙에 따른 결과에도 주의를 기울여야 한다. #include <stdio.h> #include "executor/spi.h" /* SPI 함수를 위한 헤더 파일 */ int execq (text *sql, int cnt) ; int execq (text *sql, int cnt) { int ret ; int proc = 0 ; SPI_connect () ; /* 먼저 SPI 관리자에 접속한다 */ /* textout 내부함수는 text *를 char *로 변환한다. SPI에서 기본 문자열형은 text 형으로, psql에서 입력하는 문자열은 text *로 서버에게 전달된다. */ ret = SPI_exec (textout(sql), cnt) ; /* execq 함수에 전달되는 질의인 sql을 실행한다. */ proc = SPI_processed ; /* SPI_processed 에는 처리된 튜플의 갯수가 저장된다. */ /* SELECT 가 성공하였고, 반환된 튜플이 0개를 초과한다면 */ if (ret == SPI_OK_SELECT && SPI_processed > 0) { /* SPI_tutable->tupdesc는 현재 튜플 리스트에 대한 기술자이다. */ TupleDesc tupdesc = SPI_tuptable->tupdesc ; /* SPI_tuptable A>> tuptable로 백업한다. */ SPITuleTable *tuptable = SPI_tuptable ; char buf[8192] ; int I ; for (ret = 0 ; ret < proc ; ret++) { /* 하나의 튜플데이터형에 회수된 튜플을 하나씩 차례대로 집어넣는다. */ HeapTuple tuple = tuptable->vals[ret] ; /* 하나의 튜플에 대해 각각의 속성의 값을 하나씩 얻어낸다. */ /* tupdesc->natts SPI_tuptable->tupdesc->natts 이며, 해당 튜플에서 속성의 개수이다. */ for (i = 1, buf[0] = 0 ; I <= tupdesc->natts ; i++) sprintf(buf + strlen(buf), "%s%s", SPI_getvalue(tuple, tupdesc, i), (i == tupdesc->natts) ? " " : "|") ; /* 더 이상의 속성이 없다면 공백을 출력하고, 아니라면 필드 구분자로 |를 출력한다. */ /* 로그 메시지를 출력한다. */ elog(NOTICE, "EXECQ: %s", buf) ; } /* 접속을 종료한다. */ SPI_finish() ; /* 실제로 처리된 튜플의 개수가 함수의 반환값이 된다. */ return proc ; } |
위 소스 파일을 testspi.c로 저장한다. 일단 여기에서 개별적인 내용만 이해하고 넘어가자. 전체적인 줄기는 실제 사용 예를 보면서 이해하는 것이 좋다. 먼저 위의 함수를 공유 라이브러리로 컴파일을 해보자. 참고로 C 함수를 공유 라이브러리로 컴파일하기 위해서는 PostgreSQL 가 설치된 디렉토리의 헤더파일 뿐만이 아니라 원본 소스 디렉토리의 헤더파일까지 필요하다. 필자의 디렉토리와 여러분의 디렉토리가 다를 수 있으므로 헤더 파일 참조 디렉토리는 알아서 수정하자. $ gcc -c testspi.c -fPIC -I/usr/local/pgsql/include -I/usr/local/postgresql-6.3.1 >> /src/include/ $ gcc -shared -0 testspi.so testspi.0
자주 이렇게 긴 명령 행을 타이핑 하는 것은 정신 건강에 해롭다. 간단히 스크립트를 만들어서 대체하도록 한다. # ! /bin/sh # name : funcom - Compiling Shared Object function for PostgreSQL CC=gcc PG_SRC_ROOT=/usr/local/postgersql-6.3.1/ PG_INS_ROOT=/usr/local/pgsql/ if [$# -le 0] ; then echo "usage : $0 <source>" exit fi PREFIX=${1%.*} $CC -c -o ${PREFIX}.o ${PREFIX}.c -fPIC -I${PG_INS_ROOT}/ include -I${PG_SRC_ROOT}/src/include gcc -shared -o ${PREFIX}.so ${PREFIX}.o |
앞으로는 다음과 같이 간단하게 컴파일한다. 이제 함수를 만들어보자. 이름은 execq로 한다. $ psql -c "create function execq (text, int4) returns int4 as ''pwd'/testspi.so' >> language 'c' ; " mydb
execq 함수를 테스트하기 전에 execq 함수를 전체적인 안목에서 잠시 살펴보자. 첫 번째 if으로 인해 SELECT 시에만 execq 의 나머지 부분이 실행된다. 아울러 검색된 튜플이 0개를 초과할 때에만 실행된다는 것을 알 수 있다. execq 함수에서 반환되는 값은 SELECT 질의가 처리하는 튜플의 개수와 동일함을 알 수 있다. 그리고 for 루프안을 보면 매번 각각의 튜플마다 그 칼럼을 elog 함수로 출력함을 알 수 있다. 즉, a 라는 테이블에 2개의 튜플이 있다면 execq('select * from a', 0)을 실행하면 두 번의 상위 for 문이 수행되어 두 개의 알림글이 출력된다는 것을 짐작할 수 있다. 이제 SPI 함수로 작성한 execq함수를 테스트 해보자. mydb=> select execq('create table a (x int4)', 0) ; execq ----- 0 (1 row) mydb=> insert into a values (execq('insert into a values (0)', 0) ; INSERT 167631 1 mydb=> select execq('select * from a', 0) ; NOTICE : EXECQ : 0 <- execq가 삽입한 것임 NOTICE : EXECQ : 1 <- execq가 반환하고 상위 INSERT가 삽입한 값 execq ----- 2 (1 row) mydb=> select execq('insert into a select x + 2 from a', 1) ; execq ----- 1 (1 row) mydb=> select execq('select * from a', 10) ; NOTICE : EXECQ : 0 NOTICE : EXECQ : 1 NOTICE : EXECQ : 2 <- 0+2, 첫 번째 튜플만 삽입되었음 execq ----- 3 <- 10 개가 튜플의 최대 횟수 제한 갯수인데, 3개가 실제 갯수임 (1 row) mydb=> delete from a ; DELETE 3 mydb=> insert into a values (execq('select * from a', 0) +1) ; INSERT 167712 1 mydb=> select * from a ; x - 1 <<< (0) +1 (1 row) mydb=> insert into a values (execq('select * from a', 0) +1) ; NOTICE : EXECQ : 0 INSERT 167713 1 mydb=> select * from a ; x - 1 2 <- (1) +1 (2 rows) -- 변화된 데이터의 가시성 규칙을 설명한다. mydb=> insert into a select execq('select * from a', 0) * x from a ; NOTICE : EXECQ : 1 NOTICE : EXECQ : 2 NOTICE : EXECQ : 1 NOTICE : EXECQ : 2 NOTICE : EXECQ : 2 INSERT 0 2 mydb=> select * from a ; x - 1 2 2<<< 2개 튜플 * 1(첫번째 튜플의 x값) 6 <<< 3개 튜플 (원래의 2개 + 방금 입력된 1개) * 2(두번째 튜플의 x값), 서로 다른 세션에서 execq() 함수는 변경된 내용을 볼 수 있다. (4 rows) |
트리거! 그 이름만으로도 설레이는 마음 PostgreSQL 의 현재버전(6.3.1) 에서는 다양한 인터페이스로 Perl 과 Tcl, Python, C 등을 지원한다. 하지만 아직 실제 프로시저 언어인 PL 에는 약점을 가지고 있다. 멀지 않은 세월에 제대로 된 PL 언어가 지원될 것으로 보인다. 뭐 하지만 C 함수를 호출하여 트리거의 역할을 수행하는 것은 현재로서도 가능하다. 그래도 아직 질의어(구문/절)차원에서는 아직 트리거 사건에 대한 처리는 지원하지 않고 있다. 트리거 사건시의 해당 튜플에 대해 INSERT, DELETE, UPDATE 시에 BEFORE 나 AFTER 구문을 지정하여 사용할 수 있다. C 함수 내부에서의 트리거는 주로 SPI 함수를 사용하여 처리된다. SPI 와 트리거를 땔래야 땔 수 없는 하나의 이유이다.
트리거의 생성 트리거 사건이 일어나면, 트리거 관리자가 실행기에 의해 호출되어서 전역 구조체인 TriggerData *CurrentTriggerData를 초기화한다. 그리고 사건을 처리하기 위해 지정 트리거 함수를 호출한다. 트리거 함수는 트리거 함수를 등록하기 전에 만들어야 하며, 트리거 함수는 전달 매개 인자가 없으며, opaque를 반환하도록 해야 한다. 트리거의 생성 구문은 다음과 같다. CREATE TRIGGER <트리거 이름> <BEFORE|AFTER> <INSERT|DELETE|UPDATE> ON <> FOR EACH <ROW|STATEMENT> EXECUTE PROCEDURRE <프로시저 이름> (<함수인자>) ;
트리거의 이름은 트리거를 제거할 때 빼고는 사용할 일이 없을 것이다. DROP TRIGGER 명령에서 트리거 이름이 인자로 사용된다. 그 다음의 BEFORE, AFTER 는 사건의 발생시점을 지정한다. 즉, 사건이 발생한 이후, 또는 이전에 트리거 함수를 호출할 것인지를 지정한다. 다음의 INSERT/DELETE/UPDATE 구문은 어떤 사건이 함수를 트리거할 것인지를 지정한다. 여러 개의 사건을 OR 연산하여 함께 지정할 수 있다. 테이블 이름은 해당 사건이 어느 테이블에 적용될 것인지를 지정한다. 다음의 FOR EACH 구문은 트리거가 테이블에 적용될 때, 각각의 로우마다 적용할 것인지, 아니면 전체 구문 단위로 적용할 것인지를 지정한다. 프로시저 이름은 호출할 C 함수의 이름이다. 함수인자는 CurrentTriggerData 구조체 내에 있는 함수로 전달된다. 함수에 매개 인자를 전달하는 목적은 같은 함수를 호출하는 서로 유사한 요청을 처리하는 다른 트리거를 허용하기 위해서 이다. 물론, 함수는 서로 다른 테이블을 트리거하는 데 사용된다. 이러한 함수는 '일반 트리거 함수'라고 부른다. 트리거 함수는 자신을 호출한 실행기에게 HeapTuple을 반환할 수 있다. 만일 사건 이후에 (AFTER) 트리거가 수행되도록 트리거를 생성했다면, 실행기가 INSERT/DELETE/UPDATE를 수행한 이후에 트리거 함수를 호출하게 된다. 이 트리거 함수에서 사용자가 어떤 튜플(HeapTuple)을 반환한다고 해도 실행기 입장으로 볼 때는 자신은 일을 다 마친 이후이므로 해당 튜플을 무시한다. 하지만 BEFORE 트리거시에는 이야기가 달라진다. 먼저 실행기는 자신에게 맡겨진 중요한 작업(INSERT/DELETE/UPDATE)을 수행하기 전에 트리거함수를 먼저 호출한다. 사용자의 트리거 함수에서 적당한 튜플이 반환되면, 그것으로 자신의 작업을 수행한다. 즉, 사용자가 작성한 트리거 함수에서 무엇을 넘겨주느냐에 따라서 실행기가 작업하는 내용이 달라진다는 것이다. 트리거 함수에서 NULL을 반환하면, 실행기는 반환받은 튜플이 NULL 이므로 아무일도 하지 않고 넘어간다. 즉, BEFORE 트리거에서 INSERT 시에 NULL을 돌려주도록 하였다면, 사용자가 해당 테이블에 INSERT 하더라도 실행기의 작업에서는 INSERT 되지 않는다는 것이다. 트리거의 작업에서는 INSERT를 할 수 있을 지는 몰라도. 만일 BEFORE 트리거에서 다른 튜플을 넘겨준다면(INSERT 와 UPDATE 시에만 가능하다) 원래의 튜플 대신에 반환한 튜플이 삽입될 것이다. 물론 UPDATE 시에는 갱신된 새로운 튜플이 될 것이다. PostgreSQL의 현재 버전에서는 트리거의 확장성에서 조금 부족한 듯한 부분이 있다. 즉, CREATE TRIGGER 시에 트리거의 초기화가 수행되지 않는다는 것이다. 또한, 동일한 테이블상에서 동일한 사건에 대해 하나 이상의 트리거를 정의할 시에, 트리거의 실행 순서는 예측할 수 없다는 것이다. 이러한 점들은 앞으로 보완될 것이다. 트리거 함수가 SPI를 사용하여 SQL 질의를 실행하면, 이 질의는 또다시 트리거된다. 이러한 트리거는 '종속 트리거(cascading trigger)'라 부른다. 종속 횟수의 명시적인 제한은 없다. 트리거가 INSERT 사건시에 수행되고, 동일한 테이블에 새로운 튜플이 삽입된다면, 이러한 트리거는 다시 트리거된다. 현재, 이러한 경우를 대비해 제공되는 동기화 기능은 아직 없지만, 조만간 제공될 것으로 보인다.
트리거 관리자와의 즐거운 대화 위에서도 언급했듯이, 트리거 관리자에 의해 함수가 호출되면, TriggerData *CurrentTriggerData 구조체는 NOT NULL 이 되면서, 초기화된다. 처음에 CurrentTriggerData 가 NULL 인지 검사하는 것이 좋다. TriggerData 구조체는 src/include/commands/trigger.h에 정의되어 있다. typedef struct TriggerData { TriggerEvent tg_event ; Relation tg_relation ; HeapTuple tg_trigtuple ; HeapTuple tg_newtuple ; Trigger *tg_trigger ; } TriggerData
각각의 멤버를 잠시 살펴보자. tg_event 이 멤버는 트리거의 종류를 설명하는데 사용된다. 아래의 다양한 매크로를 사용하여 tg_event의 종류를 확인할 수 있다. TRIGGER_FIRED_BEFORE(event) 트리거가 사건의 BEFORE시에 수행된다면 TRUE반환 TRIGGER_FIRED_AFTER(event) 트리거가 사건의 AFTER시에 수행된다면 TRUE반환 TRIGGER_FIRED_FOR_ROW(event) 트리거가 매 로우마다 수행된다면 TRUE반환 TRIGGER_FIRED_FOR_STATEMENT(event) 트리거가 매 구문마다 수행된다면 TRUE반환 TRIGGER_FIRED_BY_INSERT(event) 트리거가 INSERT 시에 수행된다면 TRUE반환 TRIGGER_FIRED_BY_DELETE(event)트리거가 DELETE 시에 수행된다면 TRUE반환 TRIGGER_FIRED_BY_UPDATE(event)트리거가 UPDATE 시에 수행된다면 TRUE반환 tg_relation 이 멤버는 트리거되는 테이블을 설명하는 구조체에 대한 포인터이다. src/include/utils/rel.h에 자세한 내용이 정의되어 있다. 흥미로운 점은 tg_relation->rd_att(테이블 튜플의 기술자)와 tg_relation->rd_rel->relname(테이블의 이름. 이것은 char * 가 아니라 NameData 형이다. char * 의 복사본을 얻으려면, SPI_getrelname(tg_relation)을 사용하면 된다.)이다. tg_trigtuple 이 멤버는 트리거되는 튜플을 가르키는 포인터이다. 이 튜플은 INSERT/DELETE/UPDATE 시에 각각 삽입/제거/갱신 되는 튜플이다. tg_newtuple 이 멤버는 UPDATE 시에 튜플의 새로운 버전을 가리키는 포인터이다. 단, INSERT 나 DELETE 시에 이 값은 NULL이 된다. tg_trigger 이 멤버는 src/include/utils/rel.h에정의되어 있는 트리거 구조체를 가리키는 포인터이다. typedef struct Trigger { char *tgname ; Oid tgfoid ; func_ptr tgfunc ; int16 tgtype ; int16 tgnargs ; int16 tgattr[8] ; char **tgargs ; } Trigger ;
이 구조체에서 tgname 은 트리거의 이름이고, tgnargs 는 tgargs 에 있는 인자의 개수이며, tgargs 는 CREATE TRIGGER 구문에서 지정한 인자를 가리키는 포인터의 배열이다. 다른 멤버는 내부적인 목적으로만 사용된다.
트리거의 예제 트리거에서의 데이터변화에 따른 가시성 규칙은 기본적으로 SPI 에서의 가시성 규칙과 동일하다. 덧붙여, BEFORE 트리거에서 삽입되는 튜플(tg_trigtuple)은 질의시에 나타나지 않는다. 이 튜플은 AFTER 트리거시에는 나타난다. 이제 간단한 트리거의 예제를 살펴보자. 트리거의 대부분의 기능은 SPI를 통해서 실현된다. 아울러 트리거에서만 사용 되는 것은 위에서도 살펴본 트리거 관련 데이터 구조체들이다. 여기에 등장하는 trigf 함수는 트리거되는 테이블인 ttest 에 있는 튜플의 갯수를 출력하고, 만일 질의가 NULL을 x 에 삽입하려고 한다면 작동을 하지 않고 건너뛴다.(이 부분은 NOT NULL 규정과 똑같이 동작한다. 하지만 trigf 함수에서는 트랜잭션을 중단하지는 않는다.) #include <stdio.h> #include "executor/spi.h" /* SPI 에 필요한 헤더파일 */ #include "commands/trigger.h" /* 트리거에 필요한 헤더파일 */ HeapTuple trigf(void) ; HeapTuple trigf () { TupleDesc tupdesc ; /* 튜플 기술자 */ HeapTuple rettuple ; /* 하나의 튜플 저장고 */ char *when ; bool checknull = false ; bool isnull ; int ret, I ; /* CurrentTriggerData가 NULL이라면 아직 초기화되지 않았다는것을 의미 */ if (!CurrentTriggerData) elog(NOTICE, "trigf : triggers are not initialized") ; /* 실행기로 되돌릴 튜플을 지정한다. UPDATE 트리거시에는 새로운 튜플을 */ /* 지정하고, INSERT/DELETE 경우에는 트리거되는 튜플을 지정한다. */ if (TRIGGER_FIRED_BY_UPDATE(!CurrentTriggerData->tg_event)) rettuple = CurrentTriggerData->tg_newtuple ; else rettuple = CurrentTriggerData->tg_trigtuple ; /* 만일, INSERT/DELETE 트리거이고, BEFORE 트리거이면 NULL이 삽입되지 않도록 체크한다. */ if (TRIGGER_FIRED_BY_DELETE(!CurrentTriggerData->tg_event)) && TRIGGER_FIRED_BEFORE(!CurrentTriggerData->tg_event)) checknull = true ; /* 더 이상 트리거 데이터 구조에 접근할 일이 없으므로, NULL로 지정한다. */ CurrentTriggerData = NULL ; /* SPI 관리자로 접속을 성립한다. */ if ((ret = SPI_connect()) < 0) elog(NOTICE, "trigf (fired %s) : SPI_connect returned %d", when, ret) ; /* 테이블에 있는 튜플의 갯수를 얻는다. */ ret = SPI_exec("select count (*) from ttest", 0) ; if ( ret<0 ) elog(NOTICE, "trigf (fired %s) : SPI_exec returned %d", when, ret) ; /* SPI_tuptable 에는 ttest 테이블의 튜플의 갯수가 첫 번째 튜플에 들어있다. */ /* 이 갯수는 바로 위에서 실행한 "select count(*) from ttest" 의 결과이다. */ I = SPI_ getbinval(SPI_tuptable->vals[0], SPI_tuptale->tupdesc, 1, &isnull) ; elog(NOTICE, "trigf (fired %s) : there are %d tuples in ttest", when, i) ; SPI_finish() ; /* 만일 튜플의 값이 NULL 인가를 체크해야 할 필요가 있다면, 첫 번째 컬럼의 값을 검사해서*/ /* 그 값이 NULL 이라면, 튜플의 값을 NULL로 하여 실행기에서 처리하지 않도록 한다. */ if (checknull) { I = SPI_getbinval(rettuple, tupdesc, 1, &isnull) ; if (isnull) } /* 튜플을 실행기로 반환한다. */ return (rettuple) ; } |
이 프로그램을 testtrig.c 로 저장하고, 컴파일을 하도록 하자. 먼저, 트리거를 생성하기 이전에, 트리거가 의존하고 있는 테이블을 만들어 보자. 여기서는 동시에 트리거 함수를 등록한다. $ psql -c "create table ttest (x int4) ; create function trigf() returns opaque as >>''pwd'/testtrig.so' language 'c' ; " mydb
아래는 트리거를 생성하고, 실제로 테스트하는 내용이다. 트리거를 생성할 때, INSERT/UPDATE/DELETE 모두를 BEFORE/AFTER 에 하나의 로우마다 트리거 되도록 하였다. 잘 따라해보고 왜 그렇게 되는지 위의 소스와 비교해서 유심히 살펴보도록 하자. mydb=>create trigger tbefore before insert or update or delete on ttest for each >> row execute procedure trigf () ; CREATE mydb=>create trigger tafter after insert or update or delete on ttest for each >> row execute procedure trigf () ; CREATE mydb=> insert into ttest values(null) ; NOTICE : trigf (fired before) : there are 0 tuples in ttest INSERT 0 0 -- null을 INSERT 시도한 관계로 BEFORE 트리거에서 걸려서 실행기에게 null을 반환 하여 삽입이 되지 않았다. 아울러 AFTER 트리거에서는 실행기가 반환 값을 무시 된다. 따라서 아래에서 검색해 본 결과 삽입된 데이터는 없었다. mydb=> select * from ttest ; x - (0 row) mydb=> insert into ttest values (1) ; NOTICE : trigf (fired before) : there are 0 tuples in ttest NOTICE : trigf (fired after ) : there are 1 tuples in ttest 가시성 규칙에 따른 결과이다. INSERT 167793 1 -- SELECT 에는 트리거가 적용되지 않는다. mydb=> select * from ttest ; x - 1 (1 row) mydb=> insert into ttest select x * 2 from ttest ; NOTICE : trigf (fired before) : there are 1tuples in ttest NOTICE : trigf (fired after ) : there are 2 tuples in ttest 가시성 규칙에 따른 결과이다. INSERT 167794 1 mydb=> select * from ttest ; x - 1 2 (2 rows) mydb=> update ttest set x = null where x = 2 ; NOTICE : trigf (fired before) : there are 2 tuples in ttest UPDATE 0 mydb=> update ttest set x = 4 where x = 2 ; NOTICE : trigf (fired before) : there are 2 tuples in ttest NOTICE : trigf (fired after ) : there are 2 tuples in ttest UPDATE 1 mydb=> select * from ttest ; x - 1 4 (2 rows) mydb=> delete from ttest ; NOTICE : trigf (fired before) : there are 2 tuples in ttest NOTICE : trigf (fired after ) : there are 1 tuples in ttest NOTICE : trigf (fired before) : there are 1 tuples in ttest NOTICE : trigf (fired after ) : there are 0 tuples in ttest 가시성 규칙에 따른 결과 delete 2 mydb=> select * from ttest ; x - (0 row)
위에서 트리거되는 상황을 살펴보자. 먼저 trigf를 생성할 때, INSERT/UPDATE/DELETE 에 대해 BEFORE/AFTER 에 모두 트리거를 걸었다. 물론 SELECT 사건시에는 트리거를 걸 수 없다. INSERT 시에 BEFORE 트리거에서는 아직 삽입을 하지 않아서 삽입되는 데이터가 나타나지 않는다. 물론 AFTER 트리거시에는 이미 액션이 이루어진 후이므로 데이터가 나타난다. 사용자가 null을 INSERT/UPDATE을 통해 입력하려고 하면, BEFORE 트리거에서 검사되어서 삽입되지 않는다. 그리고 하나의 로우가 처리될 때 마다 두 개의 AFTER/BEFORE 트리거가 실행됨을 상기하자. 가령, 마지막의 "delete from ttest" 이전에 현재 두 개의 로우가 테이블에 들어있다. DELETE 구문에 의해 첫 번째 로우를 제거하기 이전에(BEFORE) 트리거가 실행되어서 현재 튜플안에 있는 로우(튜플)의 갯수를 출력한다. 삭제가 이루어진 다음, AFTER 트리거가 실행되어서 1개의 로우가 있음을 알린다. 다음 로우를 삭제하기 위해서 DELETE 가 적용되면 또다시 위와 같이 BEFORE 트리거와 AFTER트리거가 작용한다.
나오면서 지금까지 설명한 SPI 와 트리거는 저수준의 서버 프로그래밍 인터페이스였다. 사실 PostgreSQL 이 저수준에서 대단한 확장 가능성을 가지고 있다는 것은 관심있는 사람이라면 잘 알 것이다. PostgreSQL 내부에 정의되어 있는 데이터 구조체까지도 접근할 수 있다는 점은 깊이있는 데이터베이스 시스템 공부에 많은 도움이 된다. 하지만 PostgreSQL 은 아직 PL(프로시저언어) 쪽에서는 약점을 가지고 있다. 이것은 조만간 PostgreSQL 개발팀에 의해 보완이 될 것으로 보인다. 트리거와 SPI 는 꼭 필요한 곳에 제대로만 사용한다면 아주 유용한 도구가 될 수 있을 것이다. 일반 SQL 구문을 주로 사용하는 이들에게는 그다지 유용할 것 같지는 않지만 ... PostgreSQL 은 트리거와 유사한 규칙(RULE) 시스템을 가지고 있다. 이 규칙 시스템을 사용하면 트리거의 역할을 SQL 구문 차원에서 수행할 수 있다. 중요한 테이블의 내용을 갱신이 있을 때마다 백업한다든지 하는 작업에 주로 사용될 것이다. 하지만 현재의 규칙 시스템은 대단히 불안하기 때문에 사용을 권장하지는 않는다. 다만 그다지 중요하지 않은 개인적인 작업에 규칙 시스템을 사용하는 것은 괜찮을지 모르지만 회사의 중요한 작업을 처리하는 곳에는 규칙 시스템 대신 트리거와 SPI를 사용하도록 하자. 이번 호의 강좌는 이것으로 마친다.
|
RECENT COMMENT