1. 생성
(1) DB 생성시
- DB명칭은 해당 서비스를 파악할 수 있도록 명명한다.
(2) USER 생성시
- USER ID는 유관 서비스를 파악할 수 있도록 명명한다.
- Password는 운용팀 DBA의 생성규칙을 따른다.
(3) TABLE 생성시
- (table 길이 + column 길이)가 8K를 넘지 않도록 한다.
- table 및 column 이름은 일관성 있게 준다. (EX. TBL_OOO, TN_OOO)
- PK / FK column은 고정길이 형식을 사용한다. (EX. CHAR TYPE)
- Trigger 사용은 자제한다.
- table 소유자는 항상 DBO가 되도록 한다.
(4) DATA TYPE 정의시
- 컬럼에 필요한 데이터를 저장할 수 있는 데이터타입 중 항상 가장 작은 데이터타입을 선택한다.
- 컬럼에 저장되는 텍스트 데이터의 길이가 매우 가변적이라면 VARCHAR 타입을 사용하는것이 좋다.
- 16비트문자 데이터를 저장할 계획이 아니라면 NVARCHAR, NCHAR 데이터 타입을 사용하지 않는 것이 좋다.
- 긴 문자열 저장할 때, 문자열 길이가 8000자 이하라면 TEXT대신 VARCHAR 데이터타입을 사용하는 것이 좋다.
- 숫자만을 저장하는 컬럼은 VARCHAR, CHAR 대신 INTEGER와 같은 숫자데이터 타입을 사용하는 것이 좋다.
(5) 인덱스 생성시
- WHERE절에서 많이 사용하는 경우 생성한다.
- Covered Index인경우 선택도가 좋은 조건(10%이하)부터 순서대로 생성한다.
- 구간별 선택이 많은 컬럼인 경우 클러스터 인덱스 추천
- PK정의시 non-Cluster index로 정의하되 구간별 선택이 많은 컬럼인 경우 클러스터 인덱스로 생성한다.
2. Query 작성시
(1) 테이블의 모든 컬럼이 아닌 필요 컬럼의 레코드만 반환한다.
- select * 를 사용하는 것은 피한다.
- 사용하지않는 데이터를 호출하는 것만으로도 많은 부하가 걸린다.
- 특히 text 타입 데이터 호출시에 그 정도가 심해진다.
- data type의 byte가 적은 column을 주로 사용하는 것이 좋다.
-- 잘못된 예
SELECT * FROM tbl_member WEHRE f_idx = 101
--올바른 예
SELECT f_idx, f_Name, f_nickname, f_age, f_gender
FROM tbl_member
WHERE f_idx = 101
(2) 테이블 전체 Row 수를 알고싶다면 sys.sysindexes table의 rows 컬럼을 이용한다.
- count(특정 column)으로 호출하는 경우가 있다. 이 경우 해당 컬럼의 null 값을 제외한 count를 가져오게 된다.
- null값을 일일이 체크하면 호출 속도가 저하된다. null을 체크하는 경우가 아닌 대부분 경우 count(*)를 사용한다.
- count(*)는 null값의 경우도 모두 count에 추가하여 계산하므로 그로 인한 성능의 저하가 많이 개선된다.
- 그러나 SELECT count(*)의 경우도 테이블을 스캔해서 전체 Row 수를 반환하기 때문에 큰 테이블에서는 시간이 오래 걸린다. 이 경우에는 sysindexes 시스템 테이블을 사용한다. 이 테이블의 컬럼은 각 테이블의 총 Row 수를 값으로 가지고 있다.
-- 잘못된 예
SELECT count(f_nickname) FROM tbl_member
SELECT count(*) FROM tbl_member
-- 올바른 예
SELECT f_nickname FROM sys.sysindexes WHERE id OBJECT_ID('tbl_member') AND indid < 2;
(3) 단순 SELECT면 WITH NOLOCK 옵션을 사용한다.
-- 잘못된 예
SELECT f_nickname FROM tbl_member
-- 올바른 예
SELECT f_nickname FROM tbl_member WITH(NOLOCK)
(4) WHERE절을 사용하여 쿼리 결과셋을 제한한다.
- 성능에 가장 영향을 미치는 것으로 클라이언트에 모든 결과가 아니라 꼭 필요한 결과만 반환하도록 한다. 이렇게 하면 쓸모없는 네트워크 트래픽을 감소시킬 수 있으며 쿼리 성능 또한 향상된다.
(5) WHERE 조건문의 왼쪽은 되도록 변형되지않은 순수한 column만을 선언한다.
- WHERE name + " = 조건 " 과 같이 좌항은 변형하지 않고 우항에 조건을 선언한다.
- 조건 일치를 매 Row마다 확인할 떄 좌항을 변형하게 되며 그만큼 부하도 눈에 띄게 증가한다.
(6) WHERE 조건이 최적인지 확인한다.
- WHERE 조건이나 table join에 쓰이는 컬럼이 인덱스로 걸려있도록 해야 join 성능이나 필터링이 좋다.
- 여러 필터링 중 예상되는 결과 레코드 수가 적은 것부터 WHERE조건절에 사용한다.(SQL server 2000부터는 SQL 실행계획이 자동으로 수정하여 실행해줌)
- 여러번의 쿼리를 통해 임시 테이블을 만들고, 조작해야할 때는 SQL 작성자가 유의해서 작성한다.
(7) 가능한 HAVING절의 사용을 피한다.
- HAVING절은 GROUP BY에 의한 결과를 제한할 때 사용한다. GROUP BY에 HAVING절을 사용하였을 경우 GROUP BY에 의해서 결과들을 모두 집계한 다음 HAVING절에 명시한 조건으로 맞지 않는 결과를 버리게 된다. 대부분의 경우 HAVING절의 필요없이 GROUP BY와 WHERE절 만으로 원하는 결과를 얻을 수 있다.
(8) 가능한 DISTINCT문의 사용을 피한다.
- SORT에 따른 성능 하락이 있기 때문에 꼭 필요한 경우에만 사용한다.
(9) 특정 레코드 존재 유무를 파악할때 COUNT를 세지말고 EXISTS를 사용한다.
- COUNT는 모든 레코드 중 관련된 것을 필터링한 후 COUNT 함수를 수행하지만, EXISTS는 필터링 시 하나라도 레코드가 있음을 인지했을 때 반환한다. 테이블의 전체 레코드 수가 적을 때는 구별이 안되지만 많을 때는 EXISTS가 효과적이다.
-- 잘못된 예
SET @v_count = (SELECT count(*) FROM tbl_member WHERE f_age = 20)
IF @v_count > 0
BEGIN
END
-- 올바른 예
IF EXISTS(SELECT f_idx FROM tbl_member WHERE f_age = 20)
BEGIN
END
(10) 처음 몇개의 Row만 필요하다면 TOP 또는 SET ROWCOUNT문을 사용한다.
- 결과 전체가 아닌 일부만 반환하므로 네트워크 트래픽을 감소시킬 수 있다.
(11) 몇개 Row의 빠른 반환이 필요하다면 FAST number_rows 힌트를 사용한다.
- 이를 사용하면 N개의 Row를 빠르게 얻을 수 있으며 이후 쿼리는 계속 실행되서 전체 결과를 만들어낸다.
SELECT f_idx, f_nickname, f_age, f_gener, f_joindate FROM tbl_member WHTH (NOLOCK)
WHERE f_age = 20
OPTION (FAST 100)
(12) JOIN을 사용하는 경우 INNER JOIN을 되도록 사용하라.
- 동일한 효과를 가지는 쿼리를 작성할 경우 INNER JOIN이 아닌 LEFT OUTER JOIN을 습관적으로 사용하는 경우가 있다. 두개의 조인방식은 확연한 속도 차이가 나므로 되도록이면 INNER JOIN을 사용하는 것이 좋다.
-- 잘못된 예
SELECT a.f_idx, a.f_name, a.f_nickname, a.f_age, a.f_gender, a.f_joindate, b.f_address
FROM tbl_member AS a LEFT OUTER JOIN tbl_memberdesc AS b
ON a.f_idx = b.f_idx
WHERE a.f_age > 19 AND a.f_age < 30
-- 올바른 예
SELECT a.f_idx, a.f_name, a.f_nickname, a.f_age, a.f_gender, a.f_joindate, b.f_address
FROM tbl_member AS a INNER JOIN tbl_memberdesc AS b
ON a.f_idx = b.f_idx
WHERE a.f_age > 19 AND a.f_age < 30
(13) 서브쿼리의 사용시 불필요한 SELECT 구문을 줄인다.
- SELECT가 해당 Row를 호출할 때마다 서브 쿼리에 있는 Address를 구하는 쿼리를 호출하기 때문이다. 출력하는 Row가 많으면 많을수록 서브 쿼리의 실행 횟수 또한 증가하게 되며 불필요한 부하를 가져온다.
(14) 가능한 UNION 대신 UNION ALL을 사용한다.
- UNION ALL은 Row의 중복검사를 하지 않는 반면, UNION은 중복행 존재 유무와 관계없이 중복검사를 수행한다.
(15) VIEW 사용은 자제한다.
- 개발 편의상, 보안상 여러가지 이유로 VIEW는 좋은 개념이고 사용할만한 가치가 있다. 다만 성능상의 문제 때문에 무분별한 사용은 자제해야한다.
- (5)번에서 언급한 바와 같이 결과 레코드가 가장 적은 (HIT RATIO가 높은) 문장이 먼저 실행되어야하는데, VIEW를 먼저 가져오는 작업이 그 뒤에 실행되는 WHERE조건절보다 더 많은 결과를 가져오게 하는 것이 대부분이므로, 성능에는 불리할 수 밖에 없다.
- 또한 여러개의 테이블이 조인되는 경우 인덱스 사용에 제약이 많다. 물론 INDEXED VIEW가 있어서 어느정도 성능에는 효과가 있으나, 관련 테이블의 수정이 발생할 경우 문제가 생길 수 있다.
(16) 커서 및 임시 테이블의 사용을 최대한 자제한다.
- 커서 보다는 임시 테이블을 사용하는 것이 좋고, 임시 테이블보다는 테이블 변수를 사용하는 것이 성능에 좋다.
- 커서의 경우 내부적으로는 임시테이블을 사용하지만 임시테이블을 쓴다고 부하가 더 발생하는 것은 아니다. 오히려 커서의 부가적 기능으로 서버 자원을 더 낭비하게 된다.
- 커서로 처리할 수 있는 것은 모두 임시 테이블이나 테이블 변수로도 처리가 가능하므로 되도록 커서를 쓰지 않는다.
- 만약 커서를 반드시 사용해야 한다면 클라이언트 측 커서를 사용하고, 서버의 커서를 사용할 때에는 가능한 작은 결과 셋을 가져오도록 한다.
- 커서를 다 사용한 후에는 그냥 닫는 것(close)이 아니라 반드시 해제(deallocate) 시켜야한다.
(17) 가능한 트리거 대신 제약조건을 사용한다.
- 제약조건은 트리거보다 훨씬 성능면에서 효율적이다.
(18) table hints를 사용한다.
- IGNORE_CONSTRAINT, IGNORE_TRIGGERS, NOWAIT, PAGELOCK, TABLELOCK, ROWLOCK, UPDLOCK, XLOCK, FASTFIRSTROW 등 여러가지 힌트가 존재하며 일반 쿼리문에 적절한 table hints를 사용해서 성능을 높힌다.
(19) 저장 프로시저를 사용한다.
- 저장 프로시저는 복잡한 SQL문을 단순화 시켜주고, 보안문제를 해결해주며 더 나아가 빠른 성능의 매개변수, 출력 매개변수, 리턴 값을 사용할 수 있다.
- 저장 프로시저는 실행 계획이 Plan 캐쉬에 캐싱된다. 즉, 한번 컴파일되면 이것이 캐쉬에 저장되고 재사용이 되면 될수록 재컴파일이 필요없기에 효율이 올라간다.
(20) SET NOCOUNT ON/OFF를 사용한다.
- 저장 프로시저의 경우 결과 셋으로 보내는게 아니라, 내부적으로 중간 단계에서 사용하기 위해 조회를 수행하는 경우가 많다. 이 때 SET NOCOUNT ON을 지정하고 수행해야 Client에게 조회 결과가 전달되지 않는다. 최종 결과만을 Client에게 보내기 위해서 SET NOCOUNT OFF를 설정한 후 최종 결과 조회문을 실행해야한다. 이렇게 하는 것이 쓸데없는 정보로 인한 네트워크 트래픽 증가를 방지할 수 있다.
(21) 되도록 한꺼번에 SQL 문장을 실행한다.
- Connection Pool 이 있어서 Connection Resource에 의한 성능 저하는 별로 없겠지만, 계속된 Network Round-Trip은 성능에 많은 영향을 미친다. 되도록 한꺼번에 요청하고 반환받아 Round-Trip을 최소화 해야한다.
(22) 다른 서버에서 정보를 가져올 경우 연결된 서버를 이용할 때, 4-part name 방식 말고 OPENQUERY를 사용한다.
① 4-part name 방식
- 성능이 나쁘고 자주 사용하지 않는다.
- 쿼리 작성이 간단하다.
- INSERT, UPDATE, DELETE 문장을 보통 쿼리문과 비슷하게 사용한다.
② OPENQUERY 방식
- 성능이 나쁘지 않고, 자주 사용한다.
- 쿼리를 문자열로 조합하기 복잡하다.
- OPENQUERY 내의 문자열은 완성된 형태의 문자열만 가능하며, 변수는 받을 수 없다.
-- 잘못된 예
SELECT count(*) FROM [Linked Server Name].[Database Name].[Owner Name].[Table Name]
-- 올바른 예
SELECT * FROM OPENQUERY([Linked Server Name], 'SELECT count(*) FROM [Database Name].[Owner Name].[Table Name]')
3. Tunning
(1) MSSQL을 사용하는 경우 예상 실행 계획을 자주 확인한다.
- MSSQL은 쿼리분석기에서 쿼리를 테스트하기 편하다. 좋은 기능 중 하나가 예상 실행계획인데 해당 쿼리가 성능상 어떤 장단점을 가지고 있는지 보기 쉽게 그래픽 실행 계획 아이콘으로 표시해준다.
- 실행계획 내용은 버릴것이 없으므로 꼼꼼히 따져보아야 한다.
- 튜닝의 시작은 성능 분석이다.
(2) Index를 타는지 항상 체크한다.
- Index를 활용하지 않은 검색은 데이터가 많으면 많을수록 성능은 급격히 떨어진다. schema를 잘못 짠 경우 이런 현상이 흔히 발생하는데 이에 대한 점검을 늘 해야한다.
- 흔히 오해하기 쉬운 것 중 WHERE 조건절은 필요한 column에만 존재해야 한다는 생각이다. WHERE 조건절에는 Clustered Index Seek를 타기 위한 Column이 우선 존재해야 하고 그 후에 원하는 데이터를 얻기 위한 조건절이 존재해야 한다. 조건 자체가 Clustered Index Column이면 가장 좋다.
- 조건절에 Index에 해당하는 column들이 존재하는 경우 우선적으로 해당 조건을 만족하는 행을 호출한 후 나머지 조건에 대해 만족하는 행을 다시 호출하게 된다.
(3) Clustered Index Seek를 항상 체크한다.
- Clustered index scan을 타는 것만으로도 속도는 향상 되지만 완전하지 않다. clustered index column의 일정 구간을 타는 seek여야 대량으로 증가하는 data에 대한 부하를 감당할 수 있다. 이를 위해 index의 구간 체크를 해야 한다.
- 만약 검색하는 column이 clustered index column인 경우는 단방향 WHERE 조건문으로도 index scan이 성립된다. 자신의 column에서 그대로 찾아서 시작지점부터 끝까지 index를 타면 되기 때문이다.
- 하지만 일반 non-clustered index의 경우는 clustered를 찾기 위해 해당 column의 clustered index 정보를 호출해야하는 부담이 생긴다. 왜냐하면 결국 호출을 하기 위해서는 해당 데이터의 위치를 찾아야 하고 이 위치를 가장 밀도있게 알고 있는 clustered index에서 해당 데이터의 위치를 찾아 가져오기 때문이다. (바로 찾게 되면 clustered index보다 범위가 크기 때문에 중간에 clustered index를 통해 찾는다. ) 결국 구간 체크가 아닌 non-clustered index의 단방향 WHERE 조건문은 clustered의 전체 스캔을 하게 되는 결과를 가져온다.
'DBA' 카테고리의 다른 글
데이터베이스 해시 조인 (HASH JOIN) (0) | 2024.10.29 |
---|---|
데이터베이스 중첩 루프 조인 (NESTED LOOPS JOIN, NL JOIN) (0) | 2024.10.29 |
데이터베이스 샤딩(Database Sharding)이란? (1) | 2024.09.10 |
DBMS 별 버전 정보 확인 (0) | 2020.11.30 |
SQL 작성 표준 (0) | 2020.04.28 |