나는 쿼리에서 DISTINCT가 쓰인 걸 보면 작성자가 데이터 모델이나 집합 이론을 제대로 이해하지 못했거나, 둘 다 모를 수도 있음을 의심함
때론 DISTINCT는 지나치게 정규화되지 않은 스키마의 신호일 수도 있음. 예를 들어, 중복된 도시명이 기록되는 걸 막고자 굳이 addresses_cities 같은 테이블을 만들 필요가 없다고 생각함
내 경험도 거의 비슷함. 하지만 최근에는 모든 조인이 올바르게 되어 있어도 CTE 안에 DISTINCT를 넣었더니 성능이 크게 좋아진 경우가 있었음. 레코드의 유일성이 보장되면 쿼리 플래너가 최적화를 하는 것 같음
내가 쿼리에서 결과가 최대 한 개만 나올 거라 예상해서 LIMIT 1을 추가했더니 비슷하게 좋지 않다는 피드백을 들었음. 하지만 큰 테이블에서 (sqlite, mysql, postgresql 모두) 원하는 레코드를 찾고도 DB는 전체 테이블을 계속 탐색하는 경향이 있음
SELECT x FROM t 쿼리에서 DISTINCT를 안전하게 생략해도 되는지 어떻게 알 수 있을지 질문함. t의 스키마에서 x가 PRIMARY나 UNIQUE 제약을 갖는 걸 봤다 해도, 금방 누군가 UNIQUE 제약을 없앨 수도 있음. 그럼 중복이 생겨버려서 왜 그런지 고민하게 됨. SQL은 집합(set) 언어가 아니라 bag 언어임. 런타임에서 rel t와 속성 x를 찾으면 그냥 반환할 뿐임. 중복이 생길 수도, 타입이 바뀔 수도 있음. 만약 Set을 원하면 반드시 DISTINCT를 명시해야 함. 쿼리 플래너가 런타임에 UNIQUE나 PRIMARY면 deduplication을 안하게 됨
Cypher에서는 오히려 반대임. neo4j로 복잡한 데이터를 다룰 때, 중복 노드가 정말 쉽게 결과에 포함되는데 DISTINCT가 필수적임. 특히 가변 길이 관계를 사용할 때 DISTINCT가 없으면 느려지고 중복이 많아짐
자주 언급되지 않은 것 중 하나가 바로 "존재하지 않는 것"을 찾는 쿼리임. 예를 들어 != 나 NOT IN (...)을 사용하는 경우가 거의 대부분 비효율적임 (다만 다른 조건에서 결과 집합이 충분히 좁아졌을 때는 괜찮을 수 있음). 그리고 DB에서 null 처리를 어떻게 하는지 이해하는 게 중요함. null과 빈 문자열이 같은 건지, null == null인지, DB마다 다를 수 있음
null 처리와 인덱싱 관련해서, 내가 사용한 DB들은 null 값을 인덱스하지 않으므로 "WHERE col IS NULL"의 쿼리는 col 인덱스가 있어도 비효율적으로 동작함. 정말 필요하다면 col의 null 여부를 표시하는 char(1) 또는 bit 컬럼을 생성해서 그 필드를 인덱싱하는 방법을 추천함
!= 나 NOT IN (...)이 거의 항상 비효율적이라고 했는데, 왜 그런지 궁금함. 우측값이 상수인 경우에는 해시 테이블 룩업이 되어 일반적으로 효율적일텐데, 더 효율적인 대안이 있는지 궁금함
제시된 '안티패턴'들이 모두 진짜 안티패턴은 아니라고 봄. 쿼리 조건이 인덱스와 맞지 않는 문제는 결국 인덱스 원리 이해 부족에서 비롯됨. 그리고 여기 언급된 문제 중 상당수는 SQL 그 자체보다는 데이터베이스 스키마 설계와 더 밀접한 문제임. DISTINCT가 필요하다면 프라이머리 키 설계가 올바르지 않은 것일 수 있음. view를 너무 쌓게 되는 건 결국 기본 테이블 설계가 잘못된 것임. 좋은 DB 모델링이 모든 문제를 미리 막음
이런 '안티패턴'들은 사실 SQL 언어 설계의 한계(혹은 미설계)로 인해 발생하는 단순한 우회책에 불과함. 나는 SQL 데이터베이스에서 동작하는 새로운 언어를 만들고 있어서, 이런 문제마다 좀 더 나은 대안을 만들고 싶음. 아직 미완성이며 문서화도 부족한데, 관심 있으면 https://lutra-lang.org에서 피드백 받고 싶음
"SQL 데이터베이스"라는 표현이 모호함. SQL은 관계형뿐 아니라 비관계형 DB에도 구현된 사례가 있음. 전문가들은 이미 SQL의 문제를 오래전부터 인식하고 있었고, Chris Date와 Hugh Darwen의 Tutorial D 등 대체안도 있었음. 그럼에도 수십 년간 쌓인 SQL 코드와 도구 때문에 대체제가 자리잡지 못함. 나는 SQL 덕분에 수십년간 고용 안정과 꾸준한 수익을 얻었으니, 더 좋은 언어가 필요하긴 하지만 한편으론 이 상황을 긍정적으로 봄
많은 CASE WHEN문 남용 문제는 UDF(User Defined Function)로 로직을 한 곳에 통합하면 해소할 수 있음
인덱스 컬럼에 함수를 사용하는 건 쿼리가 sargable하지 않다는 신호임
DISTINCT 남용 대신, join에서 파생된 팬아웃 중에서 테이블 그레인에 맞게 de-dupe하는 쿼리로는
ROW_NUMBER() OVER (PARTITION BY <grain> ORDER BY <deterministic sort>) = 1
sargable하지 않은 문제는 expression index로 쉽게 해결할 수 있음. 적어도 sqlite에선 그렇다고 생각함
view를 중첩해서 쓰는 게 오히려 필요한 상황도 있음. 우리 POS 소프트웨어에서는 트랜잭션을 한 번에 깔끔하게 볼 수 있는 백본 view를 만드는 데 중첩 view를 많이 씀. 그렇지 않으면 각각의 테이블별로 where 절을 다르게 짜고, void/반품/취소 등 각종 조건을 매번 처리해야 해서 변경 사항이 있으면 수십 개 view/procedure를 다 고쳐야 함. 우리 케이스엔 중첩 view 활용이 훨씬 실용적임
인덱스 컬럼에 함수를 사용하는 문제는 명확하게 설명이 더 필요함. 인덱스가 걸린 컬럼에 함수가 들어가면, 인덱스 효율성이 사라지고 실제로는 풀스캔이 발생해서 느려짐. 이걸 직접 경험하고 체득했음
Hacker News 의견
https://kb.databasedesignbook.com/posts/systematic-design-of-join-queries/
https://en.wikipedia.org/wiki/Sargable
https://www.brentozar.com/blitzcache/non-sargable-predicates/
"sargable"이라는 단어는 "Search ARGument ABLE"의 포터맨토(합성어)임
인덱스 컬럼에 함수를 사용하는 건 쿼리가 sargable하지 않다는 신호임
DISTINCT 남용 대신, join에서 파생된 팬아웃 중에서 테이블 그레인에 맞게 de-dupe하는 쿼리로는 같은 식이 유용함. 일부 DB엔 QUALIFY라는 기능이 지원되어 쿼리가 훨씬 깔끔해짐
sargable 설명
QUALIFY in Redshift