mysql, 주요 내장 함수의 활용

MySQL의 내장 함수

  • DDL, DML이 ANSI 표준에 따라 일정 부분 호환 가능하다. 다만, DBMS 간 내장 함수는 사실상 호환되지 않는다.
  • MySQL은 자체적으로 제공하는 내장 함수 이외에 사용자 정의 함수(User Defined Function)를 제공한다. 사용자가 직접 함수를 구현할 수 있다.

NULL 값 비교 및 대체 (IFNULL, ISNULL)

  • IFNULL: 첫 번째 인자가 NULL일 경우 두 번째 인자를 넘기며 그렇지 않을 경우 첫 번째 인자를 넘긴다.
  • ISNULL: 인자의 NULL 여부를 판정한다.
select
    IFNULL(NULL, '널!') -- 널!
    , IFNULL('낫널!', '널!') -- 낫널!
    , ISNULL('낫널!') -- 0
    , ISNULL(null) -- 1
;

현재 시각 조회(NOW, SYSDATE)

  • NOW와 SYSDATE 모두 현재 시간을 제공한다.
  • 다만, NOW는 하나의 SQL에서 동일한 시간을 보장하나 SYSDATE는 함수가 호출되는 시점의 시간을 정확하게 나타낸다.
-- NOW는 동일하나 SYSDATE는 다르다.
SELECT
    NOW(), SYSDATE()
    , SLEEP (1)
    , NOW(), SYSDATE();
  • 이러한 특징으로 NOW는 상수로 사용되나 SYSDATE는 그렇지 않다. 비확정적인 값(NOT-DETERMINISTIC)은 인덱스 사용에 제한적이므로 NOW를 사용한다.
  • 인덱스 활용과 별도로 SYSDATE는 데이터 불일치 문제가 발생시킬 수 있다. WHERE SOME_DATE > SYSDATE() 형태의 쿼리를 작성할 경우, 해당 시간은 해당 함수가 호출되는 시점에 판정되며 이로 인한 예상치 못한 데이터가 추출될 수도 있다.
  • 일반적으로 NOW()만 사용하는 것을 권장한다. 이미 SYSDATE를 사용 중이며 쿼리 수정이 어려운 경우, 시스템 변수 sysdate-is-now를 통해 SYSDATE의 결과가 NOW가 동일하도록 설정할 수 있다.

날짜와 시간의 포맷(DATE_FORMAT, STR_TO_DATE)

  • DATETIME 타입의 칼럼을 원하는 형태의 문자열로 변환하거나 특정 포맷의 문자열을 DATETIME으로 변환할 수 있다.
SELECT
  DATE_FORMAT(NOW() , '%Y년 %m월 %d일 %H시 %i분 %s초')
  , STR_TO_DATE('2024년 03월 10일 00시 16분 26초', '%Y년 %m월 %d일 %H시 %i분 %s초');

날짜와 시간의 연산(DATE_ADD, DATE_SUB)

  • 날짜를 더하거나 뺄 수 있다. DATE_ADD로 뺄 수 있으므로 보통 DATE_ADD으로 통일하여 사용한다.
  • SELECT DATE_ADD(NOW(), INTERVAL -1 DAY ); -- 어제 날짜가 출력된다

타임스탬프 연산(UNIX_TIMESTAMP, FROM_UNIXTIME)

  • 타임스탬프를 제공하거나 혹은 타임스탬프로 현재 시간을 제공한다.
  • 32비트로 인한 2038년 문제가 발생한다.
SELECT
    UNIX_TIMESTAMP()
    , UNIX_TIMESTAMP('2023-01-01')
    , FROM_UNIXTIME(1672531200);

문자열 처리 및 결합

  • TRIM: 문자열 처음과 끝의 공백 문자를 제거한다.
  • CONCAT: 문자열을 합성한다.
SELECT
    TRIM('   hi    ') -- hi
    , CONCAT('hello', ', ', 'world', '!') -- hello, world!
;

GROUP BY 문자열 결합(GROUP_CONCAT)

  • 여러 개의 문자열 레코드를 특정 문자열로 연결하여 하나의 문자열로 만든다.
  • 중복은 DISTINCT를 사용하여 제거할 수 있다.
  • 메모리 버퍼의 크기는 group_concat_max_len에 따른다.
SELECT
  GROUP_CONCAT(dept_no)
   , GROUP_CONCAT(dept_no SEPARATOR '|')
   , GROUP_CONCAT(DISTINCT dept_no)
   , GROUP_CONCAT(dept_no ORDER BY dept_no ASC)
FROM departments;

값의 비교와 대체(CASE WHEN…THEN…END)

  • 프로그래밍에서의 switch 문장과 같다.
  • when에서 true로 판정 될 때, then 문장의 쿼리가 실행된다.
  • 아래는 지연판정을 통해 성능을 개선하였다. 성별이 F일 때만 시간이 많이 걸리는 쿼리를 호출한다.
SELECT de.dept_no, e.first_name, e.gender,
    CASE 
        WHEN e.gender = 'F' THEN (SELECT s.salary FROM salaries s WHERE s emp_no =e.emp_no ORDER BY from_date DESC LIMIT 1)
        ELSE 0 
    END AS last_salary
FROM dept_emp de, employees e
WHERE e.emp_no=de.emp_no
  AND de.dept_no='d001';

타입의 변환(CAST, CONVERT)

  • 리터럴의 타입을 명시하거나 타입의 변환이 필요할 때 사용한다.
  • CAST와 CONVERT를 사용할 수 있다. CAST와 CONVERT는 거의 동일하게 동작한다.
  • SELECT CAST('1234' AS SIGNED INTEGER) AS converted_integer;

처리 대기(SLEEP)

  • 쿼리 실행 도중 멈춰서 대기하는 기능.
  • SQL의 개발이나 디버깅 과정에서 일부러 쿼리의 실행을 오랜 시간 유지할 때 사용.
  • 아래는 레코드 갯수마다 SLEEP만큼 대기하며, 총 10건의 레코드가 있으면 총 1초를 대기한다.
SELECT SLEEP(0.1)
FROM employees
WHERE emp_no BETWEEN 10001 AND 10010;

벤치마크(BENCHMARK)

  • 개발 과정에서 성능 테스트 용으로 사용. 두 번째 인자의 작업을 첫 번째 인자의 횟수만큼 반복 수행한다. 두 번째 인자는 SELECT 쿼리만 사용 가능하다.
    • SELECT BENCHMARK(1000000, MD5('abcdefghijk'));
    • MD5로 해싱을 1000000번 수행한다.
    • 다만, 실제 어플리케이션의 쿼리 시간과 차이를 가진다. 왜냐하면 해당 작업은 수차례 수행되더라도 네트워크, 테이블 잠금 비용이 단 1회만 소모되기 때문이다.

IP 주소 변환(INET_ATON, INET_NTOA)

  • IP 주소는 4바이트의 부호 없는 정수이다.
  • 123.123.123.123 형태의 VARCHAR(15)보다는 INT나 VARBINARY가 저장 및 검색에 유리하다. 다만 그 값은 사람이 읽을 수 없다. 그러므로 변환을 위하여 INET_ATON, INET_NTOA를 사용한다.
  • IPv6도 지원하며 이 경우 INET6_ATON, INET6_NTOA를 사용한다. IPv4 값을 함께 처리할 수 있다.
create table my_ip(ip_int VARBINARY(16));
insert into my_ip values (inet6_aton('192.168.0.1')), (inet6_aton('2001:0db8:85a3:0000:0000:8a2e:0370:7334'));
select inet6_ntoa(ip_int) from my_ip;

JSON 도큐먼트 다루기

  • JSON_PRETTY: json 칼럼의 값을 읽기 쉽도록 만들어준다.
  • JSON_STORAGE_SIZE: json 칼럼의 길이(byte)를 리턴한다.
  • JSON_EXTRACT: json 도큐먼트의 특정 필드 값을 가져온다.
  • JSON_UNQUOTE: JSON_EXTRACT 결과에 대한 따옴표를 제거한다.
  • ->, `-»’: 람다로 값을 추출할 수 있으며 ->은 JSON_EXTRACT, -»은 JSON_UNQUOTE까지 수행한다.
with employee_docs as (
    select 
        '{"emp_no": 10005, "gender": "M", "salaries": [{"salary": 91453, "to_date": "2001-09-09", "from_date": "2000-09-09"}, {"salary": 94692, "to_date": "9999-01-01", "from_date": "2001-09-09"}], "hire_date": "1989-09-12", "last_name": "Maliniak", "birth_date": "1955-01-21", "first_name": "Kyoichi"}' 
            as doc
    , 10005 as emp_no
    -- union all -- 더 많은 레코드가 있다고 가정하자!
)
SELECT
    JSON_PRETTY(doc)
     , JSON_STORAGE_SIZE(doc)
     , JSON_EXTRACT(doc, '$.first_name')
     , JSON_UNQUOTE(JSON_EXTRACT(doc, '$.first_name'))
     , doc ->'$.first_name'
     , doc ->>'$.first_name'
FROM employee_docs WHERE emp_no=10005;
  • JSON_CONTAINS: json 도큐먼트 내부를 검색한다.
SELECT emp_no FROM employee_docs
WHERE JSON_CONTAINS(doc, '{"first_name" : "Kyoichi"}');

SELECT emp_no FROM employee_docs
WHERE JSON_CONTAINS(doc, '"Kyoichi"', '$.first_name');
  • JSON_OBJECTAGG, JSON_ARRAYAGG: 집합함수의 결과를 JSON 형태로 합친다.
select dept_no
     , group_concat(emp_no) -- 110022,110039....
     , json_arrayagg(emp_no) -- [110022, 110039]
     , json_objectagg(emp_no, from_date) -- {"110022": "1985-01-01", "110039": "1991-10-01"}....
from dept_manager
where dept_no in ('d001', 'd002', 'd003')
group by dept_no;
  • JSON_TABLE: json 데이터의 값을 RDBMS 테이블로 변환하여 반환한다. 임시테이블을 사용하므로 레코드가 많으면 성능 문제가 발생할 수 있다.
  • 아래는 doc의 json 도큐먼트 내부에 있었던 젠더 프로퍼티를 임시테이블의 칼럼으로 승격하여 select 하였다.
select e1.emp_no, e2.gender
from employee_docs e1
    , JSON_TABLE(doc, "$" COLUMNS (gender char(1) path "$.gender")) as e2;

RealMySQL 8.0, 11장을 참고하여 작성하였습니다.