SQL 기본과 쿼리 작성 원칙
3줄 요약
- SQL은 데이터를 가져오는 문자열이 아니라 DB에게 원하는 결과 집합을 선언하고, DB가 실행 계획을 선택하게 만드는 언어다.
- 백엔드 개발자는
SELECT, JOIN, GROUP BY, ORDER BY, INSERT, UPDATE, DELETE를 문법보다 실행 비용과 잠금 범위 관점으로 읽어야 한다.
- 좋은 SQL은 결과가 맞고, 인덱스를 활용할 수 있고, 동시성에서 안전하며, 운영자가 실행 계획, 영향 row 수, 잠금 범위, 실패 시 복구 방법을 설명할 수 있는 쿼리다.
핵심 정리
SELECT는 필요한 컬럼과 필요한 행만 가져오는 것이 기본이다. SELECT *는 네트워크 전송량, 메모리, ORM 매핑 비용을 늘리고 스키마 변경에 약하다.
WHERE는 필터이면서 인덱스 활용 가능성을 결정한다. 컬럼에 함수를 씌우거나 타입을 섞으면 인덱스를 못 쓰는 경우가 생긴다.
JOIN은 관계를 합치는 강력한 도구지만, 조인 순서와 조인 컬럼의 인덱스에 따라 비용이 크게 달라진다.
GROUP BY, ORDER BY, DISTINCT는 정렬, 해시, 임시 테이블 비용을 만들 수 있다. 목록 API에서 무심코 붙인 정렬이 병목이 되는 일이 많다.
UPDATE와 DELETE는 읽기보다 더 위험하다. 잘못된 조건은 대량 변경을 만들고, 넓은 범위 스캔은 락과 복구 로그 부담을 만든다.
- 운영 쿼리는 항상 영향 범위를 먼저 확인한다.
SELECT count(*), 샘플 조회, 실행 계획, 트랜잭션 범위, 백업/PITR, 롤백 또는 forward fix, 배치 크기를 갖춘 뒤 실행한다.
count(*)도 큰 테이블에서는 비용이 될 수 있다. 운영에서는 count 자체의 실행 계획과 시간, replica 사용 가능성, 샘플링 가능성을 함께 본다.
- SQL 리뷰에서는 literal 값보다 bind 값 패턴과 실제 호출 빈도를 본다. 같은 SQL이라도
member_id 단건 조회와 전체 관리자 검색은 운영 비용이 전혀 다르다.
헷갈리는 지점
- SQL은 결과만 같으면 된다고 생각하기 쉽다. 작은 데이터에서는 어떤 쿼리든 빠르게 보이기 때문이다.
- 핵심은 같은 결과라도 실행 계획과 잠금 범위가 다를 수 있다는 점이다.
- 운영 데이터에서는 비용 차이가 API 지연과 장애로 이어진다.
- ORM 메서드 이름이 짧으면 SQL도 가볍다고 오해하기 쉽다. 코드에서 SQL이 직접 보이지 않기 때문이다.
- 핵심은 ORM도 결국 SQL을 생성해 DB에 보낸다는 점이다.
- 로그로 실제 SQL과 바인딩 값, 실행 계획을 확인해야 한다.
UPDATE 전에 SELECT로 확인했으니 안전하다고 생각하기 쉽다. 확인과 실행 사이에 데이터가 바뀔 수 있기 때문이다.
- 핵심은 같은 조건을 트랜잭션 안에서 재확인하거나, 변경 조건 자체를 안전하게 작성하는 것이다.
- 동시성 조건이 있으면 락, 버전 컬럼, Unique 제약, 원자적 업데이트를 검토해야 한다.
LIMIT이 있으면 대량 쿼리도 안전하다고 생각하기 쉽다. 정렬이나 필터가 인덱스를 못 타면 DB는 limit 개수보다 훨씬 많은 row를 읽을 수 있기 때문이다.
- 핵심은
ORDER BY, WHERE, LIMIT이 같은 인덱스 전략 안에서 동작하는지 실행 계획으로 확인하는 것이다.
- 목록 API는 응답 건수보다 “앞 20개를 찾기 위해 몇 row를 읽는가”가 더 중요하다.
확인 질문
- 좋은 SQL을 판단할 때 결과 외에 무엇을 봐야 하는가?
- 실행 계획, 인덱스 사용 여부, 읽는 행 수, 정렬과 임시 작업, 잠금 범위, 네트워크 전송량, 장애 시 롤백 가능성을 봐야 한다.
SELECT *가 실무에서 위험한 이유는 무엇인가?
- 불필요한 컬럼 전송과 매핑 비용을 만들고, 커버링 인덱스 가능성을 줄이며, 스키마 변경 시 의도하지 않은 데이터를 노출할 수 있기 때문이다.
- 운영에서
UPDATE나 DELETE를 실행하기 전 확인할 것은 무엇인가?
- 조건이 정확한지, 예상 대상 건수가 맞는지, 실행 계획이 안전한지, 트랜잭션과 롤백 전략이 있는지, 배치 실행이 필요한지 확인해야 한다.
LIMIT 20이 있는 목록 API도 느려질 수 있는 이유는 무엇인가?
- 조건과 정렬을 지원하는 인덱스가 없으면 DB가 20건을 반환하기 위해 많은 row를 읽고 정렬해야 할 수 있기 때문이다.