MariaDB / Query / DISTINCT / 중복 데이터 제거

MariaDB의 SELECT DISTINCT는 조회 결과에서 선택한 열 값의 조합이 같은 행을 한 번만 보여 줍니다. 테이블에 저장된 중복 행을 실제로 삭제하는 명령은 아닙니다. 한 열만 고르면 그 값의 중복을, 여러 열을 고르면 모든 선택 열의 조합에 대한 중복을 제거합니다.

실습 데이터와 DISTINCT의 기본 위치

MariaDB에 SQL을 실행할 수 있는 클라이언트로 접속해 실습용 learning 데이터베이스를 선택합니다. 아래는 CREATE·INSERT·SELECT 권한이 필요하며, distinct_demo라는 테이블이 이미 있으면 기존 것을 지우지 말고 다른 실습 이름을 사용합니다. 데이터 준비 뒤의 DISTINCT 예제는 모두 조회만 수행합니다.

USE learning;
CREATE TABLE distinct_demo (
    id INT PRIMARY KEY,
    category VARCHAR(20) NULL,
    batch_no INT NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO distinct_demo (id, category, batch_no) VALUES
    (1,'A',1), (2,'B',1), (3,'C',2), (4,'D',2),
    (5,'A',1), (6,'B',2), (7,'C',1), (8,'D',2),
    (9,NULL,1), (10,NULL,1);
SELECT id, category, batch_no FROM distinct_demo ORDER BY id;

SELECT 문법에서 DISTINCT는 SELECT 다음의 선택 옵션입니다. 각 열에 따로 붙이는 함수가 아니므로 SELECT category, DISTINCT batch_no처럼 작성하지 않습니다. 위 데이터는 기본 키 id가 서로 다른 10행이며, category·batch_no 기준에서는 같은 값이 반복됩니다.

한 열과 여러 열의 중복 기준 비교

SELECT DISTINCT category
FROM distinct_demo ORDER BY category;

SELECT DISTINCT category, batch_no
FROM distinct_demo ORDER BY category, batch_no;

첫 결과는 NULL·A·B·C·D의 다섯 행입니다. 둘째 결과는 (NULL,1), (A,1), (B,1), (B,2), (C,1), (C,2), (D,2)의 일곱 조합입니다. B는 batch_no가 1인 행과 2인 행이 달라 두 번 나타납니다. DISTINCT가 첫 번째 열만 보고 나머지 열에서 임의의 값을 선택한 것이 아닙니다.

SELECT DISTINCT id, category, batch_no처럼 고유한 id까지 포함하면 열 조합이 모두 달라 10행이 그대로 나옵니다. SELECT DISTINCT *도 모든 선택 열이 같은 행만 합치므로, 고유 키가 있는 테이블에서 “같은 category끼리 한 행”이라는 목적을 달성하지 못할 수 있습니다. 어떤 기준으로 같다고 볼지 정한 뒤 필요한 열만 선택해야 합니다.

NULL과 고유한 값 개수 세기

NULL 값 설명에 따르면 DISTINCT의 중복 판정에서는 여러 NULL을 같은 그룹으로 취급합니다. 따라서 이 예제의 NULL 두 개는 한 행으로 표시됩니다. 이것은 일반 비교식 NULL=NULL이 true가 된다는 뜻은 아니며, NULL 행을 찾을 때는 IS NULL을 사용합니다.

SELECT COUNT(*) AS all_rows,
       COUNT(category) AS non_null_rows,
       COUNT(DISTINCT category) AS distinct_non_null_values,
       COUNT(DISTINCT category, batch_no) AS distinct_non_null_pairs
FROM distinct_demo;

SELECT COUNT(*) AS distinct_including_null
FROM (SELECT DISTINCT category FROM distinct_demo) AS values_once;

첫 결과는 10·8·4·6, 둘째 결과는 5입니다. COUNT(DISTINCT …)는 NULL을 제외한 고유 값을 셉니다. 여러 식을 넣으면 그중 NULL이 포함된 조합은 제외되므로, SELECT DISTINCT가 보여 주는 일곱 조합과 COUNT(DISTINCT category,batch_no)의 6은 모순이 아닙니다. NULL도 하나의 표시 항목으로 세려면 DISTINCT 결과를 파생 테이블로 감싸 COUNT(*)를 적용할 수 있습니다.

조건·정렬·LIMIT 함께 사용

SELECT DISTINCT category
FROM distinct_demo
WHERE batch_no = 1
ORDER BY category DESC;

SELECT DISTINCT category
FROM distinct_demo
WHERE category IS NOT NULL
ORDER BY category
LIMIT 2;

첫 조회는 batch_no=1인 행을 대상으로 하므로 C·B·A·NULL이 나옵니다. 둘째 조회는 NULL을 제외한 고유 category를 오름차순으로 정렬한 뒤 A·B 두 행을 반환합니다. DISTINCT 자체는 원하는 표시 순서를 약속하지 않으므로 순서가 필요하면 ORDER BY를 명시합니다. LIMIT 2는 원본에서 두 행을 먼저 고른 뒤 중복을 없애라는 뜻이 아닙니다.

선택하지 않은 id로 DISTINCT category를 정렬해 각 category의 최신 행을 고르려는 방식도 피해야 합니다. 중복 그룹 중 어떤 id를 대표로 쓸지 그 구문만으로 정의되지 않습니다. 최신 행 선택은 날짜·고유 키·동률 처리 기준을 정하고 윈도 함수나 집계 후 연결 같은 별도 쿼리로 해결할 주제입니다.

GROUP BY·문자열 비교와 혼동하기 쉬운 점

고유한 조합의 목록만 필요하면 DISTINCT가 목적을 직접 드러냅니다. 그룹별 건수·합계나 반복되는 그룹만 찾으려면 GROUP BY와 HAVING을 사용합니다. 다음 예제는 중복 여부와 관계없이 모든 category의 행 수를 세는 것이며 중복 행을 삭제하지 않습니다.

SELECT category, COUNT(*) AS row_count
FROM distinct_demo
GROUP BY category
ORDER BY category;

NULL·A·B·C·D 그룹의 row_count는 각각 2입니다. 여기에서 HAVING COUNT(*)>1을 추가하면 여러 번 나오는 그룹만 남길 수 있지만, 각 그룹의 원본 행 전체나 삭제 대상을 자동으로 결정하는 것은 아닙니다. “고유 목록 조회”, “중복된 값과 개수 확인”, “실제 중복 행 삭제”를 서로 다른 작업으로 구분하세요.

문자열의 같은 값 판정에는 열의 문자셋·정렬 규칙도 영향을 줍니다. 예를 들어 아래처럼 대소문자를 구분하지 않는 utf8mb4_unicode_ci를 명시한 별도 실습 테이블에서는 a와 A를 같은 비교 값으로 볼 수 있습니다. 바이트 단위의 차이를 확인할 때는 BINARY 표현식과 비교할 수 있으나, 실제 요구에 맞는 비교 규칙인지 먼저 판단해야 합니다.

CREATE TABLE distinct_case_demo (
    value_text VARCHAR(20) CHARACTER SET utf8mb4
        COLLATE utf8mb4_unicode_ci
);
INSERT INTO distinct_case_demo VALUES ('a'), ('A');
SELECT COUNT(DISTINCT value_text) AS case_insensitive_count,
       COUNT(DISTINCT BINARY value_text) AS byte_distinct_count
FROM distinct_case_demo;

이 조건의 결과는 1과 2입니다. 대소문자·악센트·끝 공백 등의 규칙은 정렬 규칙과 자료형에 따라 달라질 수 있으므로 보이는 문자열만 보고 중복 판정이 잘못됐다고 단정하지 않습니다. 결과에 대표 문자열 한 가지가 보인다고 그 대소문자 표기가 항상 선택된다고 의존하지도 마세요.

데이터가 많다면 DISTINCT가 불필요한 열·행에 적용되지 않도록 조회 범위를 정하고 실행 계획을 확인합니다. 인덱스나 임시 처리 사용 여부는 쿼리·자료형·서버 버전에 따라 달라지므로 DISTINCT를 붙이면 언제나 빠르거나 느리다고 단정하지 않습니다. JOIN 때문에 뜻하지 않은 행 증식이 생긴 경우에는 마지막에 DISTINCT로 가리기 전에 연결 조건과 관계를 먼저 검토합니다.