본문 바로가기
홍TV 홍TV

MariaDB “VIRTUAL vs PERSISTENT”, 실제로 성능 차이가 있을까?

홍TV 읽는 시간 약 22분
4.5
(300)

들어가며

앞선 글에서 MariaDB에서 함수 기반 인덱스를 사용하는 방법을 정리했습니다.

MySQL처럼 함수 자체에 바로 인덱스를 걸 수는 없지만,

함수/표현식
    ↓
Generated Column
    ↓
VIRTUAL / PERSISTENT
    ↓
INDEX

이런 구조로 비슷한 효과를 만들 수 있다는 내용이었습니다.

그런데 여기서 한 가지 궁금한 점이 생겼습니다.

VIRTUAL과 PERSISTENT 중 어떤 것을 사용하는 게 더 빠를까?

처음에는 당연히 PERSISTENT가 빠를 것 같았습니다.

PERSISTENT는 계산된 값을 실제로 저장하고 있으니 조회할 때 다시 계산할 필요가 없을 것 같았고, VIRTUAL은 조회할 때마다 값을 만들어야 하니 불리할 것 같았습니다.

그런데 실제로 테스트해보니 이야기가 조금 달랐습니다.

특히 인덱스를 사용하는 경우에는 단순히 “PERSISTENT가 무조건 빠르다”라고 말하기 어려웠습니다.

이번에는 직접 테이블을 만들어서 확인해봤습니다.

먼저 VIRTUAL과 PERSISTENT의 차이부터

테스트에 사용할 컬럼은 간단하게 LOWER() 함수를 적용한 이메일 주소로 잡았습니다.

VIRTUAL은 다음과 같이 만들었습니다.

CREATE TABLE users_virtual (
    id BIGINT NOT NULL AUTO_INCREMENT,
    email VARCHAR(255) NOT NULL,

    lower_email VARCHAR(255)
        AS (LOWER(email)) VIRTUAL,

    PRIMARY KEY (id),
    INDEX idx_lower_email (lower_email)
);

PERSISTENT는 거의 동일합니다.

CREATE TABLE users_persistent (
    id BIGINT NOT NULL AUTO_INCREMENT,
    email VARCHAR(255) NOT NULL,

    lower_email VARCHAR(255)
        AS (LOWER(email)) PERSISTENT,

    PRIMARY KEY (id),
    INDEX idx_lower_email (lower_email)
);

차이는 딱 하나입니다.

VIRTUAL

PERSISTENT

입니다.

MariaDB 공식 문서 기준으로 PERSISTENT는 생성된 값을 실제 테이블에 저장하고, VIRTUAL은 테이블에 값을 저장하지 않고 필요할 때 동적으로 생성합니다. 또한 두 종류 모두 생성 컬럼에 인덱스를 만들 수 있습니다.

테스트 환경은 최대한 단순하게 잡았다

이번 테스트에서 중요한 것은 특정 하드웨어의 절대적인 성능 수치가 아닙니다.

어떤 방식이 무조건 몇 초 빠르다는 결론을 내리기보다는 VIRTUAL과 PERSISTENT가 어떤 작업에서 차이를 만드는지 확인하는 것이 목적이었습니다.

테스트 테이블 구조는 동일하게 구성했습니다.

users_virtual
 ├─ id
 ├─ email
 └─ lower_email VIRTUAL
        └─ idx_lower_email

users_persistent
 ├─ id
 ├─ email
 └─ lower_email PERSISTENT
        └─ idx_lower_email

그리고 다음 세 가지를 중점적으로 비교했습니다.

  1. 대량 INSERT
  2. UPDATE
  3. 인덱스를 사용하는 SELECT

여기에 테이블과 인덱스 크기도 함께 확인했습니다.

1. 먼저 SELECT 성능을 확인했다

가장 먼저 실행해본 쿼리는 이것입니다.

SELECT *
FROM users_virtual
WHERE lower_email = 'test@example.com';

PERSISTENT도 동일하게 실행했습니다.

SELECT *
FROM users_persistent
WHERE lower_email = 'test@example.com';

처음에는 여기서 PERSISTENT가 훨씬 빠를 것이라고 예상했습니다.

그런데 인덱스가 정상적으로 사용되는 상황이라면 생각보다 차이가 크지 않았습니다.

왜 그럴까요?

핵심은 WHERE 조건에서 생성 컬럼의 인덱스를 사용한다는 점입니다.

MariaDB 문서에서도 생성 컬럼에 인덱스가 정의되어 있으면 옵티마이저가 일반 컬럼의 인덱스와 같은 방식으로 고려한다고 설명합니다.

즉,

WHERE lower_email = 'test@example.com'

조건에서 idx_lower_email을 사용한다면 전체 테이블을 처음부터 끝까지 읽으면서 LOWER(email)을 계산하는 구조가 아닙니다.

이미 만들어진 인덱스를 통해 필요한 데이터를 찾아가는 것이 핵심입니다.

그래서 단순한 인덱스 조회에서는

VIRTUAL이라고 해서 무조건 SELECT가 느린 것은 아니었다.

라는 결과를 확인할 수 있었습니다.

EXPLAIN으로 확인해보자

성능 테스트에서 단순히 실행 시간을 보는 것보다 먼저 확인해야 하는 것이 있습니다.

바로 EXPLAIN입니다.

EXPLAIN
SELECT *
FROM users_virtual
WHERE lower_email = 'test@example.com';

PERSISTENT도 동일합니다.

EXPLAIN
SELECT *
FROM users_persistent
WHERE lower_email = 'test@example.com';

여기서 중요한 것은 실제로 다음과 같은 인덱스가 선택되는지입니다.

possible_keys
idx_lower_email

key
idx_lower_email

만약 인덱스가 사용되지 않는다면 VIRTUAL과 PERSISTENT의 성능을 비교하기 전에 왜 인덱스가 선택되지 않았는지부터 확인해야 합니다.

이 부분은 상당히 중요합니다.

2. 그런데 INSERT에서는 이야기가 달라진다

이번에는 데이터를 대량으로 넣어봤습니다.

INSERT INTO users_virtual (email)
VALUES
('TEST@example.com'),
('User01@example.com'),
('User02@example.com'),
('User03@example.com');

PERSISTENT도 같은 데이터를 넣었습니다.

차이는 데이터가 입력되는 순간 발생합니다.

PERSISTENT는 생성된 값을 실제로 저장해야 합니다.

즉,

INSERT
  ↓
email 입력
  ↓
LOWER(email) 계산
  ↓
lower_email 저장
  ↓
인덱스 반영

과정을 거칩니다.

반면 VIRTUAL은 생성된 값을 테이블 데이터 영역에 별도로 저장하지 않습니다.

MariaDB 공식 문서도 PERSISTENT 값은 INSERT/UPDATE 시 생성되어 저장되고, VIRTUAL 값은 테이블에 저장되지 않는다고 설명합니다.

따라서 쓰기 작업이 많은 환경에서는 PERSISTENT가 항상 유리한 것이 아닙니다.

계산 결과를 저장해야 하는 만큼 쓰기 작업에서 추가적인 처리가 필요하기 때문입니다.

3. UPDATE에서는 더 명확해진다

이번에는 원본 이메일을 변경해봤습니다.

UPDATE users_virtual
SET email = 'NEW@example.com'
WHERE id = 100;

PERSISTENT 역시 동일하게 실행합니다.

UPDATE users_persistent
SET email = 'NEW@example.com'
WHERE id = 100;

원본 컬럼인 email이 변경되었기 때문에 LOWER(email)의 결과도 변경됩니다.

그리고 인덱스 역시 변경된 값을 반영해야 합니다.

즉, PERSISTENT든 VIRTUAL이든 인덱스가 붙어 있다면 단순히 “VIRTUAL은 계산을 안 한다”라고 생각하면 안 됩니다.

VIRTUAL은 테이블 본문에 생성값을 저장하지 않는 것이고, 인덱스를 사용한다면 인덱스 자체는 생성 컬럼의 값을 가지고 관리됩니다.

이 부분이 처음 생각했던 것과 달랐습니다.

4. 그래서 VIRTUAL이면 디스크를 전혀 안 쓸까?

이 부분은 상당히 많이 오해하는 부분입니다.

앞선 글에서도 잠깐 이야기했지만,

VIRTUAL = 디스크 사용량 0

이라고 생각하면 안 됩니다.

정확하게는 생성된 컬럼의 값을 테이블 본문에 별도로 저장하지 않는다는 의미입니다.

예를 들어 다음과 같습니다.

lower_email VARCHAR(255)
    AS (LOWER(email)) VIRTUAL

여기에 인덱스를 만들면,

CREATE INDEX idx_lower_email
ON users(lower_email);

인덱스에는 검색을 위해 해당 값이 들어갑니다.

구조를 보면 이해하기 쉽습니다.

VIRTUAL

테이블
┌─────────────────────┐
│ id                  │
│ email               │
│ lower_email         │ ← 별도 저장 X
└─────────────────────┘
          │
          ▼
┌─────────────────────┐
│ idx_lower_email     │
│ 생성값 기반 인덱스   │ ← 인덱스 공간 사용
└─────────────────────┘

따라서 VIRTUAL은 테이블 데이터 영역의 저장 공간을 줄이는 효과가 있지만, 인덱스까지 만들었다면 인덱스 공간은 당연히 사용합니다.

MariaDB 공식 문서에서도 VIRTUAL은 테이블에 저장되지 않지만, VIRTUAL과 PERSISTENT 모두 인덱스를 정의할 수 있다고 명시하고 있습니다.

5. PERSISTENT는 공간을 더 사용한다

PERSISTENT는 조금 다릅니다.

lower_email VARCHAR(255)
    AS (LOWER(email)) PERSISTENT

이 경우 생성된 값 자체가 테이블에 저장됩니다.

그래서 구조가 이렇게 됩니다.

PERSISTENT

테이블
┌─────────────────────┐
│ id                  │
│ email               │
│ lower_email         │ ← 실제 저장
└─────────────────────┘
          │
          ▼
┌─────────────────────┐
│ idx_lower_email     │
│ 인덱스              │
└─────────────────────┘

결과적으로 VIRTUAL보다 테이블 데이터 영역을 더 사용하게 됩니다.

데이터가 수천 건일 때는 별 문제가 아닐 수 있습니다.

하지만 데이터가 수천만 건으로 늘어나면 이야기가 달라집니다.

특히 생성 컬럼의 데이터 타입이 크거나 여러 개의 생성 컬럼을 사용하는 경우에는 저장 공간 차이를 무시하기 어렵습니다.

6. 그렇다면 PERSISTENT가 더 빠른 것 아닌가?

여기서 처음의 질문으로 돌아왔습니다.

“값을 저장해 놓으면 PERSISTENT가 더 빠른 것 아닌가?”

이론적으로는 맞는 부분이 있습니다.

특히 생성 컬럼 자체를 조회하는 경우에는 PERSISTENT는 저장된 값을 바로 읽을 수 있습니다.

SELECT lower_email
FROM users_persistent;

반면 VIRTUAL은 생성 컬럼을 실제로 조회할 때 표현식을 계산해야 합니다.

PERSISTENT

저장된 값
   ↓
조회


VIRTUAL

원본 데이터
   ↓
LOWER(email)
   ↓
조회

MariaDB 공식 문서도 VIRTUAL 컬럼은 해당 컬럼을 조회할 때 동적으로 생성된다고 설명합니다. 반대로 다른 컬럼만 조회하고 VIRTUAL 컬럼을 사용하지 않는다면 해당 값은 생성되지 않습니다.

따라서 생성 컬럼 자체를 대량으로 읽는 작업에서는 PERSISTENT가 유리할 가능성이 있습니다.

7. 그런데 인덱스 조회에서는 이야기가 조금 다르다

이번 테스트에서 가장 중요하게 느낀 부분입니다.

다음과 같은 조회가 있다고 해보겠습니다.

SELECT id, email
FROM users_virtual
WHERE lower_email = 'test@example.com';

여기서는 lower_email을 결과로 출력하는 것이 아니라 검색 조건으로 사용하고 있습니다.

그리고 해당 컬럼에 인덱스가 있습니다.

INDEX idx_lower_email (lower_email)

이 경우 데이터베이스가 전체 행을 읽고 LOWER(email)을 계산하는 것이 아니라 인덱스를 이용해 대상 행을 찾아갈 수 있습니다.

따라서 VIRTUAL이라고 해서 무조건 큰 성능 손실이 발생한다고 단정하기 어렵습니다.

특히 최신 MariaDB에서는 인덱스가 정의된 VIRTUAL 컬럼의 표현식을 WHERE 조건에서 인식해 range 또는 ref 접근에 활용할 수 있도록 옵티마이저가 개선됐습니다.

그래서 실제 운영 환경에서는 단순히

PERSISTENT가 저장되어 있으니까 무조건 빠르겠지.

라고 결정하기보다는 실제 쿼리와 실행 계획을 확인하는 것이 맞습니다.

8. 실제 테스트에서 더 중요했던 것은 이것이었다

테스트하면서 VIRTUAL과 PERSISTENT의 차이보다 더 중요하다고 느낀 것이 있습니다.

바로 어떤 쿼리를 실행하느냐였습니다.

예를 들어 다음 두 쿼리는 완전히 다른 성격을 가지고 있습니다.

인덱스 검색

SELECT id, email
FROM users
WHERE lower_email = 'test@example.com';

생성 컬럼 전체 조회

SELECT lower_email
FROM users;

첫 번째는 인덱스 활용 여부가 중요합니다.

두 번째는 VIRTUAL 컬럼의 계산 비용이 직접적으로 영향을 줄 수 있습니다.

그래서 테스트 결과를 단순하게

VIRTUAL = 빠름
PERSISTENT = 느림

또는

VIRTUAL = 느림
PERSISTENT = 빠름

으로 정리하는 것은 위험합니다.

9. 실제 개발에서는 어떻게 선택할까?

제가 이 테스트를 해보고 내린 기준은 상당히 단순했습니다.

조회 조건으로만 사용한다면

WHERE lower_email = ?

처럼 검색 조건에서 주로 사용하고,

INDEX idx_lower_email (lower_email)

을 제대로 활용한다면 VIRTUAL부터 검토해볼 만합니다.

테이블에 생성값을 중복 저장하지 않아도 되기 때문입니다.

생성값 자체를 자주 조회한다면

예를 들어,

SELECT
    id,
    email,
    lower_email
FROM users;

처럼 생성 컬럼 자체를 자주 읽는다면 PERSISTENT를 고려할 수 있습니다.

이미 계산된 값을 저장하고 있기 때문에 매번 표현식을 다시 계산할 필요가 없습니다.

데이터가 매우 크다면

수백만, 수천만 건 이상으로 데이터가 커진다면 저장 공간도 중요한 요소가 됩니다.

특히 다음과 같은 상황이라면 VIRTUAL의 장점이 커질 수 있습니다.

원본 컬럼
+
생성 컬럼 여러 개
+
각 생성 컬럼의 인덱스

이렇게 생성 컬럼이 많아질수록 PERSISTENT는 테이블 데이터 영역까지 추가로 차지합니다.

10. 그래서 테스트할 때는 실행 시간만 보면 안 된다

이번 테스트에서 가장 중요하게 생각했던 부분입니다.

성능 테스트를 한다고 해서 단순히

VIRTUAL : 1.21초
PERSISTENT : 1.08초

이런 숫자 하나만 비교하면 안 됩니다.

최소한 다음 항목을 같이 봐야 합니다.

① INSERT 성능
② UPDATE 성능
③ SELECT 성능
④ EXPLAIN 실행 계획
⑤ 테이블 크기
⑥ 인덱스 크기
⑦ 데이터 분포
⑧ 캐시 영향

특히 SELECT 테스트는 같은 쿼리를 여러 번 실행하면 버퍼 풀이나 OS 캐시의 영향을 받을 수 있습니다.

따라서 한 번 실행한 결과만 가지고

“PERSISTENT가 0.2초 빠르니까 PERSISTENT가 더 좋다.”

라고 결론 내리는 것은 의미가 없습니다.

11. 개인적으로 가장 현실적인 선택 기준

실무에서 둘 중 하나를 골라야 한다면 저는 다음 순서로 판단하는 편이 좋다고 봅니다.

① 생성 컬럼을 검색용으로 사용하는가?

WHERE generated_column = ?

그렇다면 우선 VIRTUAL + INDEX를 검토합니다.

② 생성 컬럼 자체를 자주 조회하는가?

그렇다면 PERSISTENT를 검토합니다.

SELECT generated_column
FROM ...

이런 조회가 매우 빈번하다면 저장된 값을 사용하는 것이 유리할 수 있습니다.

③ 생성 표현식이 복잡한가?

예를 들어 단순한

LOWER(email)

정도가 아니라 여러 함수를 조합한 복잡한 계산이라면 이야기가 달라질 수 있습니다.

이 경우에는 VIRTUAL의 계산 비용을 실제 데이터 규모에서 측정해보는 것이 좋습니다.

④ 저장 공간이 중요한가?

대규모 테이블이라면 PERSISTENT가 차지하는 추가 공간도 반드시 계산해야 합니다.

12. 결국 정답은 EXPLAIN + 실제 테스트

이번에 테스트하면서 느낀 결론은 의외로 간단했습니다.

VIRTUAL과 PERSISTENT 중 무조건 하나가 더 빠른 것은 아니었습니다.

둘의 목적이 조금 다르기 때문입니다.

VIRTUAL

테이블 저장공간 절약
        ↓
필요할 때 계산
        ↓
인덱스 사용 가능

반면,

PERSISTENT

계산 결과 저장
        ↓
조회 시 계산 부담 감소
        ↓
대신 저장공간 증가

그리고 두 방식 모두 인덱스를 사용할 수 있습니다.

따라서 실제로 중요한 것은 “VIRTUAL이냐 PERSISTENT냐” 자체가 아니라 어떤 데이터를 어떻게 조회하느냐였습니다.

마무리

처음에는 저도 단순하게 생각했습니다.

“계산 결과를 저장하는 PERSISTENT가 당연히 빠르겠지.”

그런데 실제로 테스트해보니 그렇게 단순하지 않았습니다.

특히 생성 컬럼을 검색 조건으로 사용하고 인덱스를 걸어놓은 경우에는 VIRTUAL도 충분히 좋은 선택이 될 수 있었습니다.

반대로 생성 컬럼 자체를 대량으로 조회하거나 계산 비용이 큰 표현식을 반복해서 사용한다면 PERSISTENT가 더 적합할 수 있습니다.

결국 다음처럼 생각하는 것이 가장 편했습니다.

                    VIRTUAL              PERSISTENT
                      │                      │
테이블 저장           ❌                     ✅
계산 결과 저장        ❌                     ✅
인덱스 사용           ✅                     ✅
저장공간              적음                   많음
계산 부담             조회 시 발생            쓰기 시 저장

그래서 제가 정리한 선택 기준은 이렇습니다.

검색용 생성 컬럼 → VIRTUAL + INDEX부터 검토

계산 결과 자체를 자주 읽음 → PERSISTENT 검토

데이터가 크거나 생성 컬럼이 많음 → 저장공간까지 비교

그리고 무엇보다 중요한 것은 EXPLAIN으로 실제 실행 계획을 확인하는 것입니다.

MariaDB에서도 생성 컬럼 인덱스는 일반 컬럼 인덱스처럼 옵티마이저가 고려할 수 있고, 최신 버전에서는 indexed virtual column expression을 WHERE 조건에서 인식하는 기능도 개선되어 있습니다.

결국 데이터베이스 성능은 문서에 적힌 “이론상 빠른 방식”보다 내가 실제로 사용하는 데이터와 쿼리에서 어떻게 동작하는지 확인하는 것이 가장 정확했습니다.

다음 테스트에서는 이것도 해볼 만하다

이번 테스트에서는 LOWER(email)처럼 비교적 단순한 표현식을 사용했습니다.

다음에는 조금 더 실무적인 환경으로 바꿔볼 수 있습니다.

JSON_VALUE()
DATE_FORMAT()
CONCAT()
CASE WHEN

같은 표현식을 생성 컬럼으로 만들고,

100만 건 / 500만 건 / 1,000만 건

정도로 데이터를 늘려서 VIRTUAL과 PERSISTENT의

  • INSERT 시간
  • UPDATE 시간
  • SELECT 시간
  • EXPLAIN
  • 테이블 용량
  • 인덱스 용량

을 실제 수치로 비교해보면 훨씬 재미있는 결과가 나옵니다.

특히 MariaDB에서 JSON 컬럼을 VIRTUAL/PERSISTENT로 인덱싱하는 경우는 실무에서도 활용도가 높아서 별도의 성능 테스트 주제로 만들기 좋습니다. MariaDB 공식 자료에서도 JSON 값에 기반한 VIRTUAL 컬럼을 만들고 인덱스를 사용하는 방식을 소개하고 있습니다.

이 게시물이 얼마나 유용했나요?

별점을 클릭하여 평가하세요!

평균 평점 4.5 / 5. 투표 수: 300

아직 투표가 없습니다! 첫 번째로 평가해보세요.

홍TV

홍TV
함께 보면 좋은 글

댓글 0

첫 댓글을 남겨보세요.

error: Content is protected !!

광고 차단 알림

광고 클릭 제한을 초과하여 광고가 차단되었습니다.

단시간에 반복적인 광고 클릭은 시스템에 의해 감지되며, IP가 수집되어 사이트 관리자가 확인 가능합니다.