2019년 1월 30일 수요일

[TOY PROJECT] 야근/대휴 관리 시스템 개발 - 데이터베이스 설계 편

토이 프로젝트로 야근이나 추가근무를 하면 그 시간만큼 대체 휴무를 제공하는 시스템을 구축해보자. 소수(10명내외)가 사용하는 시스템이라고 가정하고 설계부터 개발까지 10시간 내외면 만들어낼 수 있기 때문에 퀄리티는 보장하지 않기로 한다. 하지만 DB부터 웹서버, 화면까지 생각해본다는 점에서 프로젝트 성으로 진행해보기로 하자. 나는 괜찮으면 팀 내에서 사용할 수 있도록 공유할 생각이다.

20190608 깃헙주소 추가 https://github.com/parksuseong/toy_time_manage

1. 화면편
[TOY PROJECT] 야근/대휴 관리 시스템 개발 - 반응형 화면 설계 편( bootstrap , jqgrid, get/post 호출 )
2. 서버편
[TOY PROJECT] 야근/대휴 관리 시스템 개발 - 서버 설계 편( express 사용, mssql 연동, get/post 처리 )
3. 데이터베이스 편
- [TOY PROJECT] 야근/대휴 관리 시스템 개발 - 데이터베이스 설계 편

먼저 데이터베이스를 설계해보도록 하자. Node js를 활용하기 위해 자료 서칭을 하다보니 몽고DB와 mysql 예제가 많았지만 나는 프로시저를 활용하기 위해(?) MSSQL을 사용하기로 하였다.


테이블과 프로시저 스키마는 다음처럼 생각을 했다. 시간은 분단위로 관리한다.
1. 야근 기록 대장 테이블
2. 야근 등록 프로시저
3. 야근 사용기록 조회 프로시저
4. 대체휴무 기록 대장 테이블
5. 대체휴무 등록 프로시저
6. 대체휴무 사용기록 조회 프로시저
7. 잔여 대체휴무 조회 프로시저



이렇게 하면 1~3번과 4~6번은 거의 비슷하게 설계할 수 있기때문에 리소스를 줄일 수 있으며 7번 잔여 대체휴무 조회 프로시저에서 머리를 조금 쓰면 될 것 같았다.



1. 야근 기록 대장 테이블과 대체휴무 기록 대장 테이블을 생성한다.

-- 야근 기록 대장 테이블
CREATE TABLE OVERTIME_WORK_LOG(
USER_ID VARCHAR(6) NOT NULL, -- 사용자 ID
FR_DTS DATETIME2(0) NOT NULL, -- 시작일시
TO_DTS DATETIME2(0) NOT NULL, -- 종료일시
RSN VARCHAR(100) NOT NULL, -- 사유
GEN_TIME INT NOT NULL, -- 근무 시간
REG_DTS DATETIME2(0) NOT NULL, -- 등록 일시
CONSTRAINT PK_OVERTIME_WORK_LOG PRIMARY KEY (USER_ID,FR_DTS)
) ON [PRIMARY]
GO
-- 대휴 기록 대장 테이블
CREATE TABLE TIME_OFF_LOG(
USER_ID VARCHAR(6) NOT NULL, -- 사용자 ID
FR_DTS DATETIME2(0) NOT NULL, -- 시작일시
TO_DTS DATETIME2(0) NOT NULL, -- 종료일시
USE_TIME INT NOT NULL, -- 대휴 사용 시간
REG_DTS DATETIME2(0) NOT NULL, -- 등록 일시
CONSTRAINT PK_TIME_OFF_LOG PRIMARY KEY (USER_ID,FR_DTS)
) ON [PRIMARY]


이 두 테이블은 야근과 대휴를 기록하는 HISTORY 성 테이블이다.
사실 야근 기록 대장 테이블에 대휴 사용 유무(소진 플래그)를 넣을까 고민도 했었지만 굳이 화면을 잘 설계하면 필요가 없어보였다. 또한 만약 소진 플래그 컬럼을 넣는다면 히스토리성 팩트 테이블이 업데이트가 일어난다는 점이 찝찝하고 그렇다고 테이블을 하나 더 만들어 분리하자니 관리 포인트가 늘어나므로 PASS하기로 하였다.




2. 야근 등록 프로시저와 대휴 등록 프로시저를 만든다.

-- 먼저 야근 등록프로시저
CREATE PROC dbo.USP_INSERT_OVERTIME_WORK_LOG
(
    @USER_ID VARCHAR(6) = '',
@FR_DT VARCHAR(20) = '',
@TO_DT VARCHAR(20) = '',
@RSN   VARCHAR(100) = ''
)
AS
INSERT INTO dbo.OVERTIME_WORK_LOG 
(  USER_ID, FR_DTS, TO_DTS, RSN, GEN_TIME, REG_DTS)
VALUES(
 @USER_ID
,CONVERT(DATETIME,@FR_DT,120)
,CONVERT(DATETIME,@TO_DT,120)
,@RSN
,DATEDIFF(MINUTE,CONVERT(DATETIME,@FR_DT,120),CONVERT(DATETIME,@TO_DT,120))
,GETDATE()
);

-- 대휴 등록 프로시저
CREATE PROC dbo.USP_INSERT_TIME_OFF_LOG
(
    @USER_ID VARCHAR(6) = '',
    @FR_DT VARCHAR(20) = '',
    @TO_DT VARCHAR(20) = ''
)
AS
INSERT INTO dbo.TIME_OFF_LOG 
( USER_ID, FR_DTS, TO_DTS, USE_TIME, REG_DTS )
VALUES(
 @USER_ID
,CONVERT(DATETIME,@FR_DT,120)
,CONVERT(DATETIME,@TO_DT,120)
,DATEDIFF(MINUTE,CONVERT(DATETIME,@FR_DT,120),CONVERT(DATETIME,@TO_DT,120))
,GETDATE()
);


3. 야근 사용기록과 대휴 사용기록을 조회하는 프로시저를 만든다
-- 먼저 야근 기록을 조회하는 프로시저
CREATE PROC dbo.USPS_SELECT_OVERTIME_WORK_LOG
(
    @USER_ID VARCHAR(6) = ''
)
AS
SELECT USER_ID
, CONVERT(VARCHAR,FR_DTS) AS FR_DTS
, CONVERT(VARCHAR,TO_DTS) AS TO_DTS
, RSN
, GEN_TIME
, CONVERT(VARCHAR,REG_DTS) AS REG_DTS
FROM OVERTIME_WORK_LOG
WHERE USER_ID = @USER_ID 


-- 대휴 기록을 조회하는 프로시저
CREATE PROC dbo.USPS_SELECT_TIME_OFF_LOG
(
    @USER_ID VARCHAR(6) = ''
)
AS
SELECT
  USER_ID
, CONVERT(VARCHAR,FR_DTS) AS FR_DTS
, CONVERT(VARCHAR,TO_DTS) AS TO_DTS
, USE_TIME
, CONVERT(VARCHAR,REG_DTS) AS REG_DTS
FROM TIME_OFF_LOG
WHERE USER_ID = @USER_ID



4. 잔여 대휴 시간 조회 
-- 근무 외 시간(새벽 등)에 발생한 시간에 대해서는 1.5배 적용을 한다면 프로시저 마지막에 SELECT 할 때 * 1.5를 해주면 된다.
CREATE PROC dbo.USPS_SELECT_SPARE_TIME_OFF
( @USER_ID VARCHAR(6) = '' )
AS

DECLARE @WORK_SUM BIGINT
DECLARE @OFF_SUM BIGINT

SELECT @WORK_SUM = ISNULL(SUM(GEN_TIME),0)
FROM OVERTIME_WORK_LOG
WHERE USER_ID = @USER_ID

SELECT @OFF_SUM = ISNULL(SUM(USE_TIME),0)
FROM TIME_OFF_LOG
WHERE USER_ID = @USER_ID

SELECT @USER_ID AS USER_ID
, @WORK_SUM - @OFF_SUM AS SPARE_MINUTE
, (@WORK_SUM - @OFF_SUM)/60. AS SPARE_HOUR


날짜 같은 경우는 DB에서 VARCHAR 형식으로 미리 바꿔서 넘겨주도록 하였다.


스크립트는 아래의 경로에 남겨놓았다.
https://drive.google.com/open?id=1mestYmeMhxOX_t90PDuHtdZ5B_vbZ5Cr

2019년 1월 29일 화요일

[TOY PROJECT] 야근/대휴 관리 시스템 개발 - 서버 설계 편( express 사용, mssql 연동, get/post 처리 )

야근/대휴 관리 시스템을 개발하기 위해 화면, DB에 이어 서버 차례가 왔다.
서버는 Node js를 통해 돌릴 것이며 로그를 남기는 등의 기능은 추후에 추가하기로 하자.
이번 편에서는 간단하게 DB를 연동하고 서버와 클라이언트(화면)간에 데이터를 주고 받는 get/post 처리를 해보도록 하자.

20190608 깃헙주소 추가 https://github.com/parksuseong/toy_time_manage

1. 화면편
[TOY PROJECT] 야근/대휴 관리 시스템 개발 - 반응형 화면 설계 편( bootstrap , jqgrid, get/post 호출 )
2. 서버편
[TOY PROJECT] 야근/대휴 관리 시스템 개발 - 서버 설계 편( express 사용, mssql 연동, get/post 처리 )
3. 데이터베이스 편
- [TOY PROJECT] 야근/대휴 관리 시스템 개발 - 데이터베이스 설계 편

먼저 node js 의 대표적인 프레임워크인 express를 활용하고 DB는 MSSQL을 사용해보자.

server.js는 간단하게 구현할 수 있다.


express를 활용하고 DB는 Tedious를 써서 연동한다. 참고로 Tedious는 Microsoft의 SQL Server 인스턴스와 상호 작용하는 데 사용되는 TDS 프로토콜 구현을 제공 하는 Node 패키지라고 한다. 

config에 mssql 정보(계정,비번,서버,옵션)을 미리 작성해주었다. 참고로 node js에서 MSSQL을 연동하려면 server에 ip나 hostname을 적어주어야하는데 이 부분때문에 미리 mssql 구성관리자에서 ip나 별칭으로 접속할 수 있도록 세팅해주어야 한다.
(블로그에 ssms에서 별칭, ip로 접속하는 방법을 포스팅했었다.)


1. DB에 데이터 INSERT 하기
먼저 사용자가 입력한 값으로 DB 정보를 수정하는 기능을 만들건데 이 부분은 post로 작성했다.
화면에서는 다음처럼 넘겼다.
url : 'http://127.0.0.1:8888/add_work'
그리고 서버에서는 다음처럼 처리하였다.

화면상에서 추가 근무를 등록하면 user_id와 시작/종료 시간, 그리고 사유를 서버로 보내는데 서버에서는 아래처럼 받아서 프로시저를 호출하여 처리하였다.
app.post("/add_work", function(request, response){
    EXE_USP_DATATEAM_INSERT_OVERTIME_WORK_LOG(request.body.user_id, request.body.fr_dt, request.body.to_dt, request.body.reason);
    response.end('입력이 성공하였습니다');
});

2. DB에서 데이터 SELECT 하기
두번째로 화면에서 조회를 클릭하면 DB에 저장된 정보를 읽어와서 화면에 보내주는 것은 get으로 처리하였다.

화면에서 넘길 때는 다음처럼 넘겼다.
url : 'http://127.0.0.1:8888/search_work?user_id='+$('#search_user_id').val(),
그리고 서버에서는 다음처럼 처리하였다.

조회 결과를 rows에 담아 화면에 넘기면 화면에서는 그리드로 처리한다.

추가근무 등록/조회 용으로 위에서 설명한 서버 상태를 바꾸는 post, 데이터를 가져오는 get을 똑같이 하나 더 만들어주면 작업이 끝난다.

전체 코드는 아래 경로에 올려놓았다.
https://drive.google.com/open?id=1mestYmeMhxOX_t90PDuHtdZ5B_vbZ5Cr

2019년 1월 28일 월요일

[TOY PROJECT] 야근/대휴 관리 시스템 개발 - 반응형 화면 설계 편( bootstrap , jqgrid, get/post 호출 )


TOY PROJECT 추가근무/대휴 관리 시스템 개발 화면 설계 편이다.
대략 화면은 아래처럼 간단하게 만들되 jqgrid, bootstrap을 사용해보자.

20190608 깃헙주소 추가 https://github.com/parksuseong/toy_time_manage

1. 화면편
[TOY PROJECT] 야근/대휴 관리 시스템 개발 - 반응형 화면 설계 편( bootstrap , jqgrid, get/post 호출 )
2. 서버편
[TOY PROJECT] 야근/대휴 관리 시스템 개발 - 서버 설계 편( express 사용, mssql 연동, get/post 처리 )
3. 데이터베이스 편
- [TOY PROJECT] 야근/대휴 관리 시스템 개발 - 데이터베이스 설계 편

결과 파일은 index.html 파일이다.


대략 완성된 화면은 다음과 같다.
1. 사용자, 시작/종료 시간, 야근 사유를 등록할 수 있다.
2. 사용자를 조회하면 야근 기록을 조회할 수 있다.
3. 대체휴무 등록할 수 있다. (사용자, 시작/종료 시간 입력)
4. 대체휴무 사용 기록을 조회할 수 있다.
5. 잔여 대체휴무 시간을 조회할 수 있다.

그리고 아래와 같이 화면(그리드 포함)은 반응형으로 동작한다. 


위와 같은 형태로 만들기 위해 화면(index.html)을 그려보도록 하자.
먼저 필요한 소스들을 import 한다.
그리드를 그리기 위해 jqgrid 관련 source, jquery를 사용하기 위해 jquery, bootstrap을 사용하기 위해 관련 source를 import한다.

참고로 jqgrid로 그리드를 그렸을 때 페이지를 넘겨주는 버튼인 prev pager, next pager, first pager, last pager의 아이콘이 나오지 않았다. 이게 jqgrid 안에서의 문제인줄 알고 이것 저것 import를 해보다가 결국 <link rel="stylesheet" type="text/css" media= "screen" href="./jquery-ui-1.12.1/jquery-ui.css" /> 를 등록해줬더니 버튼이 나왔다.


그리고 공통적인 속성을 지정해주었다.

그리고 body 부분에서 위처럼 크게 야근용, 대휴용 구역을 만들어 그려준다.

전체 소스파일은 글 끝에 첨부해놓았다.

대략적으로 아래와 같은 형태로 html을 꾸민다.
<body>
        <div class="jumbotron" style="padding:10px">
          <div class="container">
            <h2>[야근/대휴] 관리 시스템 Ver1.0</h2>
          </div>
        </div>

        <div class="container">
          <h3 class="m_title">추가 근무 등록하기</h3>

          <!---->
          <div class="row">
            <div class="col-md-3">
              <div class="form-group">
                  <label for="add_user_id">사용자</label>
                  <input type="text" id='add_user_id' name="user_id" class="form-control c_work" value="">
              </div>
            </div>

            <div class="col-md-9">
              <div class="form-group">
                <label for="add_user_id">사유</label>
                <input type="text" id='add_because' class="form-control c_work" name="because" placeholder="예) 배치 장애 해결을 위한 접속">
              </div>
            </div>
          </div>

          <div class="row">
            <div class="col-md-5">
              <div class="form-group">
                <label for="work_time">시작 시간</label>
                <div class="row">
                  <div class="col-md-6">
                    <input type="date" id="add_fr_dt" class="form-control c_work" name="fr_dt" min="2019-01-01" value="2019-01-01" >
                  </div>
                  <div class="col-md-6">
                    <input type="time" id="add_fr_time" class="form-control c_work" name="fr_time" min="00:00" max="23:59" value="00:00">
                  </div>
                </div>
              </div>
            </div>

            <div class="col-md-5">
              <div class="form-group">
                <label for="work_time">종료 시간</label>
                <div class="row">
                  <div class="col-md-6">
                    <input type="date" id="add_to_dt" class="form-control c_work" name="fr_dt" min="2019-01-01" value="2019-01-01" >
                  </div>
                  <div class="col-md-6">
                    <input type="time" id="add_to_time" class="form-control c_work" name="to_time" min="00:00" max="23:59" value="00:00">
                  </div>
                </div>
              </div>
            </div>

            <div class="col-md-2">
                <label for="">&nbsp;</label>
                <input type="button" id="add_button" class="form-control btn btn-success" value="등록하기" onclick="add_Click();">
            </div>
          </div>


          <div class="form-group">
            <h4 class="m_title">접속 기록 조회</h4>
            <div class="row">
              <div class="col-md-3">
                <input type="text" id='search_user_id' class="form-control c_work" name="user_id" value="161563">
              </div>
              <div class="col-md-2">
                <input type="button" id="search_button" class="form-control btn btn-info" value="조회하기" onclick="search_Click();"> <br><br>
              </div>
            </div>
          </div>
          <div class="grid_area">
          <table id="grid"></table>
          <div id="pager"></div>
          </div>
</body>


다음은 스크립트(로직) 부분이다. 필요한 변수 값을 적당히 세팅해준다.
document.getElementById('add_fr_dt').valueAsDate = new Date();
document.getElementById('add_to_dt').valueAsDate = new Date();
document.getElementById('add_fr_dt_2').valueAsDate = new Date();
document.getElementById('add_to_dt_2').valueAsDate = new Date();
var $Grid = {};
var $Grid2 = {};
var $Grid3 = {};

그리고 add_Click() 함수는 등록 버튼을 누르면 호출되는 함수이다.
post 방식이며 서버에 user_id, add_fr_dt, add_to_dt, add_because 총 4개의 파라메터를 넘긴다.

다음은 조회하기 버튼을 클릭하면 호출되는 search_Click() 함수이다.
gridView()에서는 get방식으로 서버에 호출하며 넘기는 파라메터는 user_id 한개이다.


참고로 grid부분에서 autowidth : true를 사용하면 컬럼의 길이에 따라 자동으로 늘려주고 화면을 줄일 때 그리드가 반응형으로 그려지는 부분은 window.on 부분에 처리하면된다.



그리드를 조회하면 다음과 같다.


index.html 파일과 jqgrid, bootstrap, jquery 소스는 아래 경로에 첨부되어있다.

2019년 1월 14일 월요일

SSMS에서 127.0.0.1(localhost) 혹은 별칭으로 접속하기

MSSQL에는 저장프로시저가 아주 강력하고 문법 자체도 편하기(?) 때문에 토이프로젝트를 진행할 때 DB를 MSSQL을 주로 사용한다.


본인 노트북, 혹은 데스크탑에 MSSQL을 설치하고 SSMS로 접속을 하려고 할 때 서버이름에 localhost, 혹은 (local), 127.0.0.1 혹은 서버별칭을 써서 접속을 하려면 설정을 해주어야한다.

먼저 localhost, 혹은 (local), 127.0.0.1로 접속을 하는 방법은 다음과 같다.

위의 캡쳐 화면처럼 구성 관리자에서 SQL Server 네트워크 구성 -> TCP/IP 속성 -> IPALL 부분에서 TCP 포트 부분에 포트 번호(기본 1433)을 입력해주어야 한다.
그리고 SQL Server 엔진을 재시작하면 반영이 된다.


위처럼 이제 서버 이름에 localhost을 쳐서 접속이 가능하다.


다음으로 서버 이름에 별칭을 써서 접속하는 방법은 다음과 같다.

마찬가지로 구성 관리자에서 SQL Native Client 구성에서 별칭을 등록해주면 된다.
(32비트는 별도로 구분되어 있다.)

구성 관리자 ->  SQL Native Client 구성 -> 새별칭에서 별칭과 서버를 등록해주면 된다.


이제 서버 이름에 별칭을 써서 접속을 할 수 있다.

별칭이나 IP를 쓰고 접속해야 편하기 때문에 DB를 설치하면 두 설정 모두 꼭 하는 작업이라고 할 수 있다. 

2019년 1월 13일 일요일

MSSQL 복원에 대하여

저번 편 MSSQL 백업에 대하여에 이어 이번편은 MSSQL 복원편을 다뤄보기로 한다.

MSSQL 복원 시에는 옵션이 3가지가 있다.

데이터베이스 복원 시 옵션은 3가지가 있다.
(새로운 서버에 복원할 때는 모든 옵션이 의미가 없다.)

1. 기존 데이터베이스 덮어쓰기(WITH REPLACE)
복원 대상 데이터베이스가 이미 존재한다면 강제로 덮어쓰므로 신중해야한다.

2. 복제 설정 유지(WITH KEEP_REPLICATION)
복제와 연관된 데이터베이스는 관련 설정을 그대로 유지하고 복제와 연관되지 않은 경우 무의미하다.

3. 복원된 데이터베이스에 대한 액세스 제한(WITH RESTRICTED_USER)
복원 후 일반 사용자가 접근할 수 없도록 제한한다.
관리자 권한의 계정만 접근 허용한다.
데이터베이스 속성에서 모든 사용자가 사용하게 설정할 수 있다.


데이터베이스 복원 시 복구 상태에는 3가지가 있다.
1. RESTORE WITH RECOVERY
커밋되지 않은 트랜잭션을 롤백하여 데이터베이스를 사용할 수 있는 상태로 유지한다.
추가 트랜잭션 로그를 복원할 수 없다.

2. RESOTRE WITH NORECOVERY
데이터베이스를 비작동 상태(복원 중 상태-OFFLINE)로 유지하고 커밋되지 않은 트랜잭션을 롤백하지 않는다. 추가 트랜잭션 로그를 복원할 수 있다.

3. RESOTRE WITH STANDBY
우선 데이터베이스를 읽기 전용 모드로 유지한다.
커밋되지 않은 트랜잭션은 롤백.
롤백된 트랜잭션의 내용을 별도의 대기 파일에 기록함.
추가 트랜잭션 로그를 복원할 수 있다.



1. 전체 백업만 하는 경우
데이터베이스에 문제가 발생했하면 바로 tail-log backup을 받고 전체백업복원(NORECOVERY) 후 로그백업복원(RECOVERY)을 한다. 하지만 데이터베이스가 단순 복구 모델인 경우 tail-log backup을 받을 수 없어 가장 마지막 전체 백업만 복원할 수 있다.

2. 전체 백업 + 로그 백업을 하는 경우
우선 tail-log backup를 받는다. 이후 가장 마지막 전체 백업복원(NORECOVERY)을 하고 이 전체 백업 이후의 모든 트랜잭션 로그 백업을 복원(NORECOVERY)한다. 마지막으로 로그백업복원(RECOVERY)을 하면 데이터베이스를 완벽하게 복원할 수 있다.

3. 전체백업 + 차등백업 + 로그 백업
일단 tail-log backup을 받는다. 그리고 가장 마지막 전체 백업을 복원(NORECOVERY)하고, 이 전체 백업 이후의 가장 마지막 차등 백업을 복원(NORECOVERY)한다. 이어서 이 차등 백업 이후의 모든 로그 백업을 복원(NORECOVERY)하고 마지막으로 로그백업복원(RECOVERY)을 한다.

계속해서 복원할 백업 파일이 있으면 NORECOVERY로 복원하고 마지막 복원에는 RECOVERY로 복원한다. NORECOVERY상태인 경우 복원 중인 DB이므로 사용자는 접근할 수 없다.


USE master
GO
ALTER DATABASE TESTDB01
   SET SINGLE_USER WITH ROLLBACK IMMEDIATE
-- 웬만하면 사용하지 말도록 하자. 너무 위험하다. 쿼리창을 실수로 닫아버린다면.. 모르겠다.
GO


RESTORE DATABASE TESTDB01
    FROM DISK = 'F:\SQLBackup\TESTDB01_20170703_FULL.bak'
    WITH REPLACE, NORECOVERY, STATS = 10
GO

RESTORE LOG TESTDB01
    FROM DISK = 'F:\SQLBackup\TESTDB01_20170703_LOG.trn'
    WITH REPLACE, RECOVERY, STATS = 10
GO


ALTER DATABASE  TESTDB01
   SET MULTI_USER
GO


※ 꼭 기억하고 있자.
백업 파일의 위치, 복원에 소요되는 시간 예측, 복원 완료 여부 확인. 참고로 복원에 대한 시간도 중요하지만 백업 파일을 가져오는데 소요되는 시간도 복원 시간에 포함하여야 하므로 특히 파일이 큰 경우 수시간이 걸릴 수 있다는 점을 알고있어야 한다.

비상 로그 백업
'복원 전 비상 로그 백업 수행' 옵션은 복원 대상 데이터베이스에 백업되지 않은 트랜잭션 로그가 있을 경우 자동으로 로그를 백업받는 옵션이고 자동으로 선택된다.
SQL SERVER는 전체 복구 모델이나 대량 로그 복구 모델인 데이터베이스에 대해서는 MDF, NDF 파일이 손상됐더라도 트랜잭션 로그 파일이 물리적으로 손상되지만 않았다면 이를 백업받을 수 있다.


USE master
GO
BACKUP LOG TESTDB01
       TO DISK = 'F:\SQLBackup\TESTDB01_LOG_tail.trn'
       WITH CONTINUE_ALTER_ERROR
GO


--특정 시점으로 복원하기
USE master
GO
-- 전체 백업으로 복원
RESTORE DATABASE TESTDB01
    FROM DISK = 'F:\SQLBackup\TESTDB01_20170703_FULL.bak'
    WITH NORECOVERY, STATS = 10
GO

-- 마지막 차등 백업(있으면) 복원
RESTORE DATABASE TESTDB01
    FROM DISK = 'F:\SQLBackup\TESTDB01_20170703_DIFF.bak'
    WITH NORECOVERY, STATS = 10
GO


-- 이후 로그 백업(있으면) 복원
RESTORE LOG TESTDB01
    FROM DISK = 'F:\SQLBackup\TESTDB01_20170703_LOG.trn'
    WITH NORECOVERY, STATS = 10
GO

-- 특정 시점에서 복원 완료
RESTORE LOG TESTDB01
    FROM DISK = 'F:\SQLBackup\TESTDB01_LOG_tail.trn'
    WITH STOPAT = '2017-11-11 03:03:03'
GO

응급모드
데이터베이스가 손상되어 백업받을 수 없고 이전 백업도 없는 상황에서 현재 데이터를 급히 보고자할 때 응급 모드를 사용할 수 있다. 중요한 데이터를 이때 빠르게 다른데로 복사하자. 이전으로 돌아가려면 ONLINE 모드로 변경하면 된다.

USE master
GO
ALTER DATABASE TESTDB01
   SET EMERGENCY
GO

ALTER DATABASE TESTDB01
   SET ONLINE
GO


--백업 내역 조회하는 쿼리
USE msdb
GO
SELECT
        B.DATABASE_NAME AS 'DB'
      , CASE B.TYPE
          WHEN 'D' THEN N'FULL'
          WHEN 'L' THEN N'LOG'
          WHEN 'I' THEN N'DIF' END AS 'TYPE'
   , CONVERT(VARCHAR(20), B.BACKUP_START_DATE, 120) AS 'START'
   , CONVERT(VARCHAR(20), B.BACKUP_FINISH_DATE, 120) AS 'END'
   , CONVERT(DECIMAL(10,2), B.BACKUP_SIZE / 1024 / 1024) AS 'SIZE(MB)'
   , A.PHYSICAL_DEVICE_NAME AS 'FILE'
   , B.NAME AS 'NAME'
   , B.DESCRIPTION AS 'DESCRIPTION'
FROM BACKUPMEDIAFAMILY A
JOIN BACKUPSET                 B
                                       ON A.MEDIA_SET_ID = B.MEDIA_SET_ID
ORDER BY CONVERT(VARCHAR(20), B.BACKUP_START_DATE, 120) DESC


MSSQL 백업에 대하여

MSSQL은 여러 백업 방법을 제공하고 이를 활용하여 적절한 백업 및 복원 전략을 세워보자.


백업 옵션에는 다음과 같이 여러 가지가 있다. 
보통 1, 2, 3, 4 정도만 알아도 DB 운영에는 문제가 없어보인다.
1. full backup : 데이터베이스를 구성하는 모든 데이터 파일을 백업하고 백업이 진행되는 동안 기록된 트랜잭션 로그를 백업한다.
2. differential backup : 가장 마지막 전체 백업 이후에 변경된 데이터만을 백업한다.
(MSSQL은 내부적으로 풀백업 이후 변경된 데이터를 관리한다.)
3. transaction log backup : 트랜잭션 로그 파일을 백업하고 로그를 지운다.
4. tail-log backup : 현재 활성화된 로그, 즉 백업되지 않는 트랜잭션 로그를 백업한다.
5. file and file group backup : 특정 파일 또는 파일 그룹에 포함된 데이터 파일만을 백업한다. 복잡해서 많이 사용되지는 않는다.
6. partial backup : 파일 그룹 중에서 primary 파일 그룹과 읽기/쓰기 속성의 파일그룹, 그리고 명시적으로 지정된 읽기 전용의 파일 그룹만을 백업한다.
-데이터베이스가 읽기 전용 파일 그룹과 읽기와 쓰기가 가능한 파일 그룹이 있다면 전자는 자주 백업할 필요가 없다. partial backup은 파일 그룹 중에서 primary 파일 그룹과 읽기/쓰기가 가능한 파일 그룹을 백업한다. primary 파일 그룹만으로 구성된 DB는 partial backup이 의미가 없고, 또한 명시적으로 지정한 읽기 전용 파일그룹도 백업에 포함하여 백업할 수 있다.
7. copy only backup : 기존 백업의 흐름에 영향을 주지 않고 임시용으로 백업한다. 즉, 복사 전용 전체 백업은 이후의 differential backup에 영향을 주지 않으며, copy only backup은 이후의 로그 백업에 영향을 주지 않는다.
- 일반 적인 백업은 백업 시퀀스에 영향을 줘서 전체적인 복원 시나리오에 영향을 미친다. 하지만 copy only backup은 기존의 백업 시퀀스와 독립적으로 작용해서 특별한 용도로 백업을 수행해서 데이터 및 프로시저를 복원하는데 유용하게 사용할 수 있다. 풀백업하는데 소요되는 시간이 별로 안된다면 백업 시퀀스는 별거 아니겠지만 그게 아니라면 상당히 까다로울 수 있다.


시나리오...
∎매일 전체 백업
일월화수목금토 00시 - full backup

적합한 곳
1. 크기가 작은 데이터베이스
2. 개발이나 테스트용 데이터베이스
3. 데이터 변경이 거의 발생하지 않는 데이터베이스
4. 읽기 전용 데이터베이스
5. 단순 복구 모델 데이터베이스
6. 복구 시 데이터 유실이 크게 문제되지 않는 데이터베이스
7. 특정 시점 복원이 요구되지 않는 데이터베이스

※ 전체 복구 모델이나 대량 로그 복구 모델 데이터베이스에 데이터 변경이 많이 발생한다면 정기적으로 트랜잭션 로그를 지워주는 전략을 같이 수립해야한다.

∎전체 백업 + 차등 백업 
일요일 00시 - full backup
월화수목금토 00시 - differential backup

적합한 곳
1. 데이터베이스 크기가 커서 매일 전체 백업이 부담스러울 때
2. 단순 복구모델 데이터베이스
3. 특정 시점이 복구 안되도 상관 없는 데이터베이스
4. 복구 시 데이터 유실에 크게 상관없는 데이터베이스

※ 이 역시 전체 복구 모델이나 대량 로그 복구 모델 데이터베이스에 데이터 변경이 많이 발생한다면 정기적으로 트랜잭션 로그를 지워주는 전략을 같이 수립해야한다.


∎전체 백업 + 트랜잭션 로그 백업
일월화수목금토 00시 - full backup
일월화수목금토 06시, 12시, 18시 - transaction log backup

적합한 곳
1. 데이터베이스 크기가 커서 매일 전체 백업이 부담스러울 때
2. 데이터 변경이 많이 발생하여 트랜잭션 로그가 많이 쌓이는 데이터베이스
3. 특정 시점의 복원이 요구되는 데이터베이스

※ 트랜잭션 로그 백업은 기본적으로 트랜잭션 로그를 지워주므로 트랜잭션 로그 파일이 계속해서 증가하는 문제 해결. 특정 시점으로 복원할 수 있는 장점이 있어서 일반적으로 가장 많이 사용되는 백업 전략.


∎전체 백업 + 차등백업 + 트랜잭션 로그 백업
일 00시 - full backup
일 12시 - transaction log backup
월화수목금토 00시 - differential backup
월화수목금토 12시 - transaction log backup

적합한 곳
1. 데이터베이스 크기가 커서 매일 전체 백업이 부담스러울 때
2. 데이터 변경이 많이 발생하여 트랜잭션 로그가 많이 쌓이는 데이터베이스
3. 특정 시점의 복원이 요구되는 데이터베이스
4. 데이터베이스 복원 과정을 단순하게 가져가고 싶을 때

※ 차등 백업이 있을 때는 데이터베이스를 복원할 때 전체백업과 차등 백업 사이의 로그 백업을 복원할 필요가 없다. 그래서 좀 더 수월하다.




복구 모델(Recovery Model)
복구모델은 단순 < 대량로그 < 전체가 있으며 부등호 방향은 복원가능한 정도를 뜻한다.



∎전체(full)
전체 복구 모델을 선택하면 모든 데이터 변경에 대한 로그를 트랜잭션 로그 파일에 기록한다. 하지만 많은 로그가 쌓여서 정기적으로 트랜잭션 로그를 백업해서 로그를 지워야 한다.

∎대량 로그(bulk-logged)
데이터 변경 작업 중에서 BCP, BULK INSERT, INSERT INTO... SELECT와 같은 작업을 대량 로그 작업이라고 하고 CREATE INDEX, ALTER INDEX REBUILD와 같은 작업을 대 로그 인덱스 작업이라고 한다.
복구 모델을 전체로 하면 엄청난 로그를 남겨 작업이 느리므로 복구 모델을 대량 로그 복구 모델로 설정하면 대량 로그 작업으로 발생하는 트랜잭션 로그를 최소화한다. 하지만 특정 시점으로 복원할 수 없는 단점이 존재한다. 요즘은 디스크 성능이 좋아져서 대량 로그 복구 모델을 많이 사용하지는 않는다.

∎단순(simple)
트랜잭션 로그는 전체 복구 모델이나 대량 로그 복구 모델일 때는 백업해야 지워지지만, 단순 복구 모델이면 트랜잭션이 완료되면 자동으로 지워진다. 그래서 트랜잭션 로그 파일이 무작정 커지는 문제는 거의 발생하지 않는다고 보면된다. 하지만 트랜잭션 로그를 백업할 수 없어서 트랜잭션 로그를 사용한 다양한 복원 작업을 할 수 없다. 특정시점 복원이라던지.. 그래서 운영 데이터베이스에는 적합하지 않고 테스트용, 개발용 또는 읽기 전용의 데이터베이스에 대한 복구 모델로 적합하다.

-- 데이터베이스 복구 모델 변경
ALTER DATABASE TESTDB01
         SET RECOVERY FULL --전체
GO

ALTER DATABASE TESTDB02
         SET RECOVERY SIMPLE --단순
GO

ALTER DATABASE TESTDB03
         SET RECOVERY BULK_LOGGED --대량 로그
GO



데이터베이스 백업 방법

--전체백업
BACKUP DATABASE TESTDB01
       TO DISK = 'F:\SQLBackup\TESTDB01_20170703_FULL.bak'
       WITH NAME = N'TESTDB01- 전체 백업', COMPRESSION, STATS = 10
GO

NAME 옵션을 사용해서 백업에 대한 정보를 백업 파일의 헤더에 기록하고 STATS = 10을 지정해서 백업 진행 상황을 SSMS 쿼리 창에서 10% 간격으로 표시한다. 뭐 얼마나 걸리겠어?하며 사용하지 않는 경우가 많은데 이 옵션은 무조건 사용하자.

--차등 백업
BACKUP DATABASE TESTDB01
       TO DISK = 'F:\SQLBackup\TESTDB01_20170703_DIFF.bak'
       WITH NAME = N'TESTDB01- 차등 백업',
       DIFFERENTIAL, COMPRESSION, STATS = 10
GO


--트랜잭션 로그 백업
BACKUP LOG TESTDB01
       TO DISK = 'F:\SQLBackup\TESTDB01_20170703_LOG.trn'
       WITH NAME = N'TESTDB01- 트랜잭션 로그 백업', COMPRESSION, STATS = 10
GO


2022년 회고

 올해는 블로그 포스팅을 열심히 못했다. 개인적으로 지금까지 경험했던 내용들을 리마인드하자는 마인드로 한해를 보낸 것 같다.  대부분의 시간을 MLOps pipeline 구축하고 대부분을 최적화 하는데 시간을 많이 할애했다. 결국에는 MLops도 데이...