본문 바로가기

DevOps.

SQL이란 무엇인가

728x90

 

오늘날 구조화 쿼리 언어(Structured Query Language)는 비록 제품들 간에 자사 고유의 확장 기능을 가지고 있기는 하지만, 관계형 데이터베이스에서 데이터를 조작하고 쿼리하는 표준 수단이다. SQL의 편의성과 보편성은 하둡(Hadoop)을 위시한 수 많은 NoSQL 또는 비관계형 데이터 저장(Non-Relational Data Stores) 기술도 SQL의 일부를 도입하거나 SQL과 유사한 쿼리 언어를 만들어 낼 정도이다.

그렇지만 SQL이 과거에도 “보편적인” 관계형 데이터베이스용 언어는 아니었다. 초기, 그러니까 1980년대에 SQL은 반대 세력이 적지 않았다. 당시에는 많은 연구원과 개발자들이 SQL의 오버헤드로 인해 업무용 데이터베이스로서는 결코 실용적이지 않다고 생각했다.

분명 잘못된 생각이었다. 하지만 아직도 많은 이들이 SQL의 모든 편의성과 접근성에도 불구하고 런타임 성능에 따라 부과되는 대가가 너무 클 때가 많다고 생각한다.

SQL이 있기 전

SQL이 있기 전에는 데이터베이스는 밀접하게 결합된 탐색형 프로그래밍 인터페이스를 가지고 있었으며, 대개의 경우 코다실(CODASYL) 데이터 모델이라 부르는 네트워크 스키마(Schema)를 중심으로 설계되었다. 코다실(CODASYL: Conference/Committee on Data Systems Languages)은 1959년부터 코볼(COBOL) 프로그래밍 언어 그리고 10년 뒤부터는 데이터베이스 언어 확장기능을 책임지던 컨소시엄이었다.

코다실 데이터베이스에 대하여 프로그래밍을 했다면, 일대다 관계를 나타내는 세트(Set)를 통해서 레코드(Record)를 탐색했을 것이다. 구식 계층형(Hierarchical) 데이터베이스는 레코드가 한 개의 세트에만 속할 수 있도록 한다. 네트워크 데이터베이스는 레코드가 여러 개의 세트에 속할 수 있게 해준다.

CS 101에 등록한 학생들을 열거하고 싶어한다고 하자. 우선 이름으로 Courses 세트에서 “CS 101”을 찾을 수 있을 것이다, 그것을 Enrollees 세트의 소유자 또는 부모(Parent)로 설정하고, Enrollees 세트의 첫 번째 멤버(ffm)를 찾으면, 그것이 Student 레코드인데, 그것을 나열하면 된다. 그 다음에는 루프에 들어간다. 다음 멤버 (fnm)을 찾아서 그것을 나열한다. fnm에 실패하면, 루프를 빠져나오면 된다.

이렇게 하는 것이 데이터베이스 프로그래머에게는 많은 쓸데없는 일처럼 보일 수도 있지만, 런타임에는 매우 효율적이었다. 캘리포니아 대학교 버클리 & 인그레스 분교의 마이클 스톤브레이커 같은 전문가들은 코다실 데이터베이스에서 이런 종류의 쿼리를 하는 것이 SQL을 사용해서 관계형 데이터베이스에 대해 동일한 쿼리를 하는 것보다 약 절반의 CPU 시간과 절반 이하의 메모리를 사용했다고 지적했다.

비교를 위해, CS 101에 등록한 모든 학생들을 되돌려주기 위한 동일한 SQL 쿼리는 아마도 다음과 같을 것이다.

 

SELECT student.name FROM courses, enrollees, students WHERE course.name ="CS 101"


이 구문은 관계형 내부 조인(Inner Join) (실제로는 두 개의 조인)을 암시하고 있으며, 조인용으로 사용되는 필드 같은 몇 가지 중요한 세부사항을 빼놓고 있다.

 

관계형 데이터베이스와 SQL

실행 속도와 메모리를 두 배로 늘리는 것을 포기할 이유가 있는가? 포기하는 데는 두 가지 커다란 이유가 있다. 바로 개발 편이성과 이식성이다. 성능과 메모리 요구조건과 비교하여 둘 중 어느 한 가지도 중요하지 않다고 생각되지만, 컴퓨터 하드웨어가 개선되고 가격이 더 저렴해지면서 사람들은 실행 속도와 메모리보다는 개발 비용을 더 많이 걱정했다.

즉, 무어의 법칙이 관계형 데이터베이스를 위해 코다실 데이터베이스를 죽였다. 공교롭게도, 개발 시간은 상당히 단축되었지만, SQL 이식성은 몽상으로 판명 낫다.

관계형 모델과 SQL의 기원은 어디인가? 에드가 “테드” 코드는 1960년 대에 IBM 산호세 연구소에서 관계형 모델에 대한 이론을 고안하고 그 결과를 1970년에 발표한 컴퓨터 과학자이다. IBM은 자사의 코다실 데이터베이스인 IMS/DB의 매출을 보호하기 위한 노력의 일환으로 관계형 데이터베이스의 구현에 적극적이지 않았다. 마침내 IBM이 자사의 시스템 R 프로젝트를 시작했을 때, 개발 팀(돈 챔버린과 레이 보이스)은 테드 코드 소속이 아니었으며, 자신들의 언어인 시퀄(Sequel: Structured English Query Language: 구조화 영어 질의어)을 설계하기 위해 코드의 1971년 알파 관계형 언어 논문을 무시했다. 1979년 IBM이 자사의 제품을 출시하기도 전에, 래리 앨리슨은 (준비단계였던 IBM 시퀄 관련 사양들을 자신의 사양에 사용함으로써) 자사의 오라클 데이터베이스에 이 언어를 포함시켰다. 시퀄은 국제 상표권 침해를 회피하기 위해 곧 SQL이 되었다.

SQL에 대한 칭송은 오라클과 IBM뿐아니라 고객들에게서도 들려왔다. 코다실 데이터베이스 설계자와 프로그래머를 고용하기가 쉽지 않았기 때문에, 시퀄(그리고 SQL)이 훨씬 더 매력적으로 보였다. 1980년대 후반에는 SQL이 너무나 매력적이어서 관계형이기 위해서는 관계형 데이터베이스를 처음부터 새로 설계해야만 한다고 생각하고 있던 코드에게는 엄청나게 실망스럽게도, 많은 데이터베이스 공급업체들이 자사의 코다실 데이터베이스 위에 SQL 쿼리 처리기를 기본적으로 탑재할 정도였다.

코드가 설계한 것 같은 순수한 관계형 데이터베이스는 일차 술어 논리(First-Order Predicate Logic)와 일관되게, 관계(Relation)로 그룹화 되는 튜플(Tuple)을 기반으로 구축된다. 실제 관계형 데이터베이스는 필드, 제약 조건(Constraint), 그리고 트리거(Trigger)를 포함하고 있는 테이블을 가지고 있으며, 테이블은 외래 키(Foreign Key)를 통해서 관계가 맺어진다. 리턴 데이터를 선언하기 위해 SQL이 사용되며, SQL 쿼리 처리기(Processor)와 쿼리 최적화기(Optimizer)는 SQL 선언을 데이터베이스 엔진이 실행하는 쿼리 계획(Query Plan)으로 바꾼다.

SQL에는 데이터 수정을 위한 특수 언어인 DML(Data Manipulation Language: 데이터 조작 언어)과 함께, 스키마를 정의하는 특수 언어인 DDL(Data Definition Language: 데이터 정의 언어)이 포함되어 있다. 이 두 가지 모두 초기 코다실 사양에 뿌리를 두고 있다. SQL의 세 번째 특수 언어는 SELECT 문과 관계형 결합(Relational Join)을 통해서 쿼리를 선언한다.

 

SQL의 SELECT 문

SELECT 문은 쿼리 최적화기에게 어떤 데이터가 리턴되는지, 어떤 테이블을 살펴보아야 하는지, 어느 관계를 따라야 하는지, 그리고 리턴된 데이터에 어떤 순서를 부여해야 하는지를 알려준다. 쿼리 최적화는 특정 데이터베이스가 인덱스 힌트(Index Hint)를 지원하지 않는 한, 무작위 테이블 스캔(Brute Force Table Scan)을 회피하고 좋은 쿼리 성능을 달성하기 위해 어떤 인덱스를 사용해야 할지를 스스로 알아내야만 한다.

 

관계형 데이터베이스 설계의 기술 중 일부는 인덱스의 신중한 사용에 달려있다. 빈번한 쿼리에 대한 인덱스를 생략하면, 읽기 부하가 많은 경우, 전체 데이터베이스가 저하될 수 있다. 과도한 인덱스는 쓰기 작업이나 업데이트 부하가 많은 경우, 전체 데이터베이스의 성능을 저하시킬 수 있다.

또 다른 중요한 기술은 모든 테이블에 대해서 적합하고, 독특한 기본 키(Primary Key)를 선택하는 것이다. 흔한 쿼리에 대한 기본 키의 영향을 고려해야 할 뿐 아니라, 다른 테이블에서 그 키가 외래 키로 나타났을 때 조인 작업에서 어떻게 동작할지, 그리고 데이터의 참조 지역성(Locality of Reference)에는 어떤 영향을 줄지도 고려해야만 한다.

수평 샤딩(Horizontal Sharding)이라 부르는 기본 키의 값에 따라 서로 다른 볼륨으로 분할되는 데이터베이스 테이블에 대한 고급 사례에서, 사용자는 샤딩에 기본 키가 어떤 영향을 줄 것인지도 고려해야만 한다. 사용자는 여러 개의 볼륨에 걸쳐 테이블이 공평하게 분산되기를 원하고 있다, 이는 기본 키로 날짜 스탬프(Data Stamp)나 연속적인 정수를 사용하고 싶어하지 않는 다는 것을 의미한다.

SELECT 문에 대한 논의는 간단하게 시작되지만, 금방 헷갈릴 수 있다. 다음을 생각해 보자:

 

SELECT * FROM Customers;

 

간단하다. 그렇지 않은가? Customers 테이블에 있는 모든 필드와 모든 열을 요청하고 있다. 그렇지만, Customers 테이블에 수백만 개의 열과 수백 개의 필드가 있다고 가정해보자. 그리고 필드 중 하나가 주석용으로 사용되는 커다란 텍스트 필드라고 해보자. 각 열에 평균 1 KB의 데이터가 있다면, 10 Mbps 네트워크 연결을 통해서 그 모든 데이터를 가져오는데 얼마나 걸릴까?

아마도 유선을 통해서 송신하려는 데이터 양을 줄여야 할 것이다. 다음을 생각해 보자:

 

SELECT TOP 100 companyName, lastSaleDate, lastSaleAmount, totalSalesAmount FROM Customers
WHERE state ="Ohio" AND city ="Cleveland"
ORDER BY lastSaleDate DESCENDING;

 

이제는 훨씬 더 적은 데이터를 가져올 것이다. 데이터베이스에 클리브랜드에 있는 회사들만을 고려하고, 가장 최근에 판매가 이루어진 100개의 기업만을 전달해 줄 것을 요청했다. 그렇지만 데이터베이스 서버에서 이 작업을 가장 효율적으로 수행하기 위해서는, WHERE 절(Clause)을 위해 Customers 테이블에 state+city에 대한 인덱스 그리고 ORDER BY와 TOP 100 절을 위해 lastSaleDate에 대한 인덱스가 필요하다.

어쨌거나, TOP 100이 SQL 서버(Server)와 SQL 애저(Azure)용으로는 유효하지만, MySQL이나 오라클에서는 유효하지 않다. MySQL에서는 WHERE 절 다음에 LIMIT 100을 사용했을 것이다. 오라클에서는 WHERE 절의 일부로 ROWNUM에 대한 바운드(Bound) 즉, WHERE... AND ROWNUM <=100를 사용했을 것이다. 불행하게도 ANSI/ISO SQL 표준 (그리고 현재까지 9개가 1989년부터 2016년까지 계속됨)은 그때까지만 계속되고, 그 이후에는 각각의 데이터베이스가 자체적으로 고유의 절과 기능을 도입하고 있다.

 

SQL에서 관계형 결합(조인)

JOIN 절을 설명할 수 있으려면, 외래 키와 테이블들 간의 관계에 대해서 알고 있어야 한다. SQL 서버 구문을 사용해서, DDL로 작성된 예제를 사용해서 설명해보도록 하겠다.

 

이번 예제의 짧은 버전은 아주 간단하다. 관계에서 사용하려는 모든 테이블에는 기본 키 제약을 가지고 있어야 한다. 한 개의 필드이거나 표현식으로 정의된 여러 필드들의 조합일 수 있다. 예를 들면:

 

CREATE TABLE Persons (
PersonID int NOT NULL PRIMARY KEY,
PersonName char(80),
...

 

Persons와 관계를 맺어야 하는 모든 테이블은 Persons 기본 키에 상응하는 필드를 가지고 있어하며, 관계형 무결성을 보존하기 위해서는 해당 필드가 외래 키 제약을 가지고 있어야 한다. 예를 들자면:

 

CREATE TABLE Orders (
OrderID int NOT NULL PRIMARY KEY,
...
PersonID int FOREIGN KEY REFERENCES Persons(PersonID)
);

 

CONSTRAINT 키워드를 사용하는 두 가지 문에 대한 긴 버전도 있으며, 여기서는 사용자가 제약에 이름을 붙일 수 있다. 대부분의 데이터베이스 설계 도구들이 생성하는 것이 이것이다.

기본 키는 늘 인덱스되며 유일하다(Unique). 즉, 필드 값은 중복될 수 없다. 다른 필드들에 대한 인덱스는 옵션이다. 외래 키 필드 그리고 쓰기 작업과 업데이트로 인한 잠재적 오버헤드 때문에 능 그런 것은 아니지만, WHERE와 ORDER BY 절에 나타나는 필드에 대한 인덱스를 생성하는 것이 유용한 경우기 많다.

독자들이라면 John Doe가 주문한 모든 것을 되돌려주는 쿼리를 어떻게 작성할 것인가?

 

SELECT PersonName, OrderID FROM Persons
INNER JOIN Orders ON Persons.PersonID = Orders.PersonID
WHERE PersonName ="John Doe";

 

실제로, 네 가지 종류의 JOIN이 있다: INNER, OUTER, LEFT, 그리고 RIGHT. INNER JOIN이 기본이며(INNER라는 단어는 생략할 수 있다), 두 개의 테이블 모두에서 일치하는 값을 포함하고 있는 행들만을 포함하고 있기도 하다. 주문 여부와 관계없이 사람들(Persons)을 나열하고 싶었다면, 다음 예와 같이 LEFT JOIN을 사용했을 것이다:

 

SELECT PersonName, OrderID FROM Persons
LEFT JOIN Orders ON Persons.PersonID = Orders.PersonID
ORDER BY PersonName;

 

두 개 이상의 테이블을 결합(Join)하는 쿼리 실행을 시작하거나, 표현식을 사용 또는 강제 데이터 타입을 사용하려 할 때, 처음에는 이 구문이 조금 아슬아슬해 보일 수 있다. 다행히도, 대개 스키마 다이어그램에서 쿼리 다이어그램으로 테이블과 필드를 끌어다 놓음으로써, 정확한 SQL 쿼리를 생성할 수 있는 데이터베이스 개발 도구들이 있다.

 

저장 프로시저

때로는 SELECT 문의 선언적 속성이 사용자의 의도대로 이루어지지 않는다. 대부분의 데이터베이스는 저장 프로시저라는 기능을 가지고 있다. 불행하게도 거의 모든 데이터베이스들이 ANSI/ISO SQL 표준에 대한 고유의 확장 기능을 사용하고 있는 영역이기도 하다.

SQL 서버에서, 저장 프로시저 (또는 저장 프록(stored proc))에 대한 초기 다이얼렉트(Dialect, 방언)는 트랜잭트(Transact)-SQL 일명 T-SQL이었다. 오라클에서는 PL-SQL이다. 두 가지 데이터베이스는 저장 프로시저용으로 C#, 자바 그리고 R 같은 추가 언어를 더했다. 간단한 T-SQL 저장 프로시저는 단지 SELECT 문의 매개변수와 버전일 수도 있다. 이것의 장점은 사용의 편의성 그리고 효율성이다. 저장 프로시저는 실행될 때마다가 아니라 저장될 때 최적화된다.

좀 더 복잡한 T-SQL 저장 프로시저는 여러 개의 SQL 문, 입출력 매개변수, 지역 변수, BEGIN…END 블록, IF…THEN…ELSE 조건문, 커서 (세트에 대한 행 단위 처리), 표현식, 임시 테이블, 그리고 다른 절차형 언어의 모든 것을 사용할 수 있다. 저장 프로시저 언어가 C#, 자바 또는 R 인 경우, 해당 절차형 언어의 함수와 구문을 사용할 것이 분명하다. 즉, SQL을 사용하는 동기가 표준화된 선언적 쿼리를 사용하기 위한 것이라는 사실에도 불구하고, 현실 세계에서는 특정 데이터베이스 관련 절차형 서버 프로그래밍을 많이 볼 수 있다.

그렇다고 이런 현실이 우리를 코다실 데이터베이스 프로그래밍의 나빴던 과거로 되돌려 놓지는 않겠지만(비록 커서는 거의 그렇지만), SQL 문들을 표준화하고 성능에 대한 우려는 데이터베이스 쿼리 최적화기에 맡겨두는 것이 옳다는 생각이 들게는 한다. 다 따지고 보면, 성능을 두 배로 늘리는 것이 남겨놓은 문제가 너무 많다.

 

 

출처

“데이터베이스의 언어” SQL이란 무엇인가

728x90
반응형
LIST