ALTER DATABASE testdb ADD FILE (name='InMemory_Test', filename='E:\DATA\InMemory_Test') TO FILEGROUP InMemoryFileGroup



명시 적-Transact-SQL에는 최종 COMMIT TRANSACTION과 함께 BEGIN TRANSACTION 코드가 포함되어 있습니다. 

두 개 이상의 명령문을 동일한 트랜잭션에 상관시킬 수 있습니다. 

명시 적 모드에서는 데이터베이스 옵션 MEMORY_OPTIMIZED_ELEVATE_TO_SNAPSHOT을 사용하거나 FROM 절의 메모리 최적화 테이블에서 트랜잭션 격리 레벨에 대한 테이블 힌트를 코딩해야합니다.



아래는이 데이터베이스 옵션을 켜는 방법을 보여주는 예입니다.



SELECT is_memory_optimized_elevate_to_snapshot_on,  * FROM sys.databases

where is_memory_optimized_elevate_to_snapshot_on = 1



SELECT SCHEMA_NAME(Schema_id) SchemaName,

name TableName,

is_memory_optimized,

durability_desc,

create_date, modify_date

FROM sys.tables

where is_memory_optimized = 1

GO



ALTER DATABASE YourDatabase SET MEMORY_OPTIMIZED_ELEVATE_TO_SNAPSHOT = ON;



--

ALTER DATABASE [dbdominos2011] REMOVE FILE memory_optimized_table;

ALTER DATABASE [dbdominos2011] REMOVE FILEGROUP InMemoryGroup;

ALTER DATABASE [dbdominos2011] ADD FILE ( NAME = N'memory_optimized_web_member', FILENAME = N'E:\DATA\memory_optimized_table_web_member' ) TO FILEGROUP InMemoryGroup;

GO
-- Backup Script
DECLARE @name VARCHAR(50) -- database name   
DECLARE @path VARCHAR(256) -- path for backup files   
DECLARE @fileName VARCHAR(256) -- filename for backup   
DECLARE @fileDate VARCHAR(20) -- used for file name  

SET @path = 'C:\Backup\'

SELECT @fileDate = CONVERT(VARCHAR(20),GETDATE(),112) + '_' + REPLACE(CONVERT(VARCHAR(20),GETDATE(),108),':','')

DECLARE db_cursor CURSOR FOR
      SELECT name
      FROM master.dbo.sysdatabases  
      WHERE name NOT IN ('master','model','msdb','tempdb')  
      AND DATABASEPROPERTYEX(name, 'Recovery') IN ('FULL','BULK_LOGGED') 

OPEN db_cursor    
FETCH NEXT FROM db_cursor INTO @name    

WHILE @@FETCH_STATUS = 0    
BEGIN    
      SET @fileName = @path + @name + '_' + @fileDate + '.TRN'   
      BACKUP LOG @name TO DISK = @fileName   

      FETCH NEXT FROM db_cursor INTO @name    
END    

CLOSE db_cursor    
DEALLOCATE db_cursor

'Database > SQL Server' 카테고리의 다른 글

DB 주의대상  (0) 2020.08.28
In-Memory OLTP  (0) 2020.08.28
sp_cursorfetch 및 FETCH API_CURSOR에서 사용되는 쿼리는 무엇입니까?  (0) 2020.08.28
index dmv  (0) 2020.08.27
운영 데이터 저장소 Vs 데이터 웨어하우스  (0) 2020.08.27

활동 모니터를 보면 최근에 비싼 쿼리와 쿼리 열 아래에 "fetch api_cursor0000000000000003"이 표시됩니다. 이게 뭐야? 이 쿼리 뒤에 무엇이 실행되고 있는지 어떻게 알 수 있습니까?

분명히 그 질문의 힌트는 "Fetch and Cursor"키워드였습니다. 그래서 시작점이 무엇인지 알았습니다. 쿼리를 추적하고 싶지만 프로파일 러를 캡처하거나 sys.sysprocesses 또는 DBCC INPUTBUFFER의 기존 방법을 사용하면 다음과 같은 상황이 몇 번이나 나타납니다.

프로파일 러에서도 똑같은 것은 아래와 같습니다.

EXEC sp_cursorfetch 180150003,16,8,1

또는

FETCH API_CURSOR0000000000000001

참고 : 쿼리의 숫자는 환경에 따라 다를 수 있습니다. 다음은 문제를 시뮬레이션하는 VBScript 코드입니다.

Dim strConnection
Dim MyConnection
Dim MyRecordSet
Dim strSQL
strConnection = “Provider=SQLOLEDB;Data Source=.\SQL2014;Initial Catalog=Master;Integrated Security=SSPI;”
Set MyConnection = CreateObject(“ADODB.Connection”)
MyConnection.Open strConnection
Set MyRecordSet = CreateObject(“ADODB.recordset”)
strSQL = “SELECT TOP 3 * FROM sysobjects”
MyRecordSet.Cursorlocation = 2
MyRecordSet.Open strSQL, MyConnection, 3,3
MyRecordSet.MoveFirst
WHILE NOT MyRecordSet.EOF
MsgBox(MyRecordSet(“name”).value)
MyRecordSet.MoveNext
WEND
MyRecordSet.Close
Set MyRecordSet = Nothing

문제를 재현하려면 위의 코드를 파일에 저장하고 확장명 .vbs를 유지할 수 있습니다. 그런 다음 명령 프롬프트에서 실행할 수 있습니다. 환경에 맞게 연결 문자열에서 Source 값을 변경해야 할 수 있습니다. VB 스크립트를 실행하면 테이블 이름이 포함 된 팝업이 표시됩니다. 프로파일 러가 처음부터 캡처 된 경우 쿼리를 알 수있는 sp_cursoropen이 표시되어야합니다. 쿼리가 이미 실행중인 경우 아래 쿼리를 사용할 수 있습니다.

SELECT creation_time,
cursor_id,
c.session_id,
c.properties,
c.creation_time,
c.is_open,
SUBSTRING(st.TEXT, ( c.statement_start_offset / 2) + 1, (
( CASE c.statement_end_offset
WHEN -1 THEN DATALENGTH(st.TEXT)
ELSE c.statement_end_offset
END - c.statement_start_offset) / 2) + 1) AS statement_text
FROM   sys.Dm_exec_cursors(0) AS c
JOIN sys.dm_exec_sessions AS s
ON c.session_id = s.session_id
CROSS apply sys.Dm_exec_sql_text(c.sql_handle) AS st
GO

 

다음은 샘플 실행입니다. 보시다시피 커서를 연 쿼리 텍스트를 얻을 수 있습니다.

 

참고 : cursor_id는 sp_cursorfetch에서 본 것과 동일합니다. 또한 전체 프로필을 캡처하면 아래 출력을 볼 수 있습니다.

쿼리가 시작된 이후 전체 프로파일 러를 보거나 캡처 sp_cursorfetch 하거나 sys.Dm_exec_cursors를 사용하여 정확한 쿼리를 가져옵니다.FETCH API_CURSOR0000000000000001 

'Database > SQL Server' 카테고리의 다른 글

In-Memory OLTP  (0) 2020.08.28
Backup Script  (0) 2020.08.28
index dmv  (0) 2020.08.27
운영 데이터 저장소 Vs 데이터 웨어하우스  (0) 2020.08.27
Ad-hoc 쿼리를 매개변수화 하여 성능 높이기  (0) 2020.08.27

sys.dm_db_missing_index_group_stats

열 이름데이터 형식설명

group_handle

int

누락된 인덱스 그룹을 식별합니다. 이 식별자는 서버에서 고유합니다.

다른 열은 그룹의 인덱스가 누락된 것으로 간주되는 모든 쿼리에 대한 정보를 제공합니다.

인덱스 그룹에는 인덱스가 하나만 포함되어 있습니다.

unique_compiles

bigint

이 누락된 인덱스 그룹에 적합한 컴파일 및 다시 컴파일 수입니다. 서로 다른 많은 쿼리의 컴파일 및 다시 컴파일이 이 열 값에 영향을 줄 수 있습니다.

user_seeks

bigint

그룹의 권장 인덱스가 사용되었을 수 있는 사용자 쿼리에 의해 수행된 검색(Seek) 수입니다.

user_scans

bigint

그룹의 권장 인덱스가 사용되었을 수 있는 사용자 쿼리에 의해 수행된 검색(Scan) 수입니다.

last_user_seek

datetime

그룹의 권장 인덱스가 사용되었을 수 있는 사용자 쿼리에 의해 수행된 마지막 검색(Seek)의 날짜와 시간입니다.

last_user_scan

datetime

그룹의 권장 인덱스가 사용되었을 수 있는 사용자 쿼리에 의해 수행된 마지막 검색(Scan)의 날짜와 시간입니다.

avg_total_user_cost

float

그룹의 인덱스로 줄일 수 있는 사용자 쿼리의 평균 비용입니다.

avg_user_impact

float

누락된 인덱스 그룹을 구현할 경우 사용자 쿼리에서 얻을 수 있는 적합한 평균 백분율입니다. 즉, 이 누락된 인덱스 그룹을 구현할 경우 쿼리 비용이 평균적으로 이 백분율만큼 감소합니다.

system_seeks

bigint

그룹의 권장 인덱스가 사용되었을 수 있는 auto stats 쿼리와 같은 시스템 쿼리에 의해 수행된 검색(Seek) 수입니다. 자세한 내용은 Auto Stats 이벤트 클래스를 참조하십시오.

system_scans

bigint

그룹의 권장 인덱스가 사용되었을 수 있는 시스템 쿼리에 의해 수행된 검색(Scan) 수입니다.

last_system_seek

datetime

그룹의 권장 인덱스가 사용되었을 수 있는 시스템 쿼리에 의해 수행된 마지막 시스템 검색(Seek)의 날짜와 시간입니다.

last_system_scan

datetime

그룹의 권장 인덱스가 사용되었을 수 있는 시스템 쿼리에 의해 수행된 마지막 시스템 검색(Scan)의 날짜와 시간입니다.

avg_total_system_cost

float

그룹의 인덱스로 줄일 수 있는 시스템 쿼리의 평균 비용입니다.

avg_system_impact

float

누락된 인덱스 그룹을 구현할 경우 시스템 쿼리에서 얻을 수 있는 적합한 평균 백분율입니다. 즉, 이 누락된 인덱스 그룹을 구현할 경우 쿼리 비용이 평균적으로 이 백분율만큼 감소합니다.

 

 

sys.dm_db_missing_index_details

열 이름데이터 형식설명

index_handle

int

특정 누락된 인덱스를 식별합니다. 이 식별자는 서버에서 고유합니다. index_handle은 이 테이블의 키입니다.

database_id

smallint

누락된 인덱스가 있는 테이블이 위치한 데이터베이스를 식별합니다.

object_id

int

인덱스가 없는 테이블을 식별합니다.

equality_columns

nvarchar(4000)

다음 형식의 같음 조건자에 적용되는 쉼표로 구분된 열 목록입니다.

table.column =constant_value

inequality_columns

nvarchar(4000)

다음 형식의 같지 않음 조건자에 적용되는 쉼표로 구분된 열 목록입니다.

table.column > constant_value

"="가 아닌 모든 비교 연산자는 같지 않음을 표시합니다. 전체 비교 연산자 목록은 비교 연산자(데이터베이스 엔진)를 참조하십시오.

included_columns

nvarchar(4000)

쿼리에 대한 포함 열로서 필요한 쉼표로 구분된 열 목록입니다. 포함 열 또는 포괄 열에 대한 자세한 내용은 포괄 열을 사용하여 인덱스 만들기를 참조하십시오.

statement

nvarchar(4000)

인덱스가 없는 테이블의 이름입니다.

 

 

운영 데이터 저장소(ODS)

Data warehouse

ODS 는 온라인의(운영의) 데이터를 통합(수집) 한다.

일반적으로 온라인(운영의) 정보는 제공 하지 않는다. 오프라인 데이터(대량의 시계열 데이터) 를 제공한다.

ODS는 어떤 다른 시스템들이 직접적으로 접근하기 위해 요구되어 지는 시스템의 데이터로 사용되어 진다. 예) 다른 데이터베이스에 있는 데이터는 다양한 부분(시스템)들에 의해 갱신되어진다. 병원의 정산 업무 또는 다른 어떤 업무들을 실행하는 동안 이러한 것들이 요구되어 진다.

과거 데이터와 현재 데이터(조금 오래된 데이터, 시간 또는 몇일 전의 데이터) 로 의사 결정을 할 때 사용되어진다.

ODS 는 온라인 운영 (운영 업무)를 위한 데이터 저장소의 핵심이다.

운영 업무가 아닌 것에 사용된다.

 

Ad-hoc 쿼리를 매개변수화 하여 성능 높이기

 

  • Version : SQL Server 2005, 2008, 2008R2, 2012

 

많은 응용 프로그램에서 SQL Server에 연결하여 쿼리를 호출 한다. 보통 튜닝 가이드를 할 때 임의의 쿼리보다는 프로시저를 만들어서 호출 하기를 권장한다. 이러한 이유는 프로시저를 사용함으로써 여러 가지 이점이 있기 때문이다. 대표적인 예가 쿼리를 계속해서 컴파일 하지 않는다는 것이다. 컴파일에 따른 비용 절약과 플랜을 재사용 할 수 있기 때문에 SQL Server의 성능을 높일 수 있다.

 

일반적으로 날쿼리라 불리는 응용프로그램에서 매개변수화 하지 않는 쿼리를 요청 한다고 가정하자. 대부분의 쿼리는 같은 쿼리를 사용하면서 Where 절의 조건만 변형된 쿼리를 사용 할 것이다.

 

다음과 같이 동일한 쿼리에서 Where 절의 조건만 변형하여 호출 하여 보자.

(예제코드 : C#)

 

플랜캐쉬를 확인하여 플랜이 사용되고 있는지 확인 하자.

select qs.usecounts, cacheobjtype, objtype, qt.text

from sys.dm_exec_cached_plans qs

cross apply sys.dm_exec_sql_text(qs.plan_handle) as qt

order by qt.text

go

 

 

구문을 동일하나 Where 절의 조건에 따라 각 다르게 쿼리를 인식하여 플랜을 재사용하지 못하고 모든 쿼리를 컴파일 하여 사용 하였다.

 

 

쿼리를 매개변수화 시켜 호출 하여 보자.

 

 

Where 절이 다름에도 불구하고 처음 플랜을 생성한 뒤로 9번을 재사용한 것을 확인 할 수 있다.

프로파일러에서도 RPC 호출을 통하여 바인드된 것을 확인 할 수 있다.

 

 

DB튜닝을 하다 보면 데이터베이스뿐만 아니라 여러 응용 프로그램을 함께 수정해야 할 때가 많다. 처음 개발부터 이러한 환경의 차이를 알고 개발 한다면 성능에 여러 이점이 있으리라 생각한다.

 

[참고자료]

http://www.mssqltips.com/sqlservertip/1632/are-your-sql-server-application-queries-wasting-memory/

'Database > SQL Server' 카테고리의 다른 글

index dmv  (0) 2020.08.27
운영 데이터 저장소 Vs 데이터 웨어하우스  (0) 2020.08.27
온라인 인덱스 구성(Online Index)  (0) 2020.08.27
MSSQL TEMPDB DBFILE 삭제  (0) 2020.08.27
인덱스  (0) 2020.08.27

SQL 2005 부터 인덱스와 관련하여 개선된 기능 중 하나인 온라인 인덱스 작업에 대하여 알아 보겠습니다. 온라인 인덱스 작업은 1년 365일 운영되어야 하는 시스템을 관리하는 DBA들에게 획기 적인 기능입니다. 온라인 인덱스를 할수 있게 됨으로써 다운 타임을 감소할수 있게 되었으며 인덱스를 재구성할려고 할때도 소요 시간 때문에 많은 고민을 해야 하는 수고를 덜어 줄수 있게 되었습니다. Create Index , Alter Index , Drop index Alter Table 명령어에서 ONLINE 옵션을 사용할수 있습니다.

온라인 인덱스 구성 vs. 오프라인 인덱스 구성ONLINE 옵션은 이러한 인덱스 작업을 수행하는 동안에도 사용자가 테이블이나 클러스터형(Clustered) 인덱스와 관련된 비클러스터형(Nonclustered) 인덱스에 접근할수 있습니다. 오프라인으로 클러스터형 인덱스를 구성하거나 재구성하는 등의 DDL 작업을 하면 이는 해당 데이터와 관련 인덱스에 배타적 잠금을 보유하게 되고 이로 인해 다른 사용자가 데이터나 인덱스에 접근하지 못하도록 방해하게 된다.

Example:

CREATE INDEX idx_t ON t(c1, c2)

WITH (ONLINE = ON)

 

일반 인덱스 구성 vs. 병렬 인덱스 구성멀티 프로세서를 가진 SQL서버에서 인덱스 구문 또한 다른 쿼리를 실행할 때처럼, 스캔, 정렬, 그리고 구성 작업을 수행하는데 병렬 처리가 가능 해졌습니다. 병렬 처리 수는 최대 병렬 처리 수(sp_configure로 설정한), MAXDOP 인덱스 옵션, 현재 작업부하의 크기, 파티션되지 않은 경우, 첫 번째 키 칼럼의 데이터 분포등에 의해서 결정될 수 있다.

 

Example:

CREATE INDEX idx_t ON t(c1, c2)

WITH (MAXDOP = 2)

-- 인덱스 구성에 2개의 프로세서 사용

 

온라인 인덱스 가능한 SQL 버전Microsoft SQL Server 2005 Enterprise Edition

 

온라인 인덱스는 어떻게 동작 할까?

 

 

인덱스 DDL(데이터 정의 언어) 작업 중에 동시 사용자 작업이 가능하도록 하려면 온라인 인덱스 작업 중에 원본 및 기존 인덱스, 대상 및 임시 매핑 인덱스(클러스터형 인덱스의 경우) 로 구성됨으로써 온라인 인덱스 작업 수행을 할수 있도록 합니다.

원본 및 기존 인덱스원본은 원래 테이블이거나 클러스터형 인덱스 데이터입니다. 기존 인덱스는 원본 구조와 관련된 모든 비클러스터형 인덱스입니다. 예를 들어 온라인 인덱스 작업이 네 개의 관련 비클러스터형 인덱스가 있는 클러스터형 인덱스를 재구성하는 것이라면 원본은 기존 클러스터형 인덱스이고 기존 인덱스는 비클러스터형 인덱스입니다.기존 인덱스는 여러 사용자의 동시 선택, 삽입, 업데이트 및 삭제 작업에 사용됩니다. 여기에는 대량 삽입(지원되지만 권장하지 않음)과 트리거 및 참조 무결성 제약 조건에 의한 암시적 업데이트가 포함됩니다. 쿼리와 검색에 모든 기존 인덱스를 사용할 수 있습니다. 즉, 기존 인덱스를 쿼리 최적화 프로그램에서 선택할 수 있으며 필요한 경우 인덱스 힌트에 지정할 수 있습니다.

대상대상은 만들거나 다시 작성하는 새 인덱스(또는 힙)이거나 새 인덱스 집합입니다. SQL Server 데이터베이스 엔진에서는 인덱스 작업을 수행하는 동안 원본에 대한 사용자 삽입, 업데이트 및 삭제 작업을 대상에 적용합니다. 예를 들어 온라인 인덱스 작업이 클러스터형 인덱스를 다시 작성하는 것이라면 대상은 다시 작성되는 클러스터형 인덱스입니다. 데이터베이스 엔진에서는 클러스터형 인덱스를 다시 작성할 때 비클러스터형 인덱스를 다시 작성하지 않습니다.대상 인덱스는 인덱스 작업이 커밋될 때까지 SELECT 문을 처리하는 동안 검색되지 않습니다. 내부적으로 인덱스가 쓰기 전용으로 표시됩니다.

임시 매핑 인덱스클러스터형 인덱스를 만들거나 삭제하거나 다시 작성하는 온라인 인덱스 작업에는 임시 매핑 인덱스도 필요합니다. 이러한 임시 인덱스는 동시 트랜잭션이 기본 테이블의 행이 업데이트되거나 삭제될 때 작성되는 새 인덱스에서 삭제할 레코드를 결정하는 데 사용합니다. 이러한 비클러스터형 인덱스는 새 클러스터형 인덱스(또는 힙)와 같은 단계에서 만들어지므로 별도의 정렬 작업이 필요 없습니다. 동시 트랜잭션의 모든 삽입, 업데이트 및 삭제 작업에서도 임시 매핑 인덱스가 유지됩니다.

 

참고 사이트

 

http://blogs.msdn.com/sqlqueryprocessing/archive/2006/11/08/index-build-strategy-in-sql-server-introduction-i.aspxhttp://technet.microsoft.com/ko-kr/library/ms191261.aspx

 

 

출처: https://gdbt.tistory.com/68 [Gravity DB Team]

머신 사양들이 좋아지면서 해당 리소스를 충분히 활용하기 위해 

 

 

 

그리고 mssql 의 성능 향상을 위해 tempdb 파일을 늘리는데.. 

 

 

 

여러 자료를 조합해 본 결과 tempdb 의 데이터 파일은 머신의 코어 수와 맞추면 된단다..

 

 

 

단 요즘은 코어가 하도 많아서... 8개 이상은 의미가 없다... 고 한다 

 

 

 

뭐 스트레스 테스트까지는 해보지 않아서 정확한건 모르겠지만.. 

 

 

 

선인들이 해보셨으면 나도 비슷하다 생각하고... 파일을 늘려놨는데...

 

 

 

 

 

파일 늘리기 전 상황을 캡쳐해놓은것이 없어 파일을 늘린것이 얼마나 효과가 있는지 잘 모르겠다.. 

 

 

 

그래서 역으로 테스트를 해보려 한다.. 

 

(tempdb datafile이 8개인경우 먼저 캡쳐 && 4개 줄여서 테스트 ㅋㅋ)

 

 

 

근데 웃긴건 tempdb datafile 이 만들때는 잘 만들어지는데 지워지지 않는다 ㅋㅋㅋㅋㅋ

 

 

 

 

 

 

 

그래서 잘~~~ 찾아보니.. datafile이 비어지지 않아서란다 ㅋㅋㅋ

 

 

 

자동으로 비우고 해주면 되지... 알파고도 나온마당에 뭘 또 에러 띄우고 그러시나.. 

 

 

 

 

 

암튼 지우기 전에 아래의 쿼리를 실행하고 돌리쟈.. 

 

 

 

==================================================================

 

USE [tempdb]

 

GO

 

DBCC SHRINKFILE ([데이터파일명], EMPTYFILE);

 

GO

 

 

 

use [master]

 

ALTER DATABASE [tempdb]  REMOVE FILE [데이터파일명]

 

 

 

GO

 

===================================================================

 

 

 

그럼 겁내 잘지워진다 ㅎㅎㅎㅎ

 

 

 

 

 

 

 

근데... 실제 사용자들이 사용하는 DB는 아래의 메세지와 함께 또.. 지워지지 않는다.. 

 

 

 

=========================================================================================

 

 

 

DBCC SHRINKFILE: 페이지 9:1320은(는) 작업 테이블 페이지이므로 이동할 수 없습니다.

 

메시지 2555, 수준 16, 상태 2, 줄 3

 

파일 "tempdb8"의 일부 내용을 다른 위치로 이동할 수 없어 emptyfile 작업을 완료할 수 없습니다.

 

DBCC 실행이 완료되었습니다. DBCC에서 오류 메시지를 출력하면 시스템 관리자에게 문의하십시오.

 

 

 

==========================================================================================

 

 

 

이유인 즉슨.. 해당 데이터파일이 사용중이다..(내지는 다른데가 사용중이라 비울수 없다..)

 

 

 

그래서 찾아보니... (http://sqlserverzest.com/2012/10/07/msg-2555-level-16-state-1-line-1-error-dbcc-shrinkfile-with-emptyfile-option/

 

 

 

위와 같은 메세지가 날때는 

 

 

 

1. 외부 커넥션을 다 끊던지..

 

2. 서버를 재시작 하라고 하신다 ㅎㅎㅎㅎ

 

 

 

실운영 서버라 테스트는 못했지만.. 꼭 필요한 경우라면 진행 가능할듯 하다. ㅎ

 

 

 

그럼 오늘의 정리는 이만 ㅎ

 

'Database > SQL Server' 카테고리의 다른 글

Ad-hoc 쿼리를 매개변수화 하여 성능 높이기  (0) 2020.08.27
온라인 인덱스 구성(Online Index)  (0) 2020.08.27
인덱스  (0) 2020.08.27
쿼리 저장소 (Query Store)  (0) 2020.08.27
모든 DB의 로그 잘림 확인하기  (0) 2020.08.27

페이지(Page)란?

  • 데이터 파일을 구성하는 논리 단위

  • SQL Server의 기본 데이터 저장 단위(8KB)

  • 데이터를 쓸 때 행을 페이지에 기록됨

  • 데이터를 읽을 때 페이지 내의 모든 행이 읽어짐

  • 페이지 내의 행이 많을 수록 I/O 효율 증가

  • 0 ~ n 사이의 순차적인 번호 -> 페이지 번호

데이터를 INSERT 하게 된다면 페이지에 들어가게 되고, 테이블에 쿼리를 날려 조회하는 것이 아니라 페이지에서 SELECT하는 것으로 생각하면 됩니다. 그러므로 페이지라는 것을 숙지해야합니다.

 

인덱스(Index) 는 왜 사용하고 장점은 무엇인가?

  • 빠른 데이터를 검색하기 위해서입니다. 찾는 데이터를 가지고 있다면 직접 주거나, 없다면 어디 있는지 알려줍니다.

  • 데이터의 중복을 방지할 수 있습니다. (Primary Key, Unique)

  • 잠금을 최소화 시켜줍니다. (동시성을 높여줍니다.)

 

인덱스(Index) 의 단점은 무엇인가?

  • 물리적인 공간을 차지하게 됩니다. (인덱스도 테이블처럼 데이터를 가지므로 물리적인 공간을 차지하게 됩니다.)

  • 페이지를 가지고 있는 존재는 데이터와 인덱스 두가지입니다. (프로시져/뷰는 사이즈가 없습니다.)

  • 인덱스에 대한 유지관리 부담이 존재합니다.

  • 데이터가 극히 적다면은 얻는 효과보다 유지관리 부담이 더 클 수 있습니다.

테이블의 존재 형태는 아래 그림 처럼 두 가지로 나뉩니다. 힙(heap)과 클러스터 형 인덱스 입니다.

 

 

 

 

힙(Heap) 이란?

  • 정렬의 기준이 없이 저장 된 테이블의 형태를 말합니다.

  • 데이터 페이지 내의 행들 간에 순서가 없고, 페이들 간에도 순서가 없습니다.

  • 클러스터 형 인덱스가 없는 테이블이라고 생각하시면 쉽습니다.

힙의 장점과 단점은 무엇인가?

  • 장점은 INSERT 문이 좋아하는 테이블 형태입니다. 새로운 행을 기존 페이지의 빈 곳에 추가하면 되고, 빈 공간이 없으면 새로운 페이지에 추가하면 되기 때문입니다.

  • 단점은 SELECT 문이 싫어하는 테이블 형태입니다. 데이터가 극히 적으면 상관없겠지만, 데이터가 많으면 전체적으로 모든 테이블을 스캔해야하기 때문에 데이터를 찾기가 어렵습니다.

위에서도 말했듯이, 원하는 데이터를 찾기 위해서는 Table Scan 을 해야 합니다. 전체적으로 페이지를 다 읽는겁니다. 전체적으로 모든 페이지를 다 읽는다고 하니, 당연히 좋지 않습니다.

 

클러스터 형 인덱스란?

  • 특정 열(또는 열들)을 기준으로 데이터가 정렬되어 있습니다. (물리적 정렬이 아닌, 논리적 정렬)

  • 테이블 당 하나의 클러스터 형 인덱스를 설정 가능합니다.

 

 

 

다시 한번 말하면, 테이블의 형태는 두가지로 나뉘게 됩니다.

정렬되어 있지 않은 힙과 특수한 Key 값으로 정렬되어진 클러스터 형 인덱스로 나누어집니다. 우리는 선택을 해야 합니다. 힙을 사용할지, 클러스터 형 인덱스를 선택할지. 힙이 무조건 좋지 않다라고 말할 수 없습니다. 상황마다 다르기 때문에 그 상황에 맞게 테이블을 설계하는 것이 개발자의 몫입니다.

 

 

클러스트 형의 데이터 찾기

이제부터 많은 용어가 나옵니다. 그러한 용어는 필수적으로 숙지해야합니다.

 

 

 

  • Clustered Index Seek : Root 페이지부터 찾아가서, 아주 빠른 성능을 보여줍니다.

  • Clustered Index Scan : 모든 데이터 페이지를 읽기 때문에 힙에서 사용했던 방식인 Table Scan과 다를바 없습니다.

힙 + 비 클러스터 형 인덱스

  • 비 클러스터 형 인덱스에는 인덱스 키 열의 모든 데이터와 RID(행의 주소)를 가지고 있습니다.

  • RID를 가지고 있는 이유는 RID는 거의 변하지 않고 크기가 크지 않기 때문에 한번에 찾아갈 수 있기 때문입니다.

  • 999개 까지 만들 수 있습니다.

  • Index Seek : NonClustered 가 생략되어졌고, Root 페이지를 이용하기 때문에 성능이 좋습니다.

  • RID Lookup :  비 클러스터형 인덱스에 RID 값이 저장되어 있기 때문에, 힙을 검색할때에는 RID값을 이용해서 검색하기 때문에 RID Lookup이 됩니다.

 

 

 

클러스터 형 인덱스 + 비 클러스터 형 인덱스

  • 비 클러스터 형 인덱스에는 클러스터 형 인덱스 키 열과 인덱스 키 열의 모든 데이터를 가지고 있습니다.

  • 클러스터 형 인덱스의 키 열은 거의 변하지 않습니다. (힙의 RID는 변할 가능성이 높습니다.)

  • 999개 까지 만들 수 있습니다.

  • Key Lookup : 비 클러스터 형 인덱스가 가지지 못한 데이터를 찾아가는 과정입니다.

 

 

 

결론

아래 그림으로 한방에 정리됩니다. 테이블은 클러스터 형 인덱스가 잡혀있는 테이블과 아닌 테이블로 구성되어 진다는 것을 알 수 있습니다.

그리고 비 클러스터 형 인덱스가 결합되어지면, 힙에는 RID 값을 가지고 있고 클러스터 형 인덱스는 키 값을 가지고 있습니다. 비 클러스터 형 인덱스에서 힙으로 검색될 때에는 RID Lookup, 클러스터 형 인덱스에 검색 되어 질 때에는 Key Loopup 이라는 용어가 사용되어 집니다.

현업에서 RID Lookup 으로 검색되어지는 테이블이 있다면, 그 테이블이 왜 힙을 사용하고 있는지 원인 분석이 필요한 것으로 보여집니다.

 

 

 

'Database > SQL Server' 카테고리의 다른 글

온라인 인덱스 구성(Online Index)  (0) 2020.08.27
MSSQL TEMPDB DBFILE 삭제  (0) 2020.08.27
쿼리 저장소 (Query Store)  (0) 2020.08.27
모든 DB의 로그 잘림 확인하기  (0) 2020.08.27
VIEW에 있는 WITH SCHEMABINDING  (0) 2020.08.27
-- <<관련 Command >> 
-- Query Store 설정 확인 
SELECT actual_state, actual_state_desc, readonly_reason, current_storage_size_mb, max_storage_size_mb 
FROM sys.database_query_store_options; 
-- 실행 계획 확인 
SELECT Txt.query_text_id, Txt.query_sql_text, Pl.plan_id, Qry.* 
FROM sys.query_store_plan AS Pl JOIN sys.query_store_query AS Qry ON Pl.query_id = Qry.query_id 
JOIN sys.query_store_query_text AS Txt ON Qry.query_text_id = Txt.query_text_id ; 
--  Query Store 데이터의 메모리 내 부분을 디스크로 플러시합니다. 
EXEC sp_query_store_flush_db; 
-- 쿼리를 삭제하고 쿼리 저장소에서 관련된 모든 계획과 런타임 통계를 제거합니다. ( Clears the runtime stats for a specific query plan from the query store ) 
EXEC sp_query_store_remove_query 3; 
-- 쿼리 저장소에서 실행계획에 대한 실행 통계 삭제  ( Clears the runtime stats for a specific query plan from the query store ) 
EXEC sp_query_store_reset_exec_stats 3; 
-- 쿼리 저장소에서 단일 계획을 제거합니다. 
EXEC sp_query_store_remove_plan 3; 
-- 특정 쿼리에 대한 특정 계획을 강제 실행합니다. 
-- sp_query_store_force_plan 을 사용하면 쿼리 저장소에서 기록한 계획 만 해당 쿼리의 계획으로 강제 실행할 수 있습니다. 
-- 즉, 쿼리에 사용할 수있는 유일한 계획은 쿼리 저장소가 활성화되어있는 동안 해당 쿼리를 실행하는 데 이미 사용 된 계획뿐입니다. 
-- QUERY_ID , PLAN_ID 순서 
EXEC sp_query_store_force_plan 3, 3; 
-- 특정 쿼리에 대해 특정 PLAN 설정 해제 하기  ( Enables unforcing a particular plan for a particular query ) 
-- QUERY_ID , PLAN_ID 순서 
EXEC sp_query_store_unforce_plan 3, 3; 
-- 실행 계획 확인 
SELECT Txt.query_text_id, Txt.query_sql_text, Pl.plan_id, Qry.* 
FROM sys.query_store_plan AS Pl JOIN sys.query_store_query AS Qry ON Pl.query_id = Qry.query_id 
JOIN sys.query_store_query_text AS Txt ON Qry.query_text_id = Txt.query_text_id ; 
ALTER DATABASE TGTEST SET QUERY_STORE (INTERVAL_LENGTH_MINUTES = 30); 
ALTER DATABASE TGTEST SET QUERY_STORE (MAX_STORAGE_SIZE_MB = 200); 
ALTER DATABASE TGTEST SET QUERY_STORE 
( OPERATION_MODE = READ_WRITE, CLEANUP_POLICY = (STALE_QUERY_THRESHOLD_DAYS = 30) 
, DATA_FLUSH_INTERVAL_SECONDS = 300 
, MAX_STORAGE_SIZE_MB = 500 
, INTERVAL_LENGTH_MINUTES = 15 
, SIZE_BASED_CLEANUP_MODE = AUTO 
, QUERY_CAPTURE_MODE = AUTO 
, MAX_PLANS_PER_QUERY = 1000 
, WAIT_STATS_CAPTURE_MODE = ON ); 
SELECT * FROM sys.database_query_store_options; 
-- 수행 횟수가 2보다 작고, 마지막 수행이 24전 보다 오랜 쿼리를 QUERY STORE 에서 삭제하기 
DECLARE @id int 
DECLARE adhoc_queries_cursor 
  CURSOR FOR SELECT q.query_id 
  FROM sys.query_store_query_text AS qt JOIN sys.query_store_query AS q 
  ON q.query_text_id = qt.query_text_id JOIN sys.query_store_plan AS p 
  ON p.query_id = q.query_id JOIN sys.query_store_runtime_stats AS rs 
  ON rs.plan_id = p.plan_id 
  GROUP BY q.query_id 
  HAVING SUM(rs.count_executions) < 2  -- 수행 횟수가 2보다 작고 
  AND MAX(rs.last_execution_time) < DATEADD (hour, -24, GETUTCDATE())  -- 마지막 수행 시간이 24간 이전 
  ORDER BY q.query_id ; 
OPEN adhoc_queries_cursor ; 
FETCH NEXT FROM adhoc_queries_cursor INTO @id; 
WHILE @@fetch_status = 0 
BEGIN PRINT @id EXEC sp_query_store_remove_query @id 
FETCH NEXT FROM adhoc_queries_cursor INTO @id 
END 
CLOSE adhoc_queries_cursor ; 
DEALLOCATE adhoc_queries_cursor; 
-- 가장 최근까지 수행 쿼리 
SELECT TOP 10 qt.query_sql_text, q.query_id, qt.query_text_id, p.plan_id, rs.last_execution_time 
FROM sys.query_store_query_text AS qt JOIN sys.query_store_query AS q 
ON qt.query_text_id = q.query_text_id JOIN sys.query_store_plan AS p 
ON q.query_id = p.query_id JOIN sys.query_store_runtime_stats AS rs 
ON p.plan_id = rs.plan_id 
ORDER BY rs.last_execution_time DESC; 
-- 자주 수행되는 쿼리 
SELECT q.query_id, qt.query_text_id, qt.query_sql_text, SUM(rs.count_executions) AS total_execution_count 
FROM sys.query_store_query_text AS qt JOIN sys.query_store_query AS q 
ON qt.query_text_id = q.query_text_id JOIN sys.query_store_plan AS p 
ON q.query_id = p.query_id JOIN sys.query_store_runtime_stats AS rs 
ON p.plan_id = rs.plan_id 
GROUP BY q.query_id, qt.query_text_id, qt.query_sql_text 
ORDER BY total_execution_count DESC; 
-- 최근 1시간 동안 평균 수행 시간이 가장 오래 걸린 쿼리 
SELECT TOP 10 rs.avg_duration, qt.query_sql_text, q.query_id, qt.query_text_id, p.plan_id 
      , GETUTCDATE() AS CurrentUTCTime, rs.last_execution_time 
FROM sys.query_store_query_text AS qt JOIN sys.query_store_query AS q 
ON qt.query_text_id = q.query_text_id JOIN sys.query_store_plan AS p 
ON q.query_id = p.query_id JOIN sys.query_store_runtime_stats AS rs 
ON p.plan_id = rs.plan_id 
WHERE rs.last_execution_time > DATEADD(hour, -1, GETUTCDATE()) 
ORDER BY rs.avg_duration DESC; 
-- 최근 24시간 내에 IO가 가장 높은 쿼리 10 
SELECT TOP 10 rs.avg_physical_io_reads, qt.query_sql_text, q.query_id, qt.query_text_id, p.plan_id 
  , rs.runtime_stats_id, rsi.start_time, rsi.end_time, rs.avg_rowcount, rs.count_executions 
FROM sys.query_store_query_text AS qt JOIN sys.query_store_query AS q 
ON qt.query_text_id = q.query_text_id JOIN sys.query_store_plan AS p 
ON q.query_id = p.query_id JOIN sys.query_store_runtime_stats AS rs 
ON p.plan_id = rs.plan_id JOIN sys.query_store_runtime_stats_interval AS rsi 
ON rsi.runtime_stats_interval_id = rs.runtime_stats_interval_id 
WHERE rsi.start_time >= DATEADD(hour, -24, GETUTCDATE()) 
ORDER BY rs.avg_physical_io_reads DESC; 
-- 실행계획이 여러개인 쿼리 
WITH Query_MultPlans AS 
( SELECT COUNT(*) AS cnt, q.query_id 
  FROM sys.query_store_query_text AS qt JOIN sys.query_store_query AS q 
  ON qt.query_text_id = q.query_text_id JOIN sys.query_store_plan AS p 
  ON p.query_id = q.query_id 
  GROUP BY q.query_id 
  HAVING COUNT(distinct plan_id) > 1 ) 
SELECT q.query_id, object_name(object_id) AS ContainingObject, query_sql_text, plan_id 
  , p.query_plan AS plan_xml, p.last_compile_start_time, p.last_execution_time 
FROM Query_MultPlans AS qm JOIN sys.query_store_query AS q 
ON qm.query_id = q.query_id JOIN sys.query_store_plan AS p 
ON q.query_id = p.query_id JOIN sys.query_store_query_text qt 
ON qt.query_text_id = q.query_text_id 
ORDER BY query_id, plan_id; 
-- 실행계획 변경으로 성능 저하 쿼리 (  수행이 48 이내,  실행계획이 변경되며, 평균 수행 시간이 더 오래 걸린 쿼리 ) 
SELECT qt.query_sql_text, q.query_id, qt.query_text_id, rs1.runtime_stats_id AS runtime_stats_id_1 
    ,rsi1.start_time AS interval_1, p1.plan_id AS plan_1, rs1.avg_duration AS avg_duration_1 
    ,rs2.avg_duration AS avg_duration_2, p2.plan_id AS plan_2, rsi2.start_time AS interval_2 
    , rs2.runtime_stats_id AS runtime_stats_id_2 
FROM sys.query_store_query_text AS qt JOIN sys.query_store_query AS q 
ON qt.query_text_id = q.query_text_id JOIN sys.query_store_plan AS p1 
ON q.query_id = p1.query_id JOIN sys.query_store_runtime_stats AS rs1 
ON p1.plan_id = rs1.plan_id JOIN sys.query_store_runtime_stats_interval AS rsi1 
ON rsi1.runtime_stats_interval_id = rs1.runtime_stats_interval_id JOIN sys.query_store_plan AS p2 
ON q.query_id = p2.query_id JOIN sys.query_store_runtime_stats AS rs2 
ON p2.plan_id = rs2.plan_id JOIN sys.query_store_runtime_stats_interval AS rsi2 
ON rsi2.runtime_stats_interval_id = rs2.runtime_stats_interval_id 
WHERE rsi1.start_time > DATEADD(hour, -48, GETUTCDATE()) 
AND rsi2.start_time > rsi1.start_time 
AND p1.plan_id      <> p2.plan_id 
AND rs2.avg_duration > 2*rs1.avg_duration 
ORDER BY q.query_id, rsi1.start_time, rsi2.start_time; 
-- 가장 오래 대기한 쿼리 
SELECT TOP 10 qt.query_text_id, q.query_id, p.plan_id, sum(total_query_wait_time_ms) AS sum_total_wait_ms 
FROM sys.query_store_wait_stats ws JOIN sys.query_store_plan p 
ON ws.plan_id = p.plan_id JOIN sys.query_store_query q 
ON p.query_id = q.query_id JOIN sys.query_store_query_text qt 
ON q.query_text_id = qt.query_text_id 
GROUP BY qt.query_text_id, q.query_id, p.plan_id 
ORDER BY sum_total_wait_ms DESC 
--- 최근 1시간, 24시간 쿼리 성능 비교 
--- "Recent" workload - last 1 hour 
DECLARE @recent_start_time datetimeoffset; 
DECLARE @recent_end_time datetimeoffset; 
SET @recent_start_time = DATEADD(hour, -1, SYSUTCDATETIME());  -- 1시간전 
SET @recent_end_time = SYSUTCDATETIME(); 
--- "History" workload 
DECLARE @history_start_time datetimeoffset; 
DECLARE @history_end_time datetimeoffset; 
SET @history_start_time = DATEADD(hour, -24, SYSUTCDATETIME()); -- 24시간전 
SET @history_end_time = SYSUTCDATETIME(); 
WITH hist AS 
( SELECT p.query_id query_id, CONVERT(float, SUM(rs.avg_duration*rs.count_executions)) total_duration 
    , SUM(rs.count_executions) count_executions 
    , COUNT(distinct p.plan_id) num_plans 
    FROM sys.query_store_runtime_stats AS rs JOIN sys.query_store_plan p 
    ON p.plan_id = rs.plan_id 
    WHERE (rs.first_execution_time >= @history_start_time AND rs.last_execution_time < @history_end_time) 
      OR (rs.first_execution_time <= @history_start_time AND rs.last_execution_time > @history_start_time) 
      OR (rs.first_execution_time <= @history_end_time AND rs.last_execution_time > @history_end_time) 
    GROUP BY p.query_id ) 
, recent AS 
  ( SELECT p.query_id query_id, CONVERT(float, SUM(rs.avg_duration*rs.count_executions)) total_duration 
  , SUM(rs.count_executions) count_executions, COUNT(distinct p.plan_id) num_plans 
  FROM sys.query_store_runtime_stats AS rs JOIN sys.query_store_plan p 
  ON p.plan_id = rs.plan_id 
  WHERE (rs.first_execution_time >= @recent_start_time AND rs.last_execution_time < @recent_end_time) 
  OR (rs.first_execution_time <= @recent_start_time AND rs.last_execution_time > @recent_start_time) 
  OR (rs.first_execution_time <= @recent_end_time AND rs.last_execution_time > @recent_end_time) 
  GROUP BY p.query_id ) 
SELECT results.query_id query_id, results.query_text query_text 
, results.additional_duration_workload additional_duration_workload 
, results.total_duration_recent total_duration_recent 
, results.total_duration_hist total_duration_hist 
, ISNULL(results.count_executions_recent, 0) count_executions_recent 
, ISNULL(results.count_executions_hist, 0) count_executions_hist 
FROM ( SELECT hist.query_id query_id, qt.query_sql_text query_text 
    , ROUND(CONVERT(float, recent.total_duration/ recent.count_executions-hist.total_duration/hist.count_executions) *(recent.count_executions), 2) AS additional_duration_workload, ROUND(recent.total_duration, 2) total_duration_recent 
    , ROUND(hist.total_duration, 2) total_duration_hist, recent.count_executions count_executions_recent, hist.count_executions count_executions_hist 
    FROM hist JOIN recent ON hist.query_id = recent.query_id JOIN sys.query_store_query AS q 
    ON q.query_id = hist.query_id JOIN sys.query_store_query_text AS qt 
    ON q.query_text_id = qt.query_text_id ) AS results 
    WHERE additional_duration_workload > 0 
    ORDER BY additional_duration_workload DESC OPTION (MERGE JOIN); 

'Database > SQL Server' 카테고리의 다른 글

MSSQL TEMPDB DBFILE 삭제  (0) 2020.08.27
인덱스  (0) 2020.08.27
모든 DB의 로그 잘림 확인하기  (0) 2020.08.27
VIEW에 있는 WITH SCHEMABINDING  (0) 2020.08.27
SP 검수 시 체크해야 할 리스트  (0) 2020.08.27

+ Recent posts