Developer.

[멋사 백엔드 19기] TIL 21일차 SQL 심화

📂 목차


📚 본문

SQL 명령 정리

  • CREATE TABLE: 테이블 생성
  • ALTER TABLE: 테이블 구조 변경
  • DROP TABLE: 테이블 삭제
  • RENAME: 이름 변경
  • TRUNCATE: 테이블의 모든 데이터 삭제
  • COMMENT: 테이블에 설명 추가

INDEX 깊이 있게 다루기

인덱스는 테이블의 검색 속도를 향상시키기 위해 특정 컬럼에 대해 생성하는 자료구조이다.

-- 복합 인덱스 생성
create index emp_name_sal_idx
on employees(name, salary);

-- 인덱스 확인
show index from employees;

-- 인덱스 삭제
alter table employees
drop index emp_name_sal_idx;

유니크 값이 많은 컬럼이 효율적이다.

이제 인덱스 실행 계획을 보자.

EXPLAIN

쿼리의 성능을 분석하고 병목 구간을 찾기 위해 사용하는 구문이다. db 가 테이블 스캔 방식이나 인덱스 사용 여부, 조인 순서 등을 어떻게 결정했는지 확인이 가능하다.

-- 인덱스 사용 전
EXPLAIN SELECT * FROM employees 
WHERE hire_date = '2005-01-01';

-- 인덱스 생성
CREATE INDEX emp_hire_idx ON employees(hire_date);

-- 인덱스 사용 후
EXPLAIN SELECT * FROM employees 
WHERE hire_date = '2005-01-01';

여기서 index 로 설정된 열의 값을 where 절에서 변질시키면 안된다. 예를들어서 index 가 있는 name 이라는 속성을 where 절에 썼다고 해보자. 그러면 substring(name, 1, 1) 등으로 이를 변질시키면 index 를 사용하지 못하게 된다. LIKE 를 써서 이를 변질시키지 말고 해야한다.

FULLTEXT INDEX

문자열 컬럼(CHAR, VARCHAR, TEXT)를 대상으로 빠른 키워드 검색을 지원하는 인덱스이다. LIKE '%keyword%' 보다 훨씬 효율적이고, 자연어 처리 기반으로 동작하며, 단어 단위로 인덱스를 생성한다.

MySQL 5.6 이상에서 InnoDB 엔진에서도 FULLTEXT 지원

-- 테이블 생성할 때 생성
create table t1 (
    varchar(36) id primary key,
    varchar(20) username not null,
    varchar(255) password not null,
    text introduction not null,
    fulltext idx_intro (introduction)
);

-- 테이블 생성 후 인덱스 생성
create fulltext index idx_intro
on t1(introduction)
MATCH … AGAINST 구문

fulltext 로 인덱스를 생성했다면 해당 컬럼에 대한 검색을 수행할 수 있다.

-- 자연어 모드
select * from t1
where match(introduction)
against('hello, ');

-- boolean 모드
select * from t1
where match(introduction)
against('+hello -name' in boolean mode);

-- query expansion 모드
select * from article
where match(introduction)
against('database' with query expansion);
  • Natural Language Mode: 단어의 등장 빈도(TF-IDF) 기반으로 점수 계산 -> 연관도 높은 순으로 결과를 반환
  • Boolean Mode: +(필수 포함), -(제외), *(와일드카드), “(정확히 일치)
  • Query Expansion: 첫 검색 결과의 연관 단어들을 다시 확장하여 검색(추천 검색과 유사)
제약사항

이러한 FULLTEXT 는 다음과 같은 제약사항을 따른다.

  • ft_min_word_len 보다 짧으면 인덱싱이 안됨
    • 최소 단어 길이 변수는 MySQL 서버 시스템 변수를 따라가며 기본값은 4로 되어 있다.
    • DB 서버 재시작 후 다시 적용해야 한다(rebuilding 필요)
  • 기본적으로 불용어(a, an, the, …)등은 기본적으로 무시한다
  • collation 의 문자 정렬 규칙에 따라 대소문자 구분 여부를 정할 수 있다.
    • utf8_general_ci: 대소문자는 구분하지 않음
    • utf8_bin 계열: 대소문자 구분
  • 영어/라틴 문자 중심으로 설계가 되어서 한국어/일본어/중국어 등등의 언어들은 n-gram 파서를 활성화해야 한다.

collation

-- Collations 확인
show full columns from t1;

-- 변경
alter table t1
modify introduction varchar(255)
collate utf8_bin;

n-gram 사용

-- MySQL 5.7 부터 지원
create fulltext index idx_test
on t1(introduction)
with parser ngram;

Data Types 심화

Date and Time Types in SQL

  • date: 연도, 월, 일
  • time: 시, 분, 초, time(p) 사용 시 초의 소수 자릿수를 지정 가능 또한 time with timezone 을 사용시 시간대 정보도 함께 저장 가능
  • timestamp: 날짜와 시간을 결합, timestamp(p) 도 사용가능(기본값 6), timestamp with timezone 을 지정 시 시간대 정보도 함께 저장

중요

MySQL 은 datetime 타입을 제공해저서 timestamp with timezone 과 같은 기능을 얻을 수 있다.

Default Values

default 는 속성에 기본값을 지정하도록 하는 제약조건이다. create 문에서 지정할 수 있으며, 다음과 같이 선언한다.

create table student
(
  ID varchar(5),
  name varchar(20) not null,
  dept_name varchar(20),
  tot_cred numeric(3,0) default 0,
  primary key (ID)
);

tot_cred 속성의 기본값은 0이기에 레코드를 삽입할 때, 값을 제공하지 않으면 자동으로 0으로 설정된다.

Large-Object Types

사진, 고해상도 의료 이미지, 동영상 등과 같은 큰 데이터 항목을 도메인으로 갖는 속성을 저장하고 싶을 때는 다음을 사용한다:

  • CLOB: 문자 데이터용 대형 객체
  • BLOB: 이진 데이터용 대형 객체
book_review CLOB(10KB)
image BLOB(10MB)
movie BLOB(2GB)

여기서 이들을 결과로 내보낼때 대형 객체를 메모리 상에 올리는 것은 현실적으로 불가능하기에 이 대신 Locator 라는 것을 주고 로케이터를 통해 어플리케이션이 작성된 호스트 언어에서 객체를 참조하는 형태로 조작하게 된다.

운영체제의 read 와 유사하다.

User-Defined Types

SQL 은 두 가지 형태의 사용자 정의 데이터 타입을 지원한다.

  • distinct type
  • structured data type: 중첩된 레코드 구조, 배열, 멀티셋 등

distinct type

create type Dollars as numeric(12,2) final;
create type Pounds as numeric(12,2) final;

create table department
(
  dept_name varchar(20),
  building varchar(15),
  budget Dollars
);

개념적으로 서로 달라야 하지만 타입은 유사한 것들은 distinct type 을 쓰자.

단점으로는 위 예시에서는 (department.budget + 20) 이라는 표현식이 strong type checking 때문에 허용되지 않는 표현식이다. 따라서 다음 식을 통해 바꾸는 것을 수행해줘야 한다.

cast(department.budget as numeric(12,2))

이렇게 하면 숫자로 바꿔서 결과를 낼 수 있지만 이를 다시 저장할 때는 또 바꿔줘야 한다.

Domain

이 대안으로 domain 을 만들 수 있는데,

create domain DDollars as numeric(12,2) not null;

DDollars 는 다음 차이점이 있다.

Domain 특징

  • Dollars 와는 달리 not null 제약조건을 추가가능, primary key 로 정의 가능
  • 강력한 타입이 아니기 때문에 기본 타입이 호환되는 한 한 도메인 타입의 값을 다른 도메인 타입에 할당할 수 있음

예시

-- 범위 제한
create domain YearlySalary numeric(8,2)
constraint salary_value_test check(value >= 29000.00);

-- 특정 값으로의 제한
create domain degree_level varchar(10)
constraint degree_level_test
check (value in ('Bachelors', 'Masters', 'Doctorate'));

Generating Unique Key Values

표준 SQL 에서는 다음과 같이 값을 unique 하게 생성하여 저장할 수 있도록 하는 옵션이 있다.

ID number(5) generated always as identity

여기서 주의할 점은 이는 숫자형 키 값에 대해서만 작동한다.

generated as identity

  • PostgreSQL: SERIAL 을 쓴다.
  • MySQL: AUTO_INCREMENT 를 쓴다.

JDBC

이제 이 sql 서버를 구동중이고 이를 다루는 다양한 기술들 중 JDBC를 본다.

기본적으로 sql 만으로는 표현할 수 있는 것에 한계가 있기 때문에 이러한 쿼리를 작성하려고 하면 embedded 하여 사용해야 한다. 또한, 비선언적 작업은 SQL 만으로 수행할 수 없다. 따라서 보고서를 출력하거나, 사용자와 상호작용하거나, 쿼리를 GUI 에 전달하는 등의 작업이다. 나머지 구성 요소는 범용 프로그래밍 언어로 작성되므로 통합된 어플리케이션을 만드려면 SQL 과 범용 프로그래밍 언어를 결합할 수단이 필요하다.

이때 JDBC를 활용하여 SQL 과 소통을 할 수 있다.

Connecting to the DB

// https://mvnrepository.com/artifact/mysql/mysql-connector-java
implementation 'mysql:mysql-connector-java:8.0.33'

우선 위를 gradle.build 에 넣어 mysql driver 를 추가하자.

Java 에서 java.sql 패키지에서는 db 와 연결할 수 있는 커넥션이라는 인터페이스를 제공한다. 여기서 커넥션의 설정을 DriverManager 로 설정하고 설정된 Connection 을 가져올 수 있다.

Connection = DriverManager.getConnection(
    "jdbc:mysql://localhost:3306/hr", // url
    userid, // user
    passwd // password
);
  • url 매개변수: 서버가 실행중인 url, 포트 번호, db 이름 을 차례대로 입력해주면 해당 DB로 넘어가게 된다. db 는 생략되도 된다.
  • user 매개변수: 문자열을 넣고 db 사용자 id 를 입력
  • password 매개변수: 유저에 대한 비밀번호이다. 보통은 Java 에 직접치지 않고 따로 저장하여 운용한다.

오라클 프로토콜은 jdbc:oracle:thin 이다.

Statement

DB 연결이 됐다면, 이제 SQL 문을 실행할 준비가 된 것이다.

java 에서는 이 DB에게 명령을 내리기 위해 Statement 클래스가 있는데, 이를 통해서 전송하고 실행할 수 있게 된다.

Statement stmt = conn.createStatement();
stmt.executeUpdate("insert into instructor values(’77987’,’Kim’,’Physics’,98000)");

Statement 는

  • executeQuery()
  • executeUpdate()

메서드를 가지고 있어 이를 호출할 수 있는데 query 로 메서드를 실행하면 ResultSet 을 반환하며, 쿼리문이 아닌 문장은 결과 집합을 반환하지 않고 int 를 반환하게 된다. 이때는 삽입, 갱신, 삭제 된 튜플 수를 반환하게 된다.

업데이트 값이 너무 많아 int 값 범위를 벗어날 때, executeLargeUpdate() 를 쓸 수 있다.

Exception

SQL 메서드를 실행할 때에는 반드시 예외처리를 해주어야 하는데, SQLException 의 처리를 통해 try-catch 를 해주면 되겠다.

또한, Connection, Statement, 기타 JDBC를 여는 객체는 시스템 자원을 소모하기 때문에 프로그래머는 반드시 close() 를 호출해주어야 한다. 이때는 try-resource-with 과 함께 사용한다.

PreparedStatement

Statement 만 계속 사용하면 특정 sql 문장을 여러개 사용하고 싶을때, 수행하고 싶은 만큼의 문장들을 선언해주어야 한다. 이를 방지하기 위해 java 에서는 PreparedStatement 를 제공해준다.

conn.prepareStatement("insert into instructor values(?,?,?,?)");

? 를 파싱하여 다른 값들로 대체하여 넣을 수 있도록 해준다.

pStmt.setString(1, "88877");
pStmt.setString(2, "Perry");
pStmt.setString(3, "Finance");
pStmt.setInt(4, 125000);
pStmt.executeUpdate();
pStmt.setString(1, "88878");
pStmt.executeUpdate();

마지막에서는 1번 파라미터만 88878 로 바꾸고 나머지는 동일하게 한 후 실행하게 할 수 있다.

Callable Statements

DBMS 측에 선언된 함수와 프로시저에 대한 수행을 하라고도 할 수 있다.

CallableStatement cStmt1 = conn.prepareCall("{? = call some_function(?)}");
CallableStatement cStmt2 = conn.prepareCall("{call some_procedure(?,?)}");
자동 생성된 키 가져오기

add, update, delete 와 같은 처리는 반환 값이 굳이 없어도 되지만(boolean 으로 처리 완료를 나타내는 값을 반환하기도 한다), 가끔 DB 자체에서 생성하는 고유한 ID 들은 프로그래밍 단에서는 ID를 생성하지 않기 때문에 가져올 수 없다. 이 기본키가 가장 중요한 값인데, 이를 가져오지 못하면 저장한 행에 대해 후처리를 할 수가 없다. 이를 위해서 Statement 에서는 RETURN_GENERATED_KEYS 를 제공하고 있다.

사용법은 다음과 같다:

public Long insertAndGetId(String email, String name, String password) {
    String sql = "INSERT INTO user (email, name, password) VALUES (?, ?, ?)";
    Long generatedId = null;

    try (Connection conn = getConnection();
         PreparedStatement ps = conn.prepareStatement(sql, 
                Statement.RETURN_GENERATED_KEYS)) {

        ps.setString(1, email);
        ps.setString(2, name);
        ps.setString(3, password);

        int updateCount = ps.executeUpdate();

        // 생성된  가져오기
        if (updateCount > 0) {
            try (ResultSet rs = ps.getGeneratedKeys()) {
                if (rs.next()) {
                    generatedId = rs.getLong(1);
                }
            }
        }
    } catch (SQLException e) {
        e.printStackTrace();
    }

    return generatedId;
}