블로그 이미지
자유로운설탕

calendar

1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31

Notice

2026. 3. 2. 18:47 프로그래밍

앞에서 얘기했듯이 SQLite 라는 로컬 전용 데이터베이스를 이용하여 책 대여 프로그램의 내부를 설계해 보자. 먼저 얘기했듯이 실제 설치를 해서 실습을 한다고 했는데 SQLite 를 사용하는 방법은 크게는 3가지 정도가 있다고 볼 수 있다. 첫 번째는 SQLite 실행 파일만 다운로드 해서 윈도우의 CMD 창이나 맥의 Terminal 에서 실행하는 방법이 있고(맥은 사실 이미 SQLite3 가 기본으로 설치되어 있긴 한다고 한다). 두 번째는 파이썬이나 자바, .NET 같은 프로그램을 이용해서 호출하는 건데 보통 이 경우는 기본 패키지에 포함되어 있거나, 추가로 패키지를 설치해 쓴다고 보면 될 듯 하고, 마지막 방법이 SQL 쿼리를 도와주는 "DB Browser for SQLite" 같은 DB 클라이언트 프로그램을 사용하는 거다.

사실 이런 SQLite 같은 작은 로컬 기반의 디비가 아닌 경우는 위의 3가지 중 어느 방법을 쓰더라도 보통 SQL 서버 자체를 별도로 구축해야 하는 경우를 빼 먹을 수는 없겠지만, SQLite 는 특별하게 비 설치 형으로 아주 작은 형태의 로컬 환경에서의 디비 제공을 지향하는 프로그램이기 때문에 위와 같이 간단한 방식이 가능하다고 보면 될 것 같다. 다른 종류의 데이터베이스와의 차이는 우선 여기서 SQLite 이해한 후 다음 얘기에서 한번 비교하여 풀어보자. .

사실 데이터베이스의 사용을 얘기하려면 위의 3가지 방법을 다 이해하는 게 맞긴 해서 어떻게 할지 고민 하다가, 우선은 앞의 SQLite 웹 사이트에서 실습 한 것과 비슷한 모습을 보여주는 "DB Browser for SQLite" 를 사용해 실습 해 보여주고, 뒤에 파이썬으로 프로그램을 만들어 조회를 해보거나, 마지막에 CMD 나 Terminal 창에서 실제 명령어를 날려 조회하는 방식으로 움직이는 부분을 살짝 보여주고 마무리 하려 한다. 사실 요즘 엄청 핫 하게 얘기 되고 있는 AI Agent 들도 결국은 저 CMD 나 나중에 Nosql 에서 얘기할 API 를 기반으로 복잡하게 얽혀 AI 모델을 중심으로 마치 하나의 프로그램 같이 생각 되도록 움직이는 것에 가깝다고 보기 때문에 명령어 방식도 꼭 한번 보여주고 넘어가 보려 한다. 앞에서 얘기했듯이 IT 세상에서는 이해가 안되고 신기하게만 보이면 안되기 때문이다. 

참고로 이젠 실습이기 때문에 굳이 한 땀 한 땀 따질 필요가 적고, 아는 지식을 기반으로 AI 검색 엔진을 사용해서 기본 쿼리 들을 만들게 할 건데, 이 글의 진행에서는 무료로 사용 가능한 Gemini Flash 버전을 비 로그인 상태에서 사용하고 있으니 참고 부탁 드린다.

앞에서 윈도우, 맥 상에서 모두 시연이 가능하도록 한다고 약속을 했으니, 우선 DB Browser for SQLite를 2개의 OS에서 설치하는 방법부터 보자. 일단 어느 OS 이던 구글에서 "db browser for SQLite" 를 우선 검색해 보자. 가장 처음에 나오는 "https://sqlitebrowser.org" 사이트를 클릭 해서, "Download" 페이지로 가보자. 

 

 



[윈도우 용 DB Browser for SQLite 설치 및 쿼리 실행 해보기]

먼저 윈도우의 경우는 실제 프로그램으로 설치, zip 파일을 해제해서 실행, 하나의 파일로 된 포터블 실행 파일 버전 3가지가 있다. 어느 걸 설치해도 무방하지만 혹시 직접 설치하는 게 좀 그렇다면, "DB Browser for SQLite - .zip (no installer) for 64-bit Windows" 를 다운로드 해서 아래와 같이 적절한 폴더에 압축을 풀어보자(개인적으로 c:\SQL 폴더를 만들어서 거기에 압축을 풀었다).

<Win - 압축 풀기>




이후 DB Browser for SQLite.exe 파일을 실행하자. 아래와 같은 빈 프로그램 화면이 나오면 성공했다고 보면 된다.

<Win - SQLite 실행>




처음이니까 쿼리를 만드는 데까지 자세히 설명하고, 이후부터는 원래 모드로 쿼리 위주로 다시 설명을 들어가려 한다. 우선 "새 데이터베이스" 버튼을 클릭해 보자. 컴퓨터에서 돌아가는 모든 프로그램들은 결국은 파일로 데이터를 저장하게 된다(물론 가끔 악성 코드 같은 경우는 메모리 상에서만 돌아가다가 없어지는 경우도 있지만, 메모리도 결국은 0과 1을 저장하는 휘발성 장소이기 때문에. 데이터의 구조를 저장한 다는 측면에서는 디스크에 저장되는 파일과 특별히 다르지는 않다고 본다. 파일이라는 개념 또한 가상의 0과 1의 배열을 이용하여 내용을 저장하기 위한 OS의 규칙이기 때문이다). 여하튼 현재 실행 폴더가 나오면 "mytest" 이름으로 저장해 보자.

<Win - 새 데이터베이스 만들기>

 



이후 "테이블 정의 변경"이라는 창이 나오는데(데이터베이스를 만들었으니 테이블을 하나 만들어 보라는 뉘앙스다), 이건 그냥 우선 취소를 해서 닫자. UI 를 이용해서, 테이블 생성하기 같은 버튼을 눌러서 테이블을 만들 수도 있겠지만, 우린 이미 쿼리를 배웠으니 그냥 바로 쿼리로 얘기를 해보도록 하자. 

위 쪽 메뉴에서 "SQL 실행" 탭을 누르면, 우리가 앞 글에서 봤던 웹 상의 편집기 같은 쿼리를 입력할 수 있는 메뉴가 나온다. 우리가 하나의 파일 형태의 데이터베이스를 만들었고, 이 SQLite 편집기의 특성 상 기본적으로 하나의 데이터베이스를 대상으로 작업하도록 되어있으니 앞으로의 모든 행동은 현재의 mytest 데이터베이스에 저장이 될 것이다(실제 프로그램이 실행된 폴더를 확인해 보면 "mytest.db" 라는 파일이 생성되어 있다. 이 파일을 나중에 다른 곳으로 옮기면 앞으로 작업하는 모든 데이터가 다 같이 옮겨가게 된다).

<Win - 쿼리 입력기>





그럼 테스트 테이블을 하나 만들어 보자. AI 한테 부탁해서 일련번호가 들어가는 과자 테이블을 하나 만들었자.

CREATE TABLE snacks (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL
);



전체에 대해서 명령을 날리려면, 입력한 내용을 아무 것도 선택하지 말고 F5키를 누르거나 위의 프린트 아이콘 오른쪽 옆에 있는 재생 모양의 화살표를 누르자. 이후 밑을 보면 아래와 같이 쿼리가 잘 실행 됐다는 결과 텍스트가 나온다. 

<Win - 테이블 만들기>




여기서 왼쪽에 있는 "데이터베이스 구조" 탭을 클릭해 보면 아무것도 없던 테이블 영역에 2개의 테이블이 생긴 것을 볼 수 있다. 컬럼의 데이터 형이나 속성들을 비주얼 하게 펼쳐서 볼 수도 있다(혹시나 sqlite_sequence 테이블이 왜 생겼는지 궁금하다면 마찬가지로 궁금해 찾아봤는데, AUTOINCREMENT 속성 땜에 각 테이블 별로 자동 증가하는 id 값이 현재 몇 번인지 저장해 두는 용도로 SQLite 에서 쓰는 내부 테이블이라고 한다).

<Win - 테이블 생성 정보>





다시 쿼리 쪽으로 가서 이번엔 과자 이름들을 넣어보자, 앞에서 설명했듯이, id 는 자동 증가되기 때문에 대상 컬럼 이름에서 빠져있다.

INSERT INTO snacks (name) VALUES ('새우깡');
INSERT INTO snacks (name) VALUES ('초코송이');
INSERT INTO snacks (name) VALUES ('포카칩');



마찬가지로 이제 굳이 촌스럽게 "데이터베이스 구조" 탭으로 가지 않아도 앞에서 쿼리 설명할 때처럼 머리 속으로 논리적으로 상상을 해보자. 네모난 "snacks" 테이블을 하나 만들고, 데이터 3줄을 넣었구나 하고 말이다.

<Win - 데이터 입력>




마지막 실습으로, 아래의 조회 쿼리를 마찬가지로 실행해 보면, 우리가 상상한 대로 데이터가 들어 있다는 것을 보여주게 된다. 매번 비어있던 가운데 공간이 예상 했던 대로 데이터 값이 조회 시 출력 되는 공간 이였었다.

SELECT * FROM snacks;

 

<Win - 데이터 조회>




하나 주의할 점은 마치 엑셀 작업한 내용을 저장하지 않음 날라가듯, 프로그램 종료 전에 저장을 안 하면 모든 내용이 없어지게 된다. "파일 > 변경사항 저장하기", "파일 > 프로젝트 저장하기(이건 현재 입력한 쿼리라든지 창 배치 등을 저장해 준다고 한다)"로 저장해 주거나, 종료 할 때 저장을 묻는 경우 모두 저장을 해주자. 간단히는 엑셀처럼 메모리 상에서 작업하다가 파일로 저장된다고 생각해도 무방할 듯 싶고, 조금 데이터베이스 입장으로 얘기로 하자면 트랜젝션(Transaction-일련의 전체 셋으로 꼭 성공해야 되는 쿼리 작업 단위)의 개념으로 생각해서 "rollback" 가능한 상태로 메모리 상에 존재하다가, 작업이 완전히 끝나서 "commit" 됐다고 보면 될 것 같다. 약간 SQLite 는 완전 파일 기반으로 표현하므로, 아마 해당 DB Browser 프로그램이 워드나 엑셀 같은 데이터의 "저장" 이라는 용어로 쉽게 다가가도록 표현된 듯 싶긴 하다. 

자 그럼 테스트를 완료하고, 다시 창을 띄우게 되면 다시 빈 공간만 보이게 되는데(최근 작업 파일을 가져오는 옵션이 있는지는 모르겠다), 커스터마이즈한 작업 공간을 복구하고 싶으면 "파일 > 프로젝트 열기"를 하여 "mytest.sqbpro" 파일을 읽어오고, 아까 저장한 데이터베이스를 가져와 다른 작업을 하고 싶다면 "파일 > 데이터베이스 열기"를 이용해서 아까 저장한 mytest.db를 가져와 보자. 해당 상태로 기존 만들었던 테이블과 데이터가 그대로 보이게 된다면, 앞으로의 실습 준비는 끝났다고 보면 될 듯 싶다.






[맥 용 DB Browser for SQLite 설치 및 쿼리 실행 해보기]

이번엔 맥북을 보자. 위의 윈도우 섹션은 당연히 안 보았으리라 생각하고 비슷하게 반복적으로 얘기를 적으니 양해 바란다. 마찬가지로 해당 페이지로 이동 해서, 다운로드 버튼을 눌러서 다운로드 후 프로그램을 더블 클릭해 어플리케이션으로 등록해 주자.

<MAC - 프로그램 설치>



이후 DB Browser for SQLite.exe 파일을 실행하자. 아래와 같은 빈 프로그램 화면이 나오면 성공했다고 보면 된다.DB Browser for sqliteDB Browser for sqliteDB Browser for sqlite 실행

<MAC - DB Brwoser for SQLite 실행>





처음이니까 쿼리를 만드는 데까지 자세히 설명하고, 이후 부터는 원래 모드로 쿼리 위주로 다시 설명을 들어가려 한다. 우선 "New Database" 버튼을 클릭해 보자. 컴퓨터에서 돌아가는 모든 프로그램들은 결국은 파일로 데이터를 저장하게 된다(물론 가끔 악성 코드 같은 경우는 메모리 상에서만 돌아가다가 없어지는 경우도 있지만, 메모리도 결국은 0과 1을 저장하는 휘발성 장소이기 때문에 디스크에 저장되는 파일과 특별히 다르지는 않다고 본다. 파일이라는 개념 또한 가상의 0과 1의 배열을 이용하여 내용을 저장하기 위한 OS의 규칙이기 때문이다). 폴더는 "Documents"로 하고 "mytest" 이름으로 저장해 보자.

<MAC - 새 데이터베이스 만들기>

 



이후 "Edit table definition"이라는 창이 나오는데(데이터베이스를 만들었으니 테이블을 하나 만들어 보라는 뉘앙스다), 이건 그냥 우선 취소를 해서 닫자. UI 를 이용해서, 테이블 생성하기 같은 버튼을 눌러서 테이블을 만들 수도 있겠지만, 우린 이미 쿼리를 배웠으니 그냥 바로 쿼리로 얘기를 해보도록 하자. 

위 쪽 메뉴에서 "Execute SQL" 탭을 누르면, 우리가 앞 글에서 봤던 웹 상의 편집기 같은 쿼리를 입력할 수 있는 메뉴가 나온다. 우리가 하나의 파일 형태의 데이터베이스를 만들었고, 이 SQLite 편집기의 특성 상 기본적으로 하나의 데이터베이스를 대상으로 작업하도록 되어있으니 앞으로의 모든 행동은 현재의 mytest 데이터베이스에 저장이 될 것이다(실제 저장한 Documents 폴더를 보면 "mytest.db" 라는 파일이 생성되어 있다. 이 파일을 나중에 다른 곳으로 옮기면 앞으로 작업하는 모든 데이터가 다 같이 옮겨가게 된다).

<MAC - 쿼리 입력기>





그럼 테스트 테이블을 하나 만들어 보자. AI 한테 부탁해서 일련번호가 들어가는 과자 테이블을 하나 만들었다.

CREATE TABLE snacks (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL
);



전체에 대해서 명령을 날리려면, 입력한 내용을 아무 것도 선택하지 말고 F5키를 누르거나 위의 프린트 아이콘 오른쪽 옆에 있는 재생 모양의 화살표를 누르자. 이후 밑을 보면 아래와 같이 쿼리가 잘 실행됐다는 결과 텍스트가 나온다. 

<MAC - 테이블 생성>



여기서 왼쪽에 있는 "Database Structure" 탭을 클릭해 보면 아무것도 없던 테이블 영역에 2개의 테이블이 생긴 것을 볼 수 있다. 컬럼의 데이터 형이나 속성들을 비주얼 하게 펼쳐서 볼 수도 있다(혹시나 sqlite_sequence 테이블이 왜 생겼는지 궁금하다면 마찬가지로 궁금해 찾아봤는데, AUTOINCREMENT 속성 땜에 각 테이블 별로 자동 증가하는 id 값이 현재 몇 번인지 저장해 두는 용도로 SQLite 에서 쓰는 내부 테이블이라고 한다).

<MAC - 테이블 만든 후>





다시 쿼리 쪽으로 가서 이번엔 과자 이름들을 넣어보자, 앞에서 설명했듯이, id 는 자동 증가 되기 때문에 대상 컬럼 이름에 빠져있다.

INSERT INTO snacks (name) VALUES ('새우깡');
INSERT INTO snacks (name) VALUES ('초코송이');
INSERT INTO snacks (name) VALUES ('포카칩');



마찬가지로 이제 굳이 촌스럽게 "Database Structure" 탭으로 가지 않아도 앞에서 쿼리 설명할 때처럼 머리 속으로 논리적으로 상상을 해보자. 네모난 "snacks" 테이블을 하나 만들고, 데이터 3줄을 넣었구나 하고 말이다.

<MAC - 데이터 입력>





마지막 실습으로, 아래의 조회 쿼리를 마찬가지로 실행해 보면, 우리가 상상한 대로 데이터가 들어 있다는 것을 보여주게 된다. 매번 비어있던 가운데 공간이 예상 했던 대로 데이터 값이 조회 시 출력 되는 공간 이였었다.

SELECT * FROM snacks;

<MAC - 데이터 조회>




하나 주의할 점은 마치 엑셀 작업한 내용을 저장하지 않음 날라가듯, 프로그램 종료 전에 저장을 안하면 모든 내용이 없어지게 된다. "File > Write Changes", "File > Save Project(이건 현재 입력한 쿼리라든지 창 배치등을 저장해 준다고 한다)"로 저장해 주거나, 종료할때 저장을 묻는 경우 모두 저장을 해주자. 간단히는 엑셀 처럼 메모리 상에서 작업하다가 파일로 저장된다고 생각해도 무방할 듯 싶고, 조금 데이터베이스 입장으로 얘기로 하자면 트랜젝션(Transaction-일련의 전체 셋으로 꼭 성공해야 되는 쿼리 작업 단위)의 개념으로 생각해서 "rollback" 가능한 상태로 메모리 상에 존재하다가, 작업이 완전히 끝나서 "commit" 됬다고 보면 될 것 같다. 약간 SQLite 는 완전 파일 기반을 표현하므로, 아마 해당 DB Browser 프로그램이 워드나 엑셀 같은 "저장" 이라는 용어로 쉽게 다가가도록 표현된 듯 싶긴하다. 

자 그럼 테스트를 완료하고, 다시 창을 띄우게 되면 다시 빈 공간만 보이게 되는데(최근 작업 파일을 가져오는 옵션이 있는지는 모르겠다),, 커스터마이즈한 작업 공간을 복구하고 싶으면 "File > Open Project"를 하여 "mytest.sqbpro" 파일을 읽어오고, 아까 저장한 데이터베이스를 가져와 다른 작업을 하고 싶다면 "File > Open Database"를 이용해서 아까 저장한 mytest.db를 가져와 보자. 해당 상태로 기존 만들었던 테이블과 데이터가 그대로 보이게 된다면, 앞으로의 실습 준비는 끝났다고 보면 될 듯 싶다.




 



혹시나 나중에 MSSQL이나, Oracle, MySQL, PostgreSQL 등 여러가지 파일이 아닌 서버(서비스) 형태의 디비를 쓰더라도 마찬가지로 이런 비슷한 쿼리를 날릴 수 있는 SQL 클라이언트(SQL 서버에 쿼리를 날리는 고객이라서 SQL 클라이언트다)를 설치하거나, 클라우드라면 내부 또는 SAAS 서비스로 제공 되는 여러 툴 들을 이용해서 쿼리를 비슷하게 날리게 된다. 

나중에 얘기할 NoSQL 도 좀 더 개발 쪽 친화적이여서 모양만 낯설 뿐 비슷하다고 보면 된다. 다만 서버 형태의 데이터베이스들의 경우는 원격의 서비스 이기 때문에, 앞의 데이터베이스 파일 열기 같은 형태가 아니라 마치 우리가 웹 사이트에 로그인 할 때처럼 보통 ID, Password 를 넣어서, SQL 서버에 로그인 후 해당 세션을 유지하며 쿼리를 날리게 되고, 이 것을 정보를 입력하면 해당 클라이언트가 그런 자잘한 일들은 자동으로 해주게 된다. 

자 그럼 실습을 할 수 있는 환경은 준비가 됐으니 이젠 다시 쿼리의 세계로 여행을 떠나보자. 앞에서 얘기한 대로, 프로그램하고 연결은 안 되 있지만 데이터베이스와 쿼리를 이용해서 책 대여점 프로그램을 만들어 보자(뭐 책 대여점이라는 개념이 낯설다면, 돈을 주고 빌려주는 유료 도서관 시스템이라고 생각해 보자). 

시작하기 전에 잠시 프로그램의 세계를 엿보자면 어느 순간부터 프론트엔드, 백엔드로 경계를 나눠서 얘기하고 있었는데, 지금은 다시 어느 쪽이든 API 를 기준으로 한 비동기 프로그램이 대세가 되고, 프론트엔드도 점점 엄격한 객체지향의 세계로 변하게 되면서, 구경하는 서당 개의 입장에서 보면 두 개의 프로그램 스타일의 경계가 다시 무너지는 느낌도 들긴 하지만, 뭐 굳이 따지자면 데이터베이스 관련된 테이블과 쿼리의 설계는 백엔드 쪽에 속한다고 볼 수는 있을 것이다. 사실 디비를 중심으로 한 쿼리 중심으로 보면 외부의 프로그램 코드는 해당 쿼리들을 적절히 설계 후에 비즈니스 규칙에 맞춰서 쿼리 들을 UI 의 요청에 맞춰 적절히 호출하는 일을 주로 담게 된다.

물론 ORM(Object-Relational Mapping) 같은 방식으로, 구조적으로 디비를 정의해 User.find(1) 이런 식으로 쿼리를 자동으로 만들어주어 개발자 쪽에서 쿼리를 인지 못하게 할 수도 있지만 내부적으로는 결국 마지막에는 데이터베이스에 쿼리를 날리는 SQL 이 실행된다는 부분에서는 SQL 을 이해해야 한다는 결론에는 크게 다르진 않는 듯도 싶다. 음 뭐 나중에는 해당 부분의 번역 또한 AI 실시간으로 실행 시 해주게 되면 상황이 변하게 될 것은 같긴 하지만, 어쨌든 모든 결과의 실행은 실제로 데이터베이스 입장인 SQL 세상에서 일어나게 된다. 

요즘 핫한 OS 에서 돌아가는 AI Agent 들도 결국은 맨 마지막에서는 구닥다리 윈도우 API 나 CLI, 타 솔루션의 API 등을 호출해서 정교한 명령을 수행하게 될 테니까 말이다. 뭐 언젠가 AI 가 더 발전해서 세상의 모든 기술이 굳이 인간이 이해할 필요가 없는(아마 무생물인 AI 관점에서는 가독성에 치중된 꽤 비효율적인 인터페이스 일 수도 있을테니 말이다) 바이너리 같은 코드와 기술로 다 컨버전되어 바뀌어 버리기 전에는 이건 당분간 바뀌진 않을 것 같다(아마 현실 측면에서는 구현된 기술의 저작권 문제도 있을 거다). 아마도 그래서 이러한 미래의 레거시 후보가 될 뒷 세계의 모양을 아는 것이 현재 정확하게 AI 가 미치는 세상을 이해하는데 도움이 될 것 같긴 하다. 앞에서 얘기했지만 현재 얘기하는 SQL 도 그런 측면에서 이해하는데 의미가 있다. 다시 한번 의견을 말하자면 IT 쪽에서 기술이 이해하기 힘든 마법같이 보임 절대 안 된다고 생각한다. 뭐 약간의 발전을 위한 긍정적인 환타지는 허용되야 할테지만 말이다.

그럼 다시 현실로 돌아와서 책 대여점 프로그램에 어떤 게 필요할지 생각을 해보자. 자 우선 그전에 우리가 사용하고 있는 RDB(Relational Database) 가 뭔지 생각해 보자. 보통 복잡하게 얘기하긴 하는데 야매 입장에서 보면 RDB 가 처음 나왔던 시절은 디스크 등의 용량이 엄청 비쌌던 시절이다. 몇 십 메가의 하드가 주력이던 시절이니 말이다(WD Caviar 93044 같은 모델이 40MB 라고 한다). 그래서 그 당시 중복된 데이터를 저장한다는 것은 아마 엄청 사치스러운 접근법이였을테고, 기본적으로 중복되지 않은 데이터를 저장하는 것을 보통 정규화라는 조금 어려운 학술 표현으로 쓰는 것 같다. 

그래서 지금의 많은 RDB 테이블에서 볼 수 있는 상품 정보, 주문 정보 등의 테이블로 분리되면서 실제 길이가 긴 상품 명은 상품 테이블에만 저장되고 주문 정보 테이블에서는 상품 번호만 저장하는 식으로 중복 데이터를 최소화 하는 식으로 저장되었고, 주문 정보에서 상품 정보를 같이 보여주기 위해서 필연적으로 우리가 골치 아파하는 JOIN 키워드와 빠른 JOIN을 도와주는 인덱스라는 존재가 꼭 필요하게 됐다고 볼 수 있다(현재의 NoSQL이나 현대적인 RDB 설계의 측면에서는 이 부분을 역으로 접근했다고 생각해 보자). 

이 방식은 하나의 얘기치 않은 장점도 있는데, 중요한 키워드 데이터들이 수많은 테이블 중에 보통 한 군데에만 들어가므로(예를 들면 상품 가격은 상품 테이블에만 있거나 또는 상품 코드를 가진 가격 테이블에만 있을 수 있다), 중복된 데이터 중 특정 컬럼 데이터의 업데이트를 놓쳐서 생기는 무결성의 깨짐 부분을 방지하는 효과를 가져온다. 또한 일반적으로 단일 서버 기준으로 데이터를 저장하므로 아무래도 무결성을 유지하면서도 빠르다. 

NoSQL 은 JOIN 같은 장점과 무결성을 조금은 포기하면서 서버를 병렬적으로 늘려 지금의 AI가 나온 빅데이터를 가능케 한 접근 방식이라고 보면 될 것 같고, 요즘은 두 개를 합쳐 다수의 서버로 속도와 무결성을 유지하려 하는 NEWSQL 이라는 서버군 들도 생겨나고 있다고 한다(설명을 보면 약간 NoSQL와 정해진 서버 들까지의 투표로 진위를 가리는 기업 용 블록체인 장부가 합쳐진 느낌이라고 할까 싶다). 여하튼 뭐 이런 확장된 부분들은 관심이 있다면 직접 해당 서버들을 사용하거나 구축하면서 장 단점을 느껴보면 되지 않을까 싶다.


 



자 그럼 중복된 데이터를 저장 안 하려 하는 RDB 의 특징을 생각하면서 책 대여 프로그램의 뒷 편 설계를 해보자. 

<책 대여 하기>




먼저 책을 저장하는 테이블이 하나 있음 좋겠다. 다른 테이블과 조인할 때 쓸 수 있는 고유 일련 번호는 당연히 있어야 하고, 책 이름도 있어야 한다. 대여 가격도 포함해 볼까 했지만 그럼 같은 금액이 엄청 많이 있을 텐데, 나중에 책마다 금액을 바꾸고 싶어 할 경우 책이 많아진다면 뭔가 엄청 많은 데이터를 업데이트 해야 될 수도 있어 보여서 일종의 책 종류 코드를 대신 넣고 가격 테이블은 책 종류 코드에 따라 부여되게 별도로 저장해 보려 한다. 그럼 RDB 의 기본 목표처럼 저장되는 데이터 크기도 아끼고, JOIN 쪽도 그다지 부담은 안 될 것 같다. 

책 코드에 따라서 대여기간 등도 조정 가능하도록 테이블로 관리할 수 있을 것 같다(이걸 프로그램 로직에서 관리할지 데이터베이스에 넣을지 고민을 할 수도 있겠지만, 프로그램을 새로 만드는 것보다는 데이터베이스의 값을 간단히 업데이트 하는 게 편할 듯 해 보인다. DBA 쪽에서 변경 시 작업해야 되는 번거로움은 생길지 모르겠지만 보통 이런 중요한 값이라면 관리되는 admin 페이지가 있어서 감사 로그와 함께 저장될 가능성이 높다).

회원 가입 시 받아야 할 정보도 있을텐데, 현재 시점에서 꼭 필요한 개인정보만 생각하면 이름, 고유정보를 체크하거나 연체 독촉 시 사용할 핸드폰 전화번호만 생각하면 될 것 같다. 직접 입력하는 회원 아이디도 굳이 이쪽에서 찾을 때 필요한 부분이니 필요가 없을 듯하다, 나머지 추가적인 정보 들은 차후 고민해 보자.

책을 빌려주게 되는 상황이 되면 대여에 관한 테이블이 하나 필요할 것 같다. 대여 번호가 고유키 일거고, 조회 시 중요한 필드는 회원 아이디가 될 것 같고, 들어가는 값들을 책의 일련번호, 현재 날짜가 들어가는 대여 시작일, 대여 종료 일은 책 종류에 따라 대여 일수를 정하려 하니(만화책 3일, 소설 5일 등) 아까의 책 가격 테이블을 확장 시켜서, 책 코드 별로 가격에 대여 기간도 같이 저장해 보자. 나중에 대여 기간을 연장 한다던지 하는 여러 기능이 추가될 수도 있고, 특정 시기에 대여 기간 정책을 바꿀 수 있기 때문에 시작 일과 종료 일은 명확히 하는 게 좋겠다. 또 해당 대역의 상태를 관리하기 위해서 하나의 상태 코드를 만들면 좋을 거 같다. 처음 대여 했을 때는 Rent 로 했다가 반납하게 되면 Return 상태가 되도록 하게 말이다. 음 연체료를 계산하거나 실제 반납 일이 다른 수 있으니 반납 일도 하나 넣는 것도 좋겠다. 

이렇게 생각하다보니 거래나 너무 많아지게 되면 나중에 테이블에서 이런 부분을 조회하는 부분이 부담이 될 것도 같지만 현재 프로그램의 특성 상 그렇게 많은 데이터가 쌓이진 않을 듯 하니, 반납이 완료된 히스토리 테이블을 따로 분리하는 건 차후에 고민하도록 해보자.

 



위와 같은 고민으로 AI 한테 부탁할 테이블 정보를 정리해 봤다. 아무래도 모두 우리말로 적다보면 테이블이나 컬럼 이름 같은 걸 너무 딱딱하게 적어서 맘에 안 드는 경우가 있어서, 지금 까지 배운 쿼리 지식을 이용해서 하나하나 최대한 정의를 해봤다. AI 가 알아서 잘 해주는 거 같은 Foreign 키 정의 같은 건 일단 그냥 빼놓자.

책 대여하는 프로그램을 sqlite 로 설계를 해보려고 해. 아래와 같은 테이블 들을 간단히 만들고 싶어.

1) 일단 책을 저장하는 테이블은 book 으로 하고, 책 일련번호(book_no), 책 제목(book_name), 책 타입(book_type - 나중에 가격이나 대여기간을 여기다 넣으려해) 컬럼을 만들어줘. 책이 대여나 파손, 또는 불가 상태가 되면 대여 가능 상태 여부를 알 수 있게 status 컬럼도 하나 만들어 줄래? 여기서 아마 고유 값은 book_no 만 있을 거 같네.

2) 책 종류에 대한 대여 정책 정보를 관리하는 book_policies 테이블을 만들고, 여기는 책 타입(book_type) 과 가격(price), 대여 기간(rental_period) 컬럼을 만들어줘. 아 연체시 하루 연체료(penalty_per_day) 도 넣어야 할 거 같네. 나중에 혹시 코드별 가격이나 대여기간 변경이 필요하면 이 테이블을 수정하려 해. 여기서는 book_type이 고유 값이 될거 같아.

3) 회원정보(customers) 테이블은 최소한의 개인정보만 저장하려해서 회원 고유 아이디(customer_id), 회원 이름(name), 한국 핸드폰 번호기준(phone), 회원 가입일(reg_date)만 넣으려해. 이름은 중복이 가능하니 핸드폰 번호(또는 이름+핸드폰 번호)가 아마 손님이 왔을때 검색하는 고유키가 될거 같네. 나머지는 기능이 확장되면 고려해 보려고. 여기서는 customer_id 와 phone이 중복이 있음 안 될거 같아.

4) 책 대여 테이블(book_rentals) 을 만들려 하는데, 대여 일련번호(rental_no), 회원 아이디(customer_id)가 주요 where 조건 키 일 거 같고(하지만 여러권을 책을 빌릴 수 있으니 회원 아이디는 중복은 있을 거야), 책 일련번호(book_no)와 나중에 혹시 대여기간 등이 변경 될수도 있으니 빌려줄 당시의 대여 시작일(rental_start_date)과 대여 종료일(rental_end_date)을 넣고 싶어. 또 대여, 반납, 취소 등의 상태를 관리하는 상태 코드(status)와 반납일(return_date)도 있음 좋겠어. 거래가 완료된 건만 타 테이블로 분리해 이관하는건 나중에 데이터가 많아짐 고민해 보려고. 여기서는 rental_no 이 중복 값이 없을 거 같아.

5) 조금 과한거 같긴 하지만 매출 등의 계산을 위해서 book_borrow_history 테이블을 만들려해 하다가 그냥 대여 테이블(book_rentals)에서 조회 할까도 싶은데 어떻게 생각해?

 




자 그럼 위의 내용을 기반으로 AI 가 만들어 준 테이블 정보를 보면서 우리가 의도한 대로 만들어 줬나 생각해 보자(이걸 직접 해봤던 사람들은 이런 게 얼마나 귀찮고 인형 눈 끼우는 거 같은 일인지 잘 알고 있을 것이다.

1) 결과적으로 히스토리 테이블은 나중에 규모가 커지면 만들라고 하고, 아래와 같이 4개의 테이블을 만들어 주고, 앞에서 얘기한 데이터가 많을 경우 검색을 빠르게 하기 위한 인덱스도 추천해 줬다. 



2) 책 정책 테이블은 책 타입을 고유 값("PRIMARY KEY") 으로 해서 중복을 막으면서 인덱스를 만들고, 가격이나 기간은 연체료는 꼭 입력 시 있도록 강제 하기 위해서 "NOT NULL" 코드가 들어갔다. SQLite 는 데이터 형태가 간단해서 글자는 TEXT, 숫자는 INTEGER 로 정의했다.

CREATE TABLE book_policies (
    book_type TEXT PRIMARY KEY,        -- 책 타입 (고유 값)
    price INTEGER NOT NULL,             -- 대여 가격
    rental_period INTEGER NOT NULL,     -- 대여 기간 (일 수)
    penalty_per_day INTEGER NOT NULL    -- 연체료 (하루당)
);




3) 책 정보 테이블은 책 일련 번호를 고유 값으로 하고, 자동("AUTOINCREMENT") 증가 되게 했다. book_type 은 외래키("FOREIGN KEY")로 해서 book_policies 에 들어 있지 않는 값이 못 들어 가도록 방지 했다. status 는 적당히 대화를 해석해서 네 개의 상태 값(가능, 대여 중, 훼손, 분실)이 들어가는 걸로 가정했는데, 뭐 그냥 실습이니 나쁘지 않아 보이니 넘어가자. 또한 status 는 빈 값이 들어올 경우 "AVAILABLE" 이 들어간다.

CREATE TABLE book (
    book_no INTEGER PRIMARY KEY AUTOINCREMENT, -- 책 일련번호
    book_name TEXT NOT NULL,                   -- 책 제목
    book_type TEXT,                            -- 책 타입 (외래키 후보)
    status TEXT DEFAULT 'AVAILABLE',           -- 상태 (AVAILABLE, RENTED, DAMAGED, LOST)
    FOREIGN KEY (book_type) REFERENCES book_policies(book_type)
);



여기서 하나 좀 애매한 부분이 있는데, status 와 book_type 이 "NOT NULL" 조건이 없다는 부분이다. 아마도 AI 생각을 추측해 보면 status 는 default 가 있어서 값을 안 넣어도 'AVAILABLE' 을 자동으로 넣어주어 괜찮고, book_type 은 외래키가 이상한 값을 막아 줄거라고 생각해서 뺀 거 같은데, 현실 적으로 보면 아래와 같은 쿼리에 대해서 실제 NULL 이 들어가는 이슈가 있을 수 있다(궁금하면 위의 테이블의 이름을 book_test 로 만들어서 직접 넣어보자).

INSERT INTO book (book_name, status) VALUES ('SQL 테스트', NULL);

<NULL 입력 테스트>

 

그래서 명시적으로 2개의 필드도 not null 이 들어가는 게 맞을 것 같고, 해당 부분을 다시 AI 한테 물어보니 그게 맞을 것 같다고 의견을 조정했다. 그럼 아래와 같이 조정할 수 있다. 대신 이 경우는 하나의 단점이 status 를 꼭 지정해 넣어줘야 한다는 부분이다. null 어떻게든 안 넣거나 막을 자신이 있으면 "NOT NULL"을 빼는 것도 하나의 선택지일 것도 같다.

CREATE TABLE book (
    book_no INTEGER PRIMARY KEY AUTOINCREMENT, -- 책 일련번호
    book_name TEXT NOT NULL,                   -- 책 제목
    book_type TEXT NOT NULL,                            -- 책 타입 (외래키 후보)
    status TEXT NOT NULL DEFAULT 'AVAILABLE',           -- 상태 (AVAILABLE, RENTED, DAMAGED, LOST)
    FOREIGN KEY (book_type) REFERENCES book_policies(book_type)
);





4) 회원 정보 테이블도 날짜에 NOT NULL 이 없어서 추가했고, 나중 코드를 보면 알겠지만 SQLite 는 날짜 형이 실제 없다고 하고, 쿼리 상에서는 날짜 같이 취급되지만 실제로는 TEXT 로 저장된다고 한다. 가벼운 효율성을 위해서 그랬다나 한다. 여기도 중복 안 되야 하는 값을 고유 값을 하고, 핸드폰의 경우는 고유 값은 아니지만(이걸 고유 값으로 잡으면 개인정보를 모든 다른 테이블의 키로 잡아야 해서 당연히 적절치 않아 보인다) 겹치는 값이 들어가면 안 되는 실제의 내부 고유 값 이여야 하므로, 겹치는 값이 못 들어가는 제한을 주기 위해 "UNIQUE" 를 넣었다. 

CREATE TABLE customers (
    customer_id INTEGER PRIMARY KEY AUTOINCREMENT, -- 회원 고유 아이디
    name TEXT NOT NULL,                            -- 이름
    phone TEXT UNIQUE NOT NULL,                    -- 핸드폰 번호 (중복 불가)
    reg_date DATE NOT NULL DEFAULT (DATE('now'))            -- 회원 가입일
);





5) 대여 테이블도 마찬가지로 status 에 "NOT NULL"을 추가했고, return_date 야 말로 빌려주자마자 반납 하는 경우는 당연히 없을 테니, 나중에 필요한 순간에 날짜가 들어가게 NULL 허용이 되게("NOT NULL" 이 없음 허용이다) 했다. 마찬가지로 똑똑하게 customer_id, book_no 는 회원 테이블과 책 정보 테이블에 없는 값은 안 들어가도록 외래키를 걸어줬다.

CREATE TABLE book_rentals (
    rental_no INTEGER PRIMARY KEY AUTOINCREMENT, -- 대여 일련번호
    customer_id INTEGER NOT NULL,                -- 회원 아이디
    book_no INTEGER NOT NULL,                    -- 책 일련번호
    rental_start_date DATE NOT NULL,             -- 대여 시작일
    rental_end_date DATE NOT NULL,               -- 대여 예정일
    return_date DATE,                            -- 실제 반납일
    status TEXT  NOT NULL DEFAULT 'RENTED',                -- 상태 (RENTED, RETURNED, CANCELLED)
    FOREIGN KEY (customer_id) REFERENCES customers(customer_id),
    FOREIGN KEY (book_no) REFERENCES book(book_no)
);





6) 이 밖에 status 값도 AVAILABLE, RENTED, DAMAGED, LOST 네 개로 제한(CHECK (status IN ('AVAILABLE', 'RENTED', ...)) 하자는 의견도 있었는데, 이건 안정성 보다는 상태가 새로 생길 때마다. 제약 조건을 업데이트 해야 하는 부분이 걸려서 그대로 두기로 한다. 

7) 회원이 방문했을 때 찾는 키가 이름하고 폰 번호일 것 같아서 아래와 같이 이름하고 전화번호 컬럼에 인덱스를 거는 것도 제안 했는데, 이것도 결국은 실제에서는 전화번호 뒷자리 4자리로 찾는 게 개인정보 보호 상 현실적일 것 같아서, 나중에 전화번호 뒷자리 컬럼을 하나 더 만드는 게 날 것 같긴 해서 실습에서는 넘어간다.

CREATE INDEX idx_customer_search ON customers(name, phone);

여하튼 AI 쪽에서는 범용적인 관점에서 내가 빼놓은 걸을 잘 체크해 주는 편이므로, 해당 부분을 잘 이용해서 비어 있는 부분을 잘 채우고 자신과 AI 둘 다 괜찮다고 생각하는 협의 점을 찾아보는 게 어떨까 싶다.

 

테이블 생성 후 데이터베이스 구조를 보면 아래와 같다. 중간에 null 테스트를 안 해봤다면 book_test 테이블은 없을 거다

<책 대여 프로그램 - 테이블 생성 후>








그 다음에 테이블이 잘 만들어 진 것을 증명하고, 차후 프로그램에서 이런 저런 쿼리를 날렸을 때 잘 되려는 지 검증을 위해서 책을 등록 하고, 고객을 등록하고, 대여하고, 반납하는 간단한 시나리오에 대한 쿼리를 만들어 보자. 위의 테이블 들을 설계하면서 머리 속에 대충 테이블들이 이제는 그려졌을 테니, 그걸 기반으로 아래와 시나리오를 만들어 봤다.

 시나리오를 한번 설명해 줄께. 관련된 쿼리를 만들어 줄래? not null 인 영역에 default 값이 있다고 빼먹고 그러진 말아줘. 그리고 상태 값이나 타입 값은 소문자로 부탁해
1) 책을 만화책(comic)과 소설(novel) 로 나눔. 각각 대여료는 500원, 1000원, 대여기간은 3일, 5일, 연체료는 하루에 100원, 200원 이야.
2) 책이 2권 들어왔는데 원피스(만화)랑, 채식주의자(소설)이야. 이 책들을 대여가 가능하게 등록 해야되.
3) 회원의 두 명 가입했는데 "홍길동"이 "010-1111-1111" 로 가입 했고, "김고객"이 "010-2222-2222" 로 가입 했어.
4) 이후 "홍길동"이 찾아와서 "이름, 전화번호 뒷자리로" 회원을 확인하고 원피스를 빌려갔어
5) 근데 10일 후에 책을 반납 했네. 7주일치 연체료를 계산해야 되.
6) 연체료를 받은 후에는 책을 반납해서 완료하려고.




AI가 만들어 준 각각의 코드를 봐보자.

1) 책을 만화책(comic)과 소설(novel) 로 나눔. 각각 대여료는 500원, 1000원, 대여기간은 3일, 5일, 연체료는 하루에 100원, 200원 이야.
얘기한 대로 book_policies 테이블에 대여료, 기간, 연체료 기준으로 입력해 준다.

INSERT INTO book_policies (book_type, price, rental_period, penalty_per_day)
VALUES 
('comic', 500, 3, 100),
('novel', 1000, 5, 200);


넣은 데이터를 조회해 보자
select * from book_policies

 

<책 대여 프로그램 - 책 정책>




2) 책이 2권 들어왔는데 원피스(만화)랑, 채식주의자(소설)이야. 이 책들을 대여가 가능하게 등록 해야 되.

관련 책을 넣으면서 상태를 기본 상태인 'available' 로 만들어 준다.

INSERT INTO book (book_name, book_type, status)
VALUES 
('원피스', 'comic', 'available'),
('채식주의자', 'novel', 'available');


넣은 데이터를 조회해 보자.
select * from book

<책 대여 프로그램 - 책 정보>

 



3) 회원 정보를 넣는다. 가입 일은 현재 날짜로 넣는다.

INSERT INTO customers (name, phone, reg_date)
VALUES 
('홍길동', '010-1111-1111', DATE('now')),
('김고객', '010-2222-2222', DATE('now'));



넣은 데이터를 조회해 보자.
select * from customers

<책 대여 프로그램 - 회원 정보>




4-1) 사실 원래의 프로그램이라면 WHERE 조건에 좀더 간단해 질 수도 있고(이미 로그인 할때 customer_id를 가져와서 그걸 계속 사용한다거나), select 를 통해서 정보를 조회헤 온후, 해당 정보를 기반으로 insert 할 수도 있을 것 같다. 여기서는 하나의 쿼리로 나타내기 위해서 아래와 같이 insert 와 select 가 결합된 형태로 나타났다. "vaulus" 키워드가 사라진게 좀 특이해 보인다. 날짜는 SQLite 에서 지원하는 방식으로 현재(DATE('now'))와 현재에 대여기간을 더 한 형태(DATE('now', '+' || p.rental_period || ' days'))로 보여준다. 참고로 이런 날짜를 계산하는 방식은 데이터베이스마다 보통 조금씩 다르다.

INSERT INTO book_rentals (customer_id, book_no, rental_start_date, rental_end_date, status)
SELECT 
    c.customer_id, 
    b.book_no, 
    DATE('now'), 
    DATE('now', '+' || p.rental_period || ' days'),
    'rented'
FROM customers c
JOIN book b ON b.book_name = '원피스'
JOIN book_policies p ON b.book_type = p.book_type
WHERE c.name = '홍길동' AND c.phone LIKE '%1111'
LIMIT 1;


넣은 데이터를 조회해 보자.
select * from book_rentals

<책 대여 프로그램 - 대여 정보>


4-2) 추가로 도서 상태도 대여로 바꾸어줘 보자.

UPDATE book 
SET status = 'rented' 
WHERE book_name = '원피스';


업데이트 된 데이터를 조회해 보자.
select * from book WHERE book_name = '원피스';

<책 대여 프로그램 - 대여 상태 변경>




5) 연체료 계산을 하다보니 테스트로 만든 쿼리라고 강제로 빌린 기간에 10일을 더 해서(DATE(r.rental_start_date, '+10 days')), 반납일로 정해 봤다. 그러다 보니 조금 헷갈리긴 할 듯 하다. 지난 기간을 체크하기 위해서 날짜를 JULIANDAY 라는 함수로 변환 후 다시 CAST 하는데, JULIANDAY 는 "기원전 특정일" 로 부터 흐른 날이라고 하고, CAST AS INTEGER는 원래 해당 값이 실수로 나올 수 있는데, 그걸 소수점을 버리는 거다. 여러 현실 적인 부분에서 연체로 계산하는 날짜 계산 방식이 바뀔 수 있는데(뭐 예를 들면 밤 11시에 빌리나 오전 11시에 빌리나 다음날 반납해야 되는 불공평함을 어떻게 개선해 본 다든지), 그 경우는 현재 날짜만 저장하는 DATE 를 DATETIME 으로 바꾸거나 하는 일이 생길 수도 있다. 여하튼 모든 건 목적을 위해서 설계하기 나름이라고 보자.

밑의 쿼리는 WHERE 조건에서는 약간 단정적으로 "홍길동"이라는 사람이 "원피스"를 빌려갔다고 가정하는데, 사실 현실에서는 이름은 고유 값이 아니기 때문에 특정 customer_id 의 현재 미 반납 목록 이라든지 그런 걸 볼 수 있으므로, WHERE 조건은 얼마든지 바뀔 수는 있어 보인다. 오히려 c.name은 고유 값이 아니므로 저렇게 사용하기가 좀 위험해 보이긴 하는데, 스크립트와 결과에 맞춰 쿼리를 만들어 주다 보니 그렇지 않을까 싶긴 한다.

SELECT 
    c.name AS 회원명,
    b.book_name AS 도서명,
    r.rental_end_date AS 반납예정일,
    DATE(r.rental_start_date, '+10 days') AS 실제반납일,
    CAST(JULIANDAY(DATE(r.rental_start_date, '+10 days')) - JULIANDAY(r.rental_end_date) AS INTEGER) AS 연체일수,
    (CAST(JULIANDAY(DATE(r.rental_start_date, '+10 days')) - JULIANDAY(r.rental_end_date) AS INTEGER) * p.penalty_per_day) AS 총연체료
FROM book_rentals r
JOIN customers c ON r.customer_id = c.customer_id
JOIN book b ON r.book_no = b.book_no
JOIN book_policies p ON b.book_type = p.book_type
WHERE c.name = '홍길동' AND b.book_name = '원피스' AND r.status = 'rented';

 

<책 대여 프로그램 - 10일 후 연체료 계산>



6-1) 마지막으로 대여한 사람이 10일 후에 반납했다고 보자. 밑의 조건은 관점에 따라 약간 과할 수도 있는데, 특정 사용자 id 와 책을 상태를 굳이 맞추는 것 보다, 스캐너로 스캔한 책 번호와 status 만 있어도 될 것 같 긴 한데, 그래도 확실히 하려면 빌려간 id 도 보는 게 날테니 일단 그대로 두자.

UPDATE book_rentals 
SET return_date = DATE(rental_start_date, '+10 days'), 
    status = 'returned'
WHERE customer_id = (SELECT customer_id FROM customers WHERE name = '홍길동' AND phone LIKE '%1111')
  AND book_no = (SELECT book_no FROM book WHERE book_name = '원피스')
  AND status = 'rented';



6-2) 책도 정상 상태로 돌려주자.

UPDATE book 
SET status = 'available' 
WHERE book_name = '원피스';



마찬가지로 앞에서 수행했던 쿼리들을 이용해서 book_rentals 와 book 테이블이 업데이트 된 부분을 보자.

<책 대여 프로그램 - 반납 시 대여 정보 업데이트>

 

<책 대여 프로그램 - 반납 시 책 정보 업데이트>








다음엔 여기까지 온 김에 프로그램에서 쿼리를 호출하는 모습도 한 번 봐보자.

밑의 코드는 AI 쪽에서 추천해 준 대로 streamlit 이라는 파이썬 라이브러리를 가지고 윈도우에 만들어 놓은 SQLite 파일을 기준으로 만들어준 코드이다. 

import streamlit as st
import sqlite3
import pandas as pd
from datetime import datetime

# 1. 데이터베이스 연결 함수
def get_data():
    conn = sqlite3.connect('c:\Sql\DB.Browser.for.SQLite-v3.13.1-win64\mytest.db')

    # 오늘 날짜를 기준으로 대여된 현황 조회 쿼리
    query = """
    SELECT 
        r.rental_no AS '번호',
        c.name AS '회원명',
        b.book_name AS '도서명',
        b.book_type AS '타입',
        p.price AS '대여료',
        r.rental_end_date AS '반납예정일',
        r.status AS '상태'
    FROM book_rentals r
    JOIN customers c ON r.customer_id = c.customer_id
    JOIN book b ON r.book_no = b.book_no
    JOIN book_policies p ON b.book_type = p.book_type
    WHERE r.rental_start_date = DATE('now')
    """
    df = pd.read_sql(query, conn)
    conn.close()
    return df

# 2. 스트림릿 UI 구성
st.set_page_config(page_title="오늘의 대여 현황", layout="wide")

st.title("📚 오늘 도서 대여 관리 현황")
st.write(f"조회 일시: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")

# 데이터 불러오기
df = get_data()

if not df.empty:
    # 요약 정보 (메트릭)
    col1, col2 = st.columns(2)
    col1.metric("오늘 총 대여 건수", f"{len(df)}건")
    col2.metric("오늘 총 매출", f"{df['대여료'].sum():,}원")

    st.divider()

    # 데이터 표 출력
    st.subheader("📋 상세 대여 리스트")
    st.dataframe(df, use_container_width=True, hide_index=True)
else:
    st.info("오늘 대여된 내역이 없습니다.")

# 새로고침 버튼
if st.button('데이터 새로고침'):
    st.rerun()




여기 서는 과정은 설명 하진 않고, 간단한 실행 모습과 결과만 보겠다. 해당 내용을 파이썬 파일(rental_book.py)로 저장 후, CMD 창에서 실행한 결과 이다. 파이썬을 조금 안다면 파이썬을 설치하고, 라이브러리를 설치하고, 파일을 실행하면 바로 된다. 일단 한 가지 주의할 점은 앞에서 얘기했지만 SQLite 는 로컬 디비 이기 때문에 지금까지 한 작업을 저장을 해야 이후 외부에서 해당 데이터에 실제 접근이 가능하다. DB Browser for SQLite 에서 "파일 > 변경사항 저장하기"를 꼭 실행 해주자. 

c:\Python\Code\linkedin>streamlit run rental_book.py

실행 하면 아래와 같이 깔끔한 페이지가 나오게 되는 데, 이건 파이썬 프로그램이 코드 중간에 있는 Sqlite 파일의 위치(결국 데이터베이스 위치)와 query 변수 안에 넣은 쿼리를 가지고 Sqlite 에 조회를 해서 결과를 얻은 후 해당 결과를 라이브리리에 잘 전달을 해서 결과를 보여준다(여기에 사실 테이블 형태의 데이터 정리를 도와주는 pandas 라는 라이브러리도 있는데 뭐 지금은 코드를 배우는 시간은 아니니 그려러니 하자). 

<파이썬 프로그램으로 SQLite 호출 하기>

 


실제 저 코드 안의 query 라는 변수 안에 있는 SQL 쿼리 문장(이제는 우리는 잘 구분할 수 있다)을 긁어다 DB Browser에서 조회하면 아래와 같이 동일한 데이터를 볼 수가 있다. 다만 프로그램에서는 저 모양 그대로가 아닌 여러 프로그램 로직으로 가공한 그래프를 포함한 다양한 모습으로 데이터들의 표현이 가능할 것이다.

<쿼리만 잘라 실행해 보기>

 

 

 

 



마지막으로 CMD 에서 실행하는 예제만 보고 마무리를 해보자. 이것도 실습을 할 건 아니라서 해보고 싶은 사람을 위해서 간단히 설명하자면, https://www.sqlite.org/download.html 사이트에서 sqlite-tools-win-x64-3510200.zip를 다운로드해 압축을 풀고 압축을 푼 디렉토리로 가서 CMD 창에서 아래와 같이 실행하고, "sqlite>" 로 입력 프롬프트가 바뀌면 이후 밑의 쿼리를 넣어본다.

c:\Sql\sqlite-tools-win-x64-3510200>sqlite3 c:\Sql\DB.Browser.for.SQLite-v3.13.1-win64\mytest.db

 SELECT 
        r.rental_no AS '번호',
        c.name AS '회원명',
        b.book_name AS '도서명',
        b.book_type AS '타입',
        p.price AS '대여료',
        r.rental_end_date AS '반납예정일',
        r.status AS '상태'
    FROM book_rentals r
    JOIN customers c ON r.customer_id = c.customer_id
    JOIN book b ON r.book_no = b.book_no
    JOIN book_policies p ON b.book_type = p.book_type
    WHERE r.rental_start_date = DATE('now');


 
이건 실행된 그림으로 대신 한다.

<CLI 를 통한 실행>






왠지 글이 길어질 거 같아서 손을 못 대고 있었는데 역시 꽤 길어지긴 했고(이 정도 글이면 링크드인 편집기가 좀 많이 버벅댄다;), 다행히 억지로 마무리는 된 듯하다. 생각보다 조회 쿼리가 많지 않아서 좀 따라가기가 힘들 것 같긴 한데 그대로 전체적인 사이클을 한번 보여주는 게 좋을 것 같아서 CRUD 를 골고루 섞어 봤다(지금 보니 D 는 중간에서 테스트 하다 개인적으로 지우기만 했긴 했다). 사람들이 데이터베이스에 이렇게 쿼리를 날리기는 하지만 실제에서는 그보다 몇 백만 배로 프로그램들(여러가지 API 나 어플리케이션)에서의 호출이 일어나는 게 더 많다고 보면 된다. 어찌 보면 뒤에서 여러 종류의 데이터베이스를 또 살펴 보겠지만 IT의 심장 같은 느낌을 가지게 된다. 

RDB에서 보통 프로그램에서는 성능 상의 이유로 저 WHERE 뒤의 변수 들을 고정 시켜 놓고, 해당 값들을 프로그램 내에서 가공해서 넣게 된다. 예를 들면 로그인 할 때 아이디, 해싱된 패스워드를 기준으로 유효한 cusommer_id를 메모리에 잘 저장한 후, 해당 값을 인자로 하는 여러 종류의 Query 를 호출하면서 데이터베이스로부터 가져와 프로그램에서 이용하게 된다. 조금 억지로 확장하자면 데이터베이스를 사용하지 않는 프로그램들도 그 나름대로의 데이터를 저장하는 구조를 가지고 해당 데이터를 처리하고 교환하면서 움직이게 된다. 그래서 데이터베이스를 이해하는 것이 어찌 보면 프로그램을 쉽게 이해하는 길이고, 또 프로그램 세상을 이해하는 것이 요즘 한참 얘기 되는 AI 가 만드는 자동화된 agent 의 세상이나 자동화된 프로그램을 쉽게 이해하는 한 발판이 되어주지 않을까 생각한다. 개인적으로는 데이터베이스가 데이터의 심장을 목표로 한다면 AI 는 로직의 심장을 목표로 하는 거 같다고 보기 때문이다. 아직 손발을 이루는 기술은 그대로가 아닌가도 싶다.

여하튼 여기에서 쿼리 자체를 설명하는 부분을 마치고, 다음 번에는 다른 여러 서버 형식의 RDB 가 현재의 SQlite 와 어떤 다른 포지션을 가지고 있는지 보려고 한다. 그 후에는 NoSQL을 함 보려하고, 그 담에 아직은 조금 애매한 기타 디비들, 그 담엔 보안에서의 데이터베이스를 얘기 잠깐하고 마무리를 향해 달려갈까 생각은 하고 있다.

<Fin>

posted by 자유로운설탕
2026. 3. 2. 18:14 프로그래밍

이번 얘기를 하기 전에 앞에서 얘기한 내용들을 다시 한번 정리해 보자. 

우선 첫 글에서 데이터베이스와 테이블에 대한 얘기를 했었다. 데이터베이스(Database)는 바깥 세상의 데이터 들을 파일 형태로 잘 저장하여 이용하기 위해서 만든 것이고, 테이블(Table)은 엑셀 같이 네모난 격자 형태로 데이터를 추상화 해 저장한 것이다(그렇다고 실제 데이터가 네모나진 않을 것이다). 데이터의 의미를 나타내는 세로 줄인 컬럼(Column)이라는 개념이 있고, 가로 줄은 로(Row)라고 해서 데이터의 행 수 또는 개 수를 나타낸다. 테이블의 제일 큰 장점은 사람에게 친숙한 네모난 칸 들로 보이게 만들어져 꽤 직관적이고 가시적이라는 것이다. 요즘의 데이터 세상에서 볼 수 있는 눈에 보이지도 않고, 수학적으로 상상해야 되는 다 차원 데이터 구조 들에 비해서는 특히 말이다.

두 번째 글에서 방 안의 특정 서랍 안에서 야구 공을 찾는 그림을 통해서, 데이터베이스에서 하나의 테이블 안의 데이터 행의 컬럼 하나를 찾아오는 예제를 보여주고, 영어의 문법 순서를 기반으로 Select, Insert, Delete 그리고 테이블을 만드는 Create 문까지 체크해봤다(지금 뒤돌아 보니 데이터를 변경하는 Update 문을 빼놓긴 했지만 Update 문도 "UPDATE 특정 컬럼을 어떻게 WHERE 어디에서"의 형태로 delete 와 거의 비슷하다고 보면 된다). 그 과정에서 테이블을 만들 때 쓰는 DEFAULT, NOT NULL 등의 키워드 들을 잠시 언급하며 그런 키워드들이 테이블을 사용하는 다양한 외부 환경(프로그램)에 대해서 테이블 안의 데이터들의 정합성을 잘 지켜주는 편리한 도구라는 얘기를 했었다.

세 번째 글에서는 잠시 하나의 테이블을 벗어나서 (두 개의) 테이블 간의 관계를 나타내는 썸과 비슷한 JOIN 이라는 키워드와 약간의 변형인 짝사랑 같은 LEFT JOIN 에 대해서 얘기하면서 텅 비어있는 NULL 이라는 키워드에 대해서도 지나가며 언급 했다. 그 담엔 UNION을 통해서 서로 같거나 이질적인 테이블들의 컬럼이 서로 힘을 합쳐 하나의 테이블 같이 동작하는 부분을 얘기하고, 이 부분에서 SQL 쿼리의 결과 또한 하나의 가상의 테이블 형태여서 그걸 자유롭게 사용할 수 있다는 것도 슬쩍 얘기한 것 같다. 그리고 하나의 테이블 안의 사건이긴 하지만 데이터 들 간의 관계를 그룹 짓는 GROUP BY 를 얘기하며, 단체 안에서의 개인의 사라짐과, 그 대신 생겨나는 수치적 통계들에 대해서 얘기 한 듯 하다.



 

테이블들이 혼자서 또는 여럿이서 움직이는 부분을 보여줬고, 그 결과 또한 가상이나 실제의 테이블로 귀결 된다는 것을 보여줬으므로, 이제 가져온 데이터를 어떻게 표현하고, 또 데이터를 가져오는 과정을 어떻게 좀 더 정교하게 하는 지에 대한 문법적인 얘기만 남은 것 같다. 

앞에서 설명한 부분과 연결한다면 "SELECT *** FROM *** WHERE ***" 에서 SELECT 와 FROM 사이에 어떤 일이 생기고, WHERE 뒤에 어떤 일이 생길 수 있느냐 에 대한 얘기이다. 사실 이 부분은 우리가 업무에서 많이 사용하는 엑셀을 상상해 보면 쉽게 알 수 있는 부분이기도 하다. 우리가 엑셀에 특히 구조화된 표 모양의 데이터를 넣은 뒤 주로 하는 일이 바로 그 데이터 들을 조합해 새로운 데이터를 만들어 내거나, 중복 데이터를 삭제 하거나, 합하거나, 조건 문을 걸어 조건에 따른 분류를 하거나 같은 일이다. 또한 데이터를 정렬하거나, 특정 조건이나 날짜의 데이터만 걸러내거나 특정 샘플 만을 가져오거나 하려 한다. 

이와 비슷한 일을 우리는 SQL 문장을 통해서 할 수 있다고 보면 된다. 다만 엑셀에서 표 형태의 데이터를 조작할 때의 제약들처럼 같은 형태의 데이터를 가지고 정리한다고 볼 수 있기 때문에, 아마 처음 사용하다 보면 보기에는 잘 돌아갈 거 같은 쿼리들이 에러가 나기도 할 것이다. 해당 부분은 요즘 같이 AI 로 쿼리를 만들어 사용하다 보면 결코 만나지 못할 부분일 수도 있겠지만, 앞에 얘기한 관계들을 머리 속에 개념화 시키는데는 좋은 경험들이므로 한번쯤은 만나 고민을 해봤음 하는 바램도 있긴하다. AI는 일종의 자동화의 산물이라고 생각하는데 모든 자동화는 수동화의 실패 경험이 기본으로 배경으로 있을 때 가장 세련되게 구현될 수 있다고 보기 때문이다. 


 



[SELCET 와 FROM 사이의 요소들]

앞에서 일반적으로 모든 SELECT 쿼리는 결국은 테이블 모양의 데이터가 반환 된다고 했다. 해당 부분을 상상해 보면 반환 되는 것은 하나의 셀(Cell)일 수도 있고, 하나의 줄(Row)일 수도 있고, 하나의 열(Column)일 수도 있고, 네모난 상자일 수도 있다(결국은 네모난 상자에 모두 속하긴 한다). 보통 여기서 사용하는 연산자 들은 열을 기준으로 동작을 하게 된다. 

우선 결과로 보여주는 열에 대해서 SUM(합), COUNT(행 수), AVG(평균) 같은 부분을 결과로 낼 수 있다. 일반적으로는 이 함수를 사용하려면, 앞에서 설명한 GROUP BY 등으로 데이터를 묶어 사용하거나 테이블 전체에 대해서만 기본적으로 사용이 가능하다고 볼 수 있다. 그래서 만약 테이블에 직원 이름, 연봉이 있고, 결과로 직원 이름, 연봉, 회사 전체 연봉을 표시하고 싶었다면 아래와 같이 조금 비 효율 적인 서브 쿼리가 필요했었다. 

여기서부터 드물게 아래 사이트에서 쿼리 10개까지는 SQLITE 기준으로 데이터 저장부터 실습을 할 수 있음을 발견하여 사이트도 구경할 겸 현재 쿼리만 한번 실습을 해보자. 무료 사용의 경우 3시간 후나 브라우저를 종료했다 다시 열면 데이터가 모두 사라지는 듯 하니 참고하자. 기본적으로 서브스크립션 대여를 목적으로 한 사이트 같아 너무 쿼리를 자주 날려도 실례일 듯 하고, 또한 아마 해당 제한으로는 좀 더 연속적인 쿼리 과정에 대한 실습은 못할 듯 하기도 하고, 로컬 디비이긴 하지만 하나 설치해 보는 경험도 나쁘진 않을 듯 해서, 다음 시간에는 SQLITE 를 PC에 함 설치해 사용해 보도록 해보자.

https://sqliteonline.com/

<SQLITEONLINE 사이트>

 


화면에서 위쪽에 쿼리를 넣고 "Run" 버튼을 실행하면, 왼 쪽에 생성한 테이블 이름이 나오고, 아래 쪽에서는 조회의 경우에는 결과가 나오게 된다(처음엔 아래 부분을 한번 클릭해 가이드를 지워야 하는 듯도 싶다).

우선 테이블 생성 쿼리를 돌려서 테이블이 생성 됬음을 확인 하고,

CREATE TABLE employees (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    salary INTEGER NOT NULL
);




이후 데이터를 넣어보자, 앞의 글에서 살짝 언급 했듯이 id 라는 값은 자동 증가 하는 값이 들어가기 때문에 입력 시 생략을 해도 에러가 나지 않으면서도 일련번호가 생겨 관리하기 좋아진다.

INSERT INTO employees (name, salary) VALUES 
('박지민', 3500),
('이영희', 5000),
('홍길동', 4000);





아래가 바로 전통적인 해당 테이블을 이용해, "직원 이름, 연봉, 직원 전체 연봉" 을 보여주는 쿼리이다. 쿼리를 보면 컬럼 안에 서브 쿼리 형태의 쿼리가 하나 통째로 들어가서 전체 연봉을 sum 을 해서 컬럼 자리를 차지한다(앞에서 얘기했지만 모든 결과가 결국 테이블 형태기 때문에 이런 유연한 끼워 넣음이 가능한 듯 싶다)

SELECT 
    e1.name, 
    e1.salary,
    (SELECT SUM(e2.salary) 
     FROM employees e2) AS dept_total
FROM employees e1;

 

<SQL 쿼리 실행 결과>



그런데 위의 방식은 데이터베이스 최적화 엔진에 따라서 모든 행에서 반복으로 SUM 에 관련된 서브 쿼리가 실행될 수 있었기 때문에, 그래서 AI 가 하는 얘기로는 예전에는 아래와 같이 한번만 조회가 일어나게 되는 좀 복잡해 보이는 방식을 추천했다고 하는데, 현재는 최신의 컴파일러들이 알아서 비 효율 적인 코드를 실행 시 최적화 하듯이 위와 같이 작성해도 아래와 같이 실행해 줄 수도 있을 듯 싶다. 이런 게 보통 데이터베이스에서 얘기하는 쿼리 튜닝 요소이고 말이다.

SELECT 
    e1.name, 
    e1.salary, 
    total.dept_total
FROM employees e1
CROSS JOIN (
    SELECT SUM(salary) AS dept_total FROM employees
) total;




앞의 부분을 전통적인 이라고 얘기했던 이유는 이 방법을 안 쓰는 뒤의 새로운 문법들은 SQLITE 같은 경우는 2014~2018년에, 상용인 Oracle 이나 MSSQL 은 2000년 초에 생겼다고 한다. 또한 신 기능들은 아마 초기의 성능 개선이나 여러 요소 때문에 DBA 팀에서 사내 쿼리 표준에 보수적으로 포함 안 하는 경우가 많았을 터이기 때문에 아마도 2010년 중 후반에나 퍼졌을 것 같고, 그 후에 오픈 소스 디비들이 그 편리함들을 차용해 도입했을 것 같다. AI 에게 의견을 물어보면 하드웨어의 발전과 용량 확대 또한 많은 기여를 했다는 데, 하나의 서버가 모든 걸 커버해야 하는 RDB 의 경우는 특히 더 많은 영향을 받지 않았을까 싶다. 이건 뭐 주위의 좀 연식이 된 DBA분들이 있으면 함 여담으로 물어봐도 될 듯 하다.

그 다음에 나온 두 가지 방법이 OVER 와 공식적으로 CTE(Common Table Expression, 공통 테이블 식)이라고 얘기하는 WITH 인데, 먼저 OVER 를 보면 앞에서 설명한 GROUP BY 역할을 하게 되며 이런 부분에 특화가 되었다. 저 OVER 는 다른 요즘 함수인 RANK 나 LAG 등과 결합하면 그룹핑이나 정렬 함수가 들어가게 되는 거 같다.

SELECT 
    name, 
    salary,
    SUM(salary) OVER () AS dept_total
FROM employees;




마지막으로 마치 프로그램에서 변수를 저장하듯 테이블을 잠시 저장해 쓰는 WITH 라는 문구를 쓸 수 있다. 파이썬이라면 pandas 테이블이 하나 있는 떠 있는 듯 싶다고 표현하고 싶다. 

WITH GrandSum AS (
    SELECT SUM(salary) as total FROM employees
)
SELECT 
    e.name, 
    e.salary, 
    g.total as grand_total
FROM employees e 
CROSS JOIN GrandSum g;




아마 레거시 SQL 만 주로 보다 보면 위의 쿼리들이 조금 신기해 보일 수도 있을 것 같긴한데, 데이터 추출 하는 쪽 같은 데서는 자주 볼 수 있는 것 같기도 하고, 기존 문법의 비효율성과 성능을 개선하기 위해 만든 기능 들이고 성능 적으로도 충분히 안정화가 된 것 같으니 DBA쪽과 의논해 유용하게 써도 무방하지 않을까 싶다. 다만 해당 사용이 결국 뒤에 나오는 테이블의 인덱스 같은 부분들과도 연관 되니 그냥 내가 쓰고 싶다고 언제든 쓸 수 있는 부분은 아닌 듯은 싶다.

일단 하나의 자세한 예제를 보았으니 나머지는 후루룩 넘어가 보려고 한다. 위의 SUM 과 비슷하게 행의 수를 세는 COUNT와 평균을 계산하는 AVG 가 있으나 이것은 결국 데이터 군을 어떻게 해석하려 하느냐의 차이이니 굳이 추가적인 예제는 필요 없을 듯 싶다.





다음으로는 CASE 문이 있는데 이건 프로그래밍 에서도 많이 보이는 형태이기도 하고, 사실 나중에 프로시저라는 반복이나 여러가지 조건 처리를 위해 여러가지 쿼리들이 조합된 방식으로 만들려고 하다 보면 결국 스크립트 형태의 프로그램이 되어버리긴 한다(다만 우리가 사용하는 SQLITE 는 경량 화에 특화되고 그런 복잡한 로직은 프로그램을 통해 해결하라는 철학을 가지고 있기 때문에 프로시저 문법은 지원 안 한다고 한다). 

문법을 보면 CASE WHEN ELSE END 가 주인공 이며 결과는 결국 하나의 값이 된다. AI 한테 영어로 함 문장을 만들어 달라고 해봤다.

CASE 
    WHEN life gives you lemons 
    THEN make lemonade
    ELSE just smile and stay positive
END




예를 들어 앞의 예에서 전체 직원 월급의 평균 보다 같거나 높으면 "이상", 낮으면 이하라고 표기하고 싶다면, 앞의 현대적인 문법인 OVER 를 배웠으니 그걸 이용해서 만들면 아래와 같다. 

SELECT 
    name, 
    salary,
    CASE 
        WHEN salary >= AVG(salary) OVER() THEN '이상'
        ELSE '이하'
    END AS salary_status
FROM employees;





또한 앞에서 얘기한 짝사랑의 관점에서 의미가 없이 비어있는 NULL 값을 치환해 주는 함수들도 있는데, IFNULL 이나 현재에 많이 쓰인다는 COALESCE 라는 키워드가 있다고 한다. 저런 예전 키워드 들은 약간 상황에 따라서 모양이 다른 경우가 있다. 예를 들면 IFNULL 이 MSSQL 에서 ISNULL 이고, 오라클에서는 NVL 이라고 한다. COALESCE 는 공통으로 사용되며 인자가 여러개 들어가 A 가 아니면 B , 아니면 C 로,... 해서 대체제를 다수 찾아줄 수 있어 더 유연성이 좋다고 한다.

직원들의 평균 연봉을 구하는데 연봉 금액을 비워 놓은 직원이 있어서(Null 존재) 해당 경우는 어림 잡아 4000으로 잡아서 계산하는 쿼리를 만들어 보자. 만약 저런 예외 처리가 없다면 Null 은 데이터베이스의 종류에 따라 맘대로 0으로 처리되거나 집계에서 빠져버릴 수 있다고 한다. 뭐랄까 이런 함수의 용도는 얘를 무시하지 말고 제도 권에 받아들이자의 의미라고 봐도 된다.

SELECT AVG(COALESCE(salary, 4000)) AS adjusted_average
FROM employees;






다른 테이블에 거리 주소와 상세 주소가 나눠져 있다고 보자. 2개를 가져와서 프로그램에서 합쳐도 될 수 있겠지만, 결과 자체에서 그렇게 보고 싶다면 SQL 종류에 따라 특정한 연산자(||, +)나 CONCAT_WS 같은 문자열을 합치는 키워드 들이 있다. AI 가 만들어준 예제에서는 친절하게도 혹시나 상세 주소가 비어서 에러 날 경우를 대비해서 앞에서 배운 COALESCE 함수로 NULL 일 경우를 대비해 주어서 그대로 가져왔다.

SELECT 
    name,
    address_road || ' ' || COALESCE(address_detail, '') AS full_address
FROM employees;





다음도 약간 프로그래밍 적인 요소인데(뭐 근데 엑셀도 마찬가지지만 함수를 사용하기 시작하면 스크립트 기반의 프로그래밍 영역에 들어갔다고 보면 될 것 같다) 데이터의 형을 바꿔주거나 날짜 포맷 등을 바꿔줄 수 있다. 예를 들어 직원의 "년월일"로 들어간 입사일 데이터를 월 별로 GROUP BY 해 "년월"별 입사한 직원 수 통계치를 뽑기 위해서, 입사일 데이터를 일단 "년월"별로 변환 후 GROUP BY 하여 수를 세는 예제를 만들면 아래와 같다. 실제 입사일 데이터는 일 또는 시간까지 들어간 데이터일 수도 있지만 잘 변환해 원하는 결과를 가져온다.

SELECT 
    strftime('%Y-%m', hire_date) AS join_month,
    COUNT(*) AS employee_count
FROM employees
GROUP BY join_month







[WHERE 뒤의 요소들]

생각보다 앞의 얘기가 길어진 듯 하지만 다음엔 WHERE 뒤이다.

사실 위의 WITH 나 OVER 가 나온 후에 이 WHERE 뒤 쪽이 SELECT 와 FROM 사이에서도 자주 보이게 됨으로서 조금 서로 간의 경계가 깨어진 듯도 싶기는 하지만(그래서 처음 배우는 사람은 더 헷갈릴 듯 싶다), 이 뒤에 나오는 키워드 들은 일종의 데이터 들을 제한하는 키워드 들이다. 요즘의 여러 다양한 빅데이터 솔루션들의 개념으로 생각하자면 필터링 역할의 키워드라고 보면 될것 같다. 실제로 사용하는 키워드들도 서로 비슷하고 말이다.

여기서도 전통적으로 보던 키워드와 현대의 프로그램 구조를 반영한 키워드들이 서로 어울려 있는 느낌이 있다. IN 이나 NOT IN인 같은 경우는 여러가지 중에 하나를 선택하는 구문이다. 앞의 예제에서 연봉이 3500이나 4000인 직원을 찾을 때 아래와 같은 정직한 쿼리를 쓸 수도 있겠지만 

SELECT name, salary FROM employees 
WHERE salary = 3500 OR salary = 4000;





실제로는 아래의 IN 쿼리가 더욱 간단하고 가독성도 좋다. 

SELECT name, salary FROM employees 
WHERE salary IN (3500, 4000);




저 IN 뒤에는 특정 값 뿐만 아니라 응용에 따라 다른 테이블의 제한된 조회 결과를 가져오는 서브 쿼리도 가능하다. 아래와 같이 2개의 테이블을 이용해 베스트셀러 테이블에 있는 과자들에 대한 정보만 가져오는 쿼리를 만들 수도 있다. 

SELECT snack_name, price, calories
FROM snacks
WHERE snack_name IN (
    SELECT snack_name 
    FROM best_sellers
);





BETWEEN ... AND 도 숫자나 날짜를 찾는 용도로 사용된다. 아래의 간단한 예제를 보자.

SELECT * FROM employees 
WHERE salary BETWEEN 3500 AND 4000;





원하는 패턴을 찾는 정규표현식의 꼬마 버전이라고 할 수 있는 LIKE 검색도 있다. "김%"는 김으로 시작하는, "%김"은 김으로 끝나는, "%김%"는 가운데 김이 들어감의 의미이다.

SELECT * FROM employees 
WHERE name LIKE '김%';





요즘은 나중에 자동화 얘기할때 설명하게 될 것 같은 정규표현식도 지원하는 디비들이 PostgreSQL, MySQL, Oracle 등도 있긴 하지만, 보통 정규표현식은 어떤 거를 찾느냐에 따라 시스템 리소스를 꽤 많이 차지 하는 작업으로 인지하고 있기 때문에 실제 현실에서는 얼마나 사용될지는 개인적으로는 잘 모르겠다. 

또한 요즘 트랜드에 맞게 JSON 을 데이터에 넣고 해당 데이터를 파싱하는 부분도 지원하고 있긴 하지만 이것도 케이스에 잘 맞게 사용하는 경우가 얼마나 많을지는 모르겠다. 요즘은 컬럼 내 json 데이터의 요소를 가상 컬럼처럼 정리해 인덱스를 걸어 속도를 높인다든지 하는 부분이 자연스럽게 추가되는 것 같긴 하다(윈도우 안의 WSL 처럼 마치 RDB 내에 작은 NoSQL 을 키우는 느낌이다). AI 쪽에서는 하이브리드 방식으로 소규모의 json 트래픽을 정합성있게 저장하는 경우 유리하다고는 말하고 있다.

CREATE TABLE snack_orders (
    id INTEGER PRIMARY KEY,
    order_info TEXT
);

INSERT INTO snack_orders (order_info) VALUES (
    '{"customer": "김철수", "items": ["홈런볼", "포카칩"], "total": 4500, "details": {"payment": "card"}}'
);

SELECT 
    order_info ->> '$.customer' AS customer_name,
    order_info ->> '$.total' AS price
FROM snack_orders;





위의 케이스들에서 공통 되게 고려해야 할 부분이 한 가지 있는데, 그게 SQL 을 보다 보면 가장 자주 듣게 되는 인덱스 이다. 인덱스는 우리가 책장에 책을 한글 이나 영어 알파벳 순으로 쉽게 찾을 수 있도록 정리해 놓는 것과 비슷하다. 근데 보통 실제로 데이터 자체를 직접 정렬하는 건 아니고(보통 일련 번호 형태인 primary키 같은 경우는 이런다고 하는 듯 하다), 다른 저장소에 데이터의 식별 가능한 전체나 일부를 순서대로 저장한 것과 비슷하다고 볼 수 있다. 그래서 우리가 특정 데이터(문구)가 들어간 컬럼을 찾으려 할 때, 순서대로 잘 정리된 인덱스 테이블을 뒤져 거기서 실제 테이블 내의 데이터의 위치를 찾아서, 전체 컬럼 데이터를 순서대로 찾지 않아도 빠르게 데이터의 위치를 찾게 된다(인덱스가 없다면 원본 데이터는 기본적으로 랜덤으로 들어오는 대로 저장되었기 때문에 처음부터 하나하나 찾아야 하지 않을까 싶다. 이렇게 하나하나 찾는 걸 보통 full scan 이라고 한다).

 

<인덱스>


특정한 한 개의 컬럼이나 몇 개의 컬럼을 묶어서 인덱스를 생성할 수 있는데, 세상에 공짜는 없어서 보통 테이블 내의 저장된 컬럼 내의 데이터 크기에 비례한 어느 정도의 공간을 인덱스 하나 당 차지하게 된다(원본 데이터를 구별할 일부 데이터를 색인 해야 하다 보니). 그래서 성능을 위해서 인덱스를 쿼리에서 요구하는 대로 한없이 늘리다 보면 배보다 배꼽이 더 커지는 일이 생길 수 있기 때문에 보통 어느 정도의 필수 인덱스들이 생성되게 되면, 이후에는 정말 어쩔 수 없는 경우가 아니면 DBA쪽에서 추가 인덱스는 잘 만들어 주려 하지 않는 듯 싶다. 특히 저장된 데이터 양이 몇 억 몇 십억으로 엄청 큰 테이블이라면, 추가 인덱스를 만드는 건 용량과 시간을 많이 잡아먹는 일이 될 테고 말이다. 

인덱스는 작은 규모의 데이터를 가진 환경에서는 그다지 그 효용성을 인지하지 못하게 될 수도 있기 때문에, 디비를 전문적으로 관리하는 사람이 없는 회사에서는 프로그램이 인덱스를 잘 지키지 않고 호출할 수도 있다(어떤 경우는 인덱스 자체가 거의 없었다는 상황도 들어봤다). 해당 부분이 규모가 작을 때는 별 일이 없이 살다가 데이터의 규모가 커지면서 인덱스를 사용하지 않는 쿼리들의 결과가 느려지고 추가로 데이터베이스의 자원을 대부분 차지하게 됨으로서 전체 사이트의 속도를 느리게 만들 수 있는 바오밥 나무 같은 일이 생길 수도 있다. 또 그 정도로 관리하는 데이터베이스였다면 아마 앞에서의 여러 테이블들의 상관 관계도 그렇게 확장성에 유리하게 설계도 안 되었을 가능성도 높을 것이고 말이다. 앞에서 쿼리와 테이블을 이해하려면 디비를 호출하는 프로그램도 어느 정도 이해하는 게 좋다고 했는데, 반대로 바깥 세상의 프로그램을 잘 이해하려면 호출할 대상인 디비 내부의 테이블과 쿼리를 어느 정도 이해하는 게 반대로도 필요하다고 생각한다.





다음으로 WHERE 과 직접접인 관계는 없지만 앞에서 나온 OVER, WITH 같은 쪽이나 결과의 정렬 부분에 대한 키워드가 있다. 이건 엑셀의 데이터 정렬 기능을 생각하면 완전히 동일하다. 

밑의 예제는 임직원 데이터에 대해서 연봉은 높은 순서(DESC)로, 만약 연봉이 같다면 차 순위인 이름이 빠른 순서(ASC)로 정렬해 보여준다. 

SELECT name, salary
FROM employees
ORDER BY salary DESC, name ASC;





마지막으로 언급할 부분은 결과를 제한 하거나 건너 뛰는 부분이다. 실제 쿼리를 날리다 보면 데이터가 너무 많이 나와서, 필요한 데이터만 몇 개 보고 싶을 수 있다. 역사적인 이유 때문에 데이터베이스 별로 LIMIT(SQLITE, MySQL)나 TOP(MSSQL), ROWNUM, FETCH FIRST(ORACLE) 등으로 약간 씩 문법이 달라 처음에 다른 데이터베이스를 쓸 때 괜한 혼란을 주곤 한다. 거의 모든 데이터베이스에 있을 OFFSET 문법은 정렬된 순서의 데이터에서 특정 숫자를 건너 띄고 조회하는 게시판의 페이징 쿼리 등에서 사용된다(이 키워드가 없다면 프로그램이 전체 데이터를 가져와 화면에서 나눠서 보여줘야 할 텐데 생각만 해도 아찔하다). 아래는 기존 테이블에서 연봉이 높은 사람 순서로 2명(LIMIT)을 가져오는데, 가장 연봉이 높은 1명은 제외(OFFSET)하고 가져온다.

SELECT name, salary
FROM employees
ORDER BY salary DESC
LIMIT 2
OFFSET 1;





이렇게 되면 SQL 의 기본적인 문법이 왜 그렇게 구성되었는지는 아는 부분은 대략 설명했다고 생각이 든다. 설명한 방식이 좀 공감이 갔음 하는 바램이 있고, 서당개 식으로 배운 거기 때문에 혹시 틀린 내용이 있더라도 양해를 해줬음 한다. 다음 시간에는 좀 예전 스타일의 구조 같긴 하지만 AI와 의논하며 앞에서 얘기했던 책 대여점 프로그램에 필요한 테이블들을 만들고, 데이터를 넣고, 쿼리를 만들면서 이제까지 설명한 부분들을 녹여 복습하고 문법 설명에 대한 마무리를 할까 한다. 또 SQLITE 도 한번 설치해 보고(다른 디비와는 달리 SQL 서버라기 보다 편집기가 설치되는 느낌이긴 하지만) 데이터가 실제 컴퓨터에 어떻게 저장 되느냐도 간단하게 봐보자.

 - Fin -

posted by 자유로운설탕
2026. 3. 2. 18:04 프로그래밍

이번엔 한번 RDB(Relational Database)가 존재하는 의미라고도 할 수 있는 "관계"에 대해서 생각해 보자. 개인적으로 SQL 의 관계에 대해서 잘 설명해 줄 수 있는 키워드 들은 JOIN, UNION, GROUP BY 라고 생각해서 세 개의 키워드의 개념이 왜 존재 하는 지에 대해서 설명해 볼까한다.

 

우리가 누군가와 썸을 탄다고 생각해 보자. 그렇게 되면 아마도 그 사람하고 뭔가 "연결(JOIN)"이 되는 것을 바라게 될 것이다. 무언가와 연결을 되려면 공통된 부분이 있어야 할테고, 우린 아마 상대방과 연결 될 수 있는 어떠한 연결 고리를 찾기 위해서 안테나를 세우게 될 것이다. 그래서 무언가가 혹시 닿게(ON) 된다면 썸이 연애로 발전하게 될 수도 있게 될 테이고 말이다. 두 개의 존재 사이에서 연결을 가져올 수 있는 무언가 동일한 것을 찾는 것처럼, SQL도 두 개의 테이블 간에서 같은 데이터들을 연결하는 기능을 제공하고, 이것을 JOIN 이라는 키워드를 사용해서 나타낸다. 실제 필드 에서는 여러 개의 테이블이 복합 해서 조인이 되거나, 특별한 관계를 뽑아내기 위해 자기 자신하고 조인을 하는 경우도 있긴 하지만 기본 적인 관계는 2개의 테이블에서 시작된다고 볼 수 있다.

<썸 타기>




사실 SQL 의 조인에는 INNER, LEFT, RIGHT, FULL, CROSS 등이 있다곤 하지만, 개인적으로는 한번에 다 머리 속에 억지로 넣어 놓고 헷갈리는 것보다는 INNER 와 LEFT 를 잘 이해하면 나머지는 실제 쿼리 들을 보거나 공부하면서 천천히 응용 방식을 익히면 된다고 생각한다. LEFT JOIN 을 보게 되면 SQL 에서 한번 쯤 꼭 만나야 할 NULL 의 의미도 생각해 볼 수 있게 되고 말이다. 관계형 이라는 말처럼 개별 문법을 익히는 게 중요한 게 아니라 어떤 유익한 점이 있어 그런 개념을 SQL 이 포함했을까를 고민하는 것이 맞다고 본다.

일단 JOIN 은 앞에서 봤던 엑셀 표 모양을 상상해 보면 2개의 테이블에서 각각 하나의 컬럼을 선택해서 두 개 안에 담긴 데이터 사이에서 동일한 값을 찾게 된다(기술적인 언어이기 때문에 유사어 같은 걸 찾진 않고 완전히 같은 데이터 값인 경우 매칭하게 된다). 매칭이 되면 테이블들의 하나의 줄 자체가 서로 매칭이 되는 거고, 만약에 한 쪽에 여러 개의 같은 데이터가 있다면 아래와 같이 그 수 만큼의 줄이 생기게 된다. 매칭이 안 되는 양 쪽 테이블의 모든 데이터 행 들은 썸 탈 때의 서로 상관없는 관심사처럼 INNER JOIN 에서는 아무 관심이 없다. 저 관계는 두 개의 테이블에 담긴 기준이 되는 같은 데이터가 어떤 것이냐에 따라서 의미를 가지게 된다. 

 

<Inner JOIN>




일단 JOIN의 시작은 앞의 시간에서 얘기했던 SELECT 문을 기본으로 진행 된다고 보면 된다. 앞에서 나왔던 SELECT 문을 생각해 보면 아래와 같은 형태이다. 낯설다면 방안의 서랍에서 물건을 찾아오던 그림을 떠올려 보자.

SELECT 대상 FROM 테이블 WHERE 조건

 


예를 들어 한 쪽 테이블에 회원 정보(회원 아이디, 성별, ...)가 있고, 다른 쪽에 구매 내역(회원 아이디, 구매 물품, ....)이 있다면 두 개를 조합하게 되면 특정 남자 회원들이 조합한 구매내역들을 뽑을 수가 있다. 예를 들면 이런 식의 쿼리가 될 것이다.

SELECT "구매상품들" FROM "회원정보 테이블" 
JOIN "구매내역 테이블" ON "회원정보의 구매아이디와 구매내역의 구매아이디 매칭" 
WHERE "회원정보의 성별이 남자인 경우"



또는 한쪽 테이블에 부서 정보(부서 코드, 부서 이름)가 있고, 다른 쪽에 직원 정보(직원 이름, 직원 아이디, 부서 코드)가 있다면 아래와 같이 사용할 수도 있다.

SELECT "부서 이름 및 사용자 이름" FROM "부서 테이블" 
JOIN "사원 테이블" ON "부서 테이블의 부서 아이디와 사원 테이블의 부서 아이디 매칭" 
WHERE "사원 이름이 홍길동"



결국 만능 키워드는 아니기 때문에 결국 사용하는 몇 가지 패턴이 있기는 하지만, 관계 자체만이 아니라 어떤 데이터가 저장되어 매칭 시키느냐에 따라서 사용하는 의미가 달라질 수 있다. 앞의 구매내역을 체크하는 곳에서도 SELECT 문에 "구매 상품들" 대신에 "구매 아이디" 넣는 다면 남자 회원들이 구매한 상품을 찾는게 아니라, 상품을 구매했던 남자 회원들을 찾는 쿼리로 의미가 바뀔 수도 있다. 결국 쿼리는 테이블 간의 관계 설계와 그 테이블이 현실과 매칭되는 의미에 따라서 비슷한 형태라도 의미가 달라질 수 있기 때문에, 해당 데이터의 해석에 따른 의미를 자연스럽게 가진다고 봐도 된다. 그런 관점이 없음 의미가 없기도 하고 말이다. 그래서 앞에서 얘기했던 SQL 을 이해하려면 데이터를 넣는 어플리케이션 단도 같이 이해해야 한다는 얘기도 그런 부분을 돕는 맥락이라고 이해를 해보자.

위의 회원정보와 구매 내역 쿼리를 설명했던 대목을 AI 한테 부탁해서 쿼리를 만들어 보면 아래와 같은 점잖은 SQL 쿼리가 나오게 된다. 테이블 뒤에 "AS 닉네임"으로 가정 하는건 쿼리의 가독성도 높여주지만, 두 개의 테이블에 같은 컬럼이 있을 경우에 구분할 수도 있고(디비에 따라서 임의로 해석해 주는 경우도 있지만, 어떤 디비는 명확히 구분해 호칭이 모호하다고 에러를 내기도 한다), 아까 얘기했듯이 자신과 자신을 JOIN 할수도 있기 때문에 임의의 네이밍을 붙이는게 여러모로 의미가 있다. 이것 저것 덕지덕지 추가됬긴 하지만, 여기서 위의 말로 표기한 의미가 대충 표현된 것이 이해된다면 성공했다고 볼 수 있다. 다른 분야도 마찬가지지만 이후 나머지는 자꾸 봐서 익숙해 지는 수밖에 없다.

SELECT 
    m.User_ID,
    m.Gender,
    p.Product_Name,
    p.Purchase_Date
FROM Member_Info AS m
JOIN Purchase_History AS p ON m.User_ID = p.User_ID
WHERE m.Gender = 'Male';




다음엔 LEFT JOIN을 함 이해해 보자. 개인적으로 첨에 봤을 때 INNER 조인, LEFT (OUTER) 조인 등의 차이가 많이 헷갈렸었는데, 지금 생각해보면 그게 왜 필요한지 잘 맘에 안 닿아서 그랬을 것도 같다. LEFT JOIN 은 위의 썸 얘기를 다시 빌려온다면 짝사랑에 가깝다. 썸은 서로 관심이 있어서 연결될 가능성이 높지만, 짝사랑은 상대의 마음을 전혀 모른다고 볼 수 있기 때문에 대부분 맹목적일 것이다. 상대의 모든 것을 신경쓰고 그 하나하나에 의미를 두게 될 것이기 때문이다. 

LEFT 조인은 비슷하게 내가 중요하다기 보다는 나의 왼쪽에 있는 상대의 모든 것을 기준으로 나를 판단하는 JOIN 형태라고 보면 된다. 뭐 현실적으로 보면 자신도 소중한 존재인건 맞지만 짝사랑에 빠지게 되면 상대의 모든 점에 대해서 내가 의미가 있는지를 살펴 보게 된다. 그리고 내가 그 상대가 가진 점에 대해서 특별한 연결 점을 찾지 못한다면 해당 부분의 나는 의미 없게 느껴지는 일도 생길 수 있다. 요 의미 없는 부분이 SQL 에서 얘기하는 NULL 이라고 봐도 된다(Null 은 순수 영어 뜻으로는 아무 힘이 없거나 비어있음을 얘기한다). JOIN 관계에서의 NULL은 상대에 비해서 아무 의미가 없다는 뜻으로 보게 되면 무방하지 않을까 싶다. 

 

<Left JOIN>


그래서 LEFT JOIN 의 의미도 위의 JOIN 과 비슷하게 여러가지 맥락을 가질 수 있다. 한 쪽이 회원 테이블이고 다른 한 쪽이 구매 테이블인 상태에서 LEFT JOIN을 한 후 구매 테이블의 회원 ID 가 NULL 인 줄(테이블 2개가 합쳐진 줄이긴 한다)을 조건으로 단다면 물품 구매를 안 한 회원을 찾아낼 수 있다. 일반적인 문장으로 표기한다면 아래와 같을 거다.

SELECT "회원정보 테이블의 구매회원들" FROM "회원정보 테이블" 
LEFT JOIN "구매내역 테이블" ON "회원정보의 구매아이디와 구매내역의 구매아이디 매칭" 
WHERE " 구매내역 테이블의 구매아이디가 NULL 인 경우"



위와 같이 진중한 쿼리로 만들어 본다면 아래와 같을 것이다.

SELECT 
    m.User_ID
FROM Member_Info AS m
LEFT JOIN Purchase_History AS p ON m.User_ID = p.User_ID
WHERE p.User_ID IS NULL;



또한 왼쪽 테이블의 데이터를 전체 표시하면서 해당 데이터에 해당되는 특정 조건이 없는 지를 전체 펼쳐서 체크해 보는 경우에도 사용할 수는 있을 것이다. 왼쪽 테이블의 데이터는 항상 모두 기본으로 가져가고 싶을 때 유용할 수도 있다.



다음에는 UNION을 생각해보자. UNION 은 정석으로 생각한다면 서로 형태가 같은 2개의 테이블을 하나의 테이블처럼 합쳐서 볼 수 있게 하는 거지만, 보안 취약점 공격인 SQL Injection에서 자주 일어나는 패턴까지 고려해 본다면, 테이블의 형태가 꼭 같을 필요는 없는 것 같고, 서로 다른 테이블 사이에서 같은 의미를 가진 데이터(SQL 인젝션 공격할 때는 수단과 방법을 안 가리기 때문에 이런 관계도 무시하긴 한다) 컬럼을 모아서 하나의 테이블을 만든다고 보면 될 것 같다. 아래와 그림과 같이 서로 다른 종류의 환경을 가진 사람들이지만 줄다리기라는 목표를 가지고 힘을 모아 동맹(UNION)을 결성해서 하나의 세력(SQL 에서는 TABLE)처럼 만들어 진다고 보면 어떨까 싶다.

<줄다리기>




테이블 뿐만 아니라 테이블에서 특정 컬럼을 모아서 SELECT 를 하면 하나의 결과 테이블이 된다는 확장 적인 관점에서 보면 UNION의 모습은 아래와 같다. 그림에서 만약 2개의 테이블이 완전히 동일하다면, 두 개의 테이블이 하나로 합쳐진 결과를 받을 수도 있다.

 

<UNION>


그럼 이런 형태의 관계는 어떤 의미가 있을 수 있을까? 예를 들어 테이블 형태가 같은 경우는 과거나 현재 테이블 또는 시간대 별 테이블을 합치는 경우가 있을 수 있다.

SELECT 동일 데이터 FROM "현재 테이블" 
UNION 
SELECT 동일 데이터 FROM "과거 테이블"



WHERE 조건은 각각의 SELECT 문이 있기 때문에 각각에 걸 수 도 있고, 두 개가 합쳐진 테이블에 통합해 걸 수도 있다. 두 개를 합칠 경우는 나중에 SELECT 추가 설명에서 얘기할 서브 쿼리 개념이 생기게 된다. 처음엔 좀 헷갈릴 수 있지만 쿼리의 반환 결과도 결국은 테이블 형태이기 때문에 그 가상의 테이블에 다시 쿼리를 날릴 수 있다고 보면 맘에 편하다. 이 무한 루프 이면서 재귀 호출 같은 부분이 SQL 문법의 강력함 중에 하나인 것 같고 말이다. 누구 표현으로 성능만 고려 안 한다면 어떻게든 원하는 데이터를 보여줄 수 있다고 하기도 한다.

SELECT 특정 조건 FRON
(
SELECT 동일 데이터 FROM "현재 테이블" 
UNION 
SELECT 동일 데이터 FROM "과거 테이블"
) AS "통합 테이블"
WHERE "통합 테이블 기준 조건"



위의 예제를 AI 한테 물어봐 만든 쿼리는 아래와 같다. 해석을 해보면 과거와 현재 거래 테이블에서 사용자와 금액을 모아서 10000만원 이상의 건만 찾는 것이다. 

SELECT 
    Total_List.User_ID,
    Total_List.Amount
FROM (
    SELECT User_ID, Amount AS Total_Amount
    FROM Sales_Present
    UNION ALL
    SELECT User_ID, Amount AS Total_Amount
    FROM Sales_Archive
) AS Total_List
WHERE Total_List.Total_Amount >= 100000



뭐 SQL 엔진이 어떻게 데이터를 찾아오려 할지(실행 계획이라고 한다)는 따져봐야 할 테지만, 현재 조건에서 저 거래 테이블에서 모든 데이터를 일단 다 가져오는 게 성능 적으로 맘에 걸린다면 아래와 같이 미리 각 테이블에서 100000만원 이상만 뽑아서 합치는 쿼리를 만들 수도 있을 것이다. 아마 요즘 SQL 엔진들은 프로그램 컴파일러 비슷하게 왠만하면 알아서 등가적으로 유리한 실행 계획을 세우긴 할 것 같긴 하다. 여튼 뭐 이런 부분은 비즈니스 설계가 데이터베이스 어떻게 녹아져 있고, 어떤 의미의 쿼리를 만들기를 원하냐에 따라서 달라질 것은 같다.

SELECT 
    Total_List.User_ID,
    Total_List.Total_Amount
FROM (
    SELECT User_ID, Amount AS Total_Amount
    FROM Sales_Present
    Amount >= 100000
    UNION ALL
    SELECT User_ID, Amount AS Total_Amount
    FROM Sales_Archive
    Amount >= 100000
) AS Total_List



또는 테이블 형태가 다른 경우도 있을 수 있을텐데, 괜히 설명만 길어지는 듯해서 그건 앞에서 본 예제에서 형태가 좀 다른 테이블에서 같은 형태의 컬럼을 SELECT 해서 나온 2개의 가상 테이블을 UNION해 합친다고 상상해 봄 될 것 같다. 이건 나중에 후반에 보안에서 SQL의 해석이 어떻게 필요한지에 대한 얘기를 하나 쓸거 같아서 그때 자세히 예제를 보자. 

현실에서 테이블 형태가 다른 경우라면 서로 형태가 다른 2개의 직원 테이블에서 공통된 정보(이름, 부서, 이메일)를 가져오는 경우가 있을 수 있을것 이다. 위의 예제에서도 Sales_Present와 Sales_Archive의 실제 컬럼이 완전히 같지 않더라도 User_ID, Amount 컬럼이 있거나 의미가 같은(컬럼 이름은 형식일 뿐이고 JOIN 에서 얘기했지만 데이터의 모양과 값 자체가 중요하다) 컬럼 들이 있다면 같은 이름으로 하나의 테이블을 만들 수 있을 것이다. 그래서 앞의 서로 다르지만 같이 힘을 모을 수 있는(데이터의 형태가 같은) 사람들이 모인 줄다리기 그림을 UNION 이라고 표현해봤다. 그리고 중복된 데이터를 제거하느냐의 차이를 가진 UNION ALL 이라는 키워드도 사족으로 있다.



마지막으로 테이블 간의 관계는 아니지만, 테이블 내의 데이터의 묶음 관계를 나타내기 때문에 현재의 글에서 같이 설명했음 하는 게 GROUP BY 이다. 가끔 여러 의미로 우리가 사회나 회사에서는 하나의 통계치로 나타날 뿐이라고 얘기하긴 하지만, GROUP BY 로 묶이는 순간에 그런 일들이 자동으로 이루어 지게 해준다. 

처음 GROUP BY 문을 만들게 되면 SELECT 문에 들어가는 컬럼이 왜 갑자기 이상하게 제한되게 되어 에러가 나는지에 대해서 헷갈리게 느낄 수 있는데, 이건 앞에서 얘기한 것 같이 GROUP BY 의 대상 이외에는 모두 통계적 익명성이 되기 때문 이라고 볼 수 있다.

<그룹>




위의 그림을 보면 우리가 해당 그림은 남자, 여자의 그룹 관점으로 보는 순간부터 남자 셋, 여자 셋이나, 남자들의 키의 평균, 여자들의 키의 평균, 남자들의 나이 합, 여자들의 나이 합 등으로 이루어질 수 밖에 없다. 그래서 GROUP BY 를 하는 순간 해당 GROUP BY 의 대상이 되는 컬럼은 그 대표성을 나타내는 남자, 여자 같은 키워드 역할을 하고, 나머지 컬럼 중에 통계적 의미를 가져올 수 있는 컬럼들은 SUM, COUNT, AVE 같은 수학 함수와 만나서 수치를 표기 하게 된다. 

왜 이렇게 답답하냐고 생각할수도 있지만 GROUP BY 가 집계를 기초로 한 통계 데이터를 쿼리를 통해서 쉽게 가져오기 하기 위한 것이기 때문에 어쩔수 없다고 생각하자. 그림으로 표시하면 아래와 같은 형태라고 볼 수 있다.

<GROUP BY>



말로 표시하자면 아래와 같을 것이다.

SELECT "분류를 원하는 데이터", COUNT, AVE, SUM 등(수치를 담은 컬럼이나 행 자체) 
FROM "집계 대상 테이블" 
GROUP BY "분류를 원하는 테이터가 들어가 있는 컬럼"



여기에서 "집계 대상 테이블" 에 추가로 제한을 넣고 싶다면 WHERE 문을 뒤에 넣음 된다. GROUP BY 를 통해 만들어진 테이블에 대해서 WHERE 조건을 넣으려면 조금 또 헷갈리는 키워드가 존재하는데, HAVING 이라는 키워드가 나온다. 위의 예제를 보면 컬럼 이름 이외에는 모두 통계적 수치이기 때문에 HAVING 에도 통계적 표현이 들어가야 해서 아마 좀 처음엔 의아할 것이다.

SELECT "분류를 원한는 데이터", COUNT, AVE, SUM 등(수치를 담은 컬럼이나 행 자체) 
FROM "집계 대상 테이블" 
GROUP BY "분류를 원한는 테이터가 들어가있는 컬럼" 
HAVING "같은 집계함수"



AI 를 통해 만든 아래의 쿼리를 보면 그룹에 등록한 20살 이상인 사람만 대상으로 스터디 그룹 별로 묶어서 평균 점수가 90점 인 스터디 그룹들만 "스터디 그룹 이름", "멤버 수", "평균 점수"를 표시해 준다. 

SELECT 
    Study_Group_Name,
    COUNT(*) AS Member_Count,
    AVG(Score) AS Average_Score
FROM Student_Registry
WHERE Age >= 20 
GROUP BY Study_Group_Name
HAVING AVG(Score) >= 90;



추가로 저 GROUP BY 도 2개 이상의 컬럼을 넣어서 특정 그룹안의 특정 타입 등으로 상세 분류로 집계할 수도 있겠지만(지역 별 스터디 그룹의 통계 숫자라든지) 시작은 위의 GROUP BY 개념에서 출발한다고 생각하고 찬찬히 따져보면 된다.



위에서 3개의 대표적인 관계를 나타내는 키워드 개념들을 설명했지만 사실 저건 어찌 보면 키워드 자체가 중요한 게 아니라 데이터를 해석하는 몇 가지 방식을 설명한 것이라고 보면 될 것 같다. SQL 은 그걸 쿼리로 테이블 안의 데이터를 지정해 쉽게 가져올 수 있게 구현한 개념이고 말이다. 여러 SQL 서버들은 그 개념들을 구현한 실체 들이다. 완전하게 같진 않지만 엑셀에서도 VLOOKUP 이라든가, GROUPBY, 피벗 테이블이라든지 vstack 같은 비슷하게 있다고 하고, 많은 데이터를 분석하는 툴에서도 마찬가지로 비슷한 관점의 기능이나 표현들이 있을 것이다. 

또한 위에서 얘기했듯이 기법은 데이터의 의미에 따라서 적용되는 패턴은 같지만 해석이 달라질 수 있다고 본다. 그래서 사실 적용하는 기법 이전에 중요한 게 데이터의 수집과 현실과 적절히 매칭된 설계라고 생각한다. 개인적으로는 현실의 모든 상황을 반영하는 완벽한 데이터가 세상에 존재할 수 있는지에 대해서는 조금 회의적이긴 한다. 이건 아날로그가 디지털로 완벽히 호환 가능하냐는 문제이기도 한 것 같고, 궁극적으로는 현실도 하나의 디지털 세상일 수 있을까 하는 문제하고도 연관되지 않을까 한다.

그래서 SQL 이 프로그램 세계에 기반한 엄청 기술적인 분야이기는 하지만, 반대로 보면 바깥의 데이터 수집 측면까지 고려했을 때 어느 정도 100% 완벽하진 않지만 확률이나 통계적으로 의미 있는 데이터의 저장과 해석을 추구한다는 시작점에서 지원되는 문법들을 봤으면 하는 생각도 있긴 하다. 또 나중에 이것과 다르게 데이터를 해석하려 하는 다른 툴과 기법들도 계속 관심을 가지면 좋겠다는 생각도 하고 말이다. AI 또한 그런 강력한 해석 툴 중에 하나라고 본다. 사실 뭐 이런 얘기에 대해서 근거에 기반에 얘기할 만큼 충분히 이 쪽 분야를 이해는 못한다고 생각하기 때문에 걍 개인적인 밤 시간의 지나가는 상념으로 봐줘도 무방할 듯은 하다.

- FIN -

posted by 자유로운설탕
prev 1 next