레이블이 SQL Server DBA의 임무인 게시물을 표시합니다. 모든 게시물 표시
레이블이 SQL Server DBA의 임무인 게시물을 표시합니다. 모든 게시물 표시

2014년 4월 30일 수요일

모든 DB의 로그 잘림 확인하기

데이터베이스의 사이즈를 모니터링 하는 것은 DBA의 중요한 업무중 하나 인데요.
트랜잭션 로그가 채워지지 않도록 트랜잭션 로그를 정기적으로 잘라야 합니다.
잘라야 합니다. 자르다...
어감이 마치... 10기가 짜리 로그 파일을 1기가로 줄이는... 그런거 같습니다.
근데 아니에요.
10기가를 1기가로 줄이는것은 로그 자르기가 아니고 파일 축소 작업입니다.
DBCC SHRINKFILE
명령을 이용해서 줄입니다. 이것도 맘대로 줄일 수 있는게 아니고 비활성 로그 부분만 줄일 수 있습니다. 로그를 자르고 나면 그 잘린 부분이 "비활성 로그"입니다. 그럼 로그는 언제 잘라질까요? 단순 복구 모델에서는 Checkpoint 발생이후 잘립니다. 전체 복구 모델이나 대량 로그 복구 모델에서는 일단 로그 백업이 한번 되고 난 후 Checkpoint가 발생하면 잘립니다. 로그가 잘렸다... 라는 것은 재사용 가능하게 바뀌었다라는 것을 뜻합니다. 트랜잭션 로그 파일은 내부적으로는 VLF 라는 걸로 나우어져서 사용됩니다. 예를 들어서 8기가 짜리 로그 파일을 만들었다면 대략 512메가 짜리 16개의 VLF 파일이 생깁니다. 그리고 처음부터 하나씩 사용되어 집니다. 로그가 잘릴일이 없으면 트랜잭션 로그는 계속 늘어나게 됩니다. 그러다 보면 데이터 파일은 1기가인데 로그 파일이 100기가가 되는 그런 기현상도 벌어지죠. 로그파일에 대한 정보는
DBCC LOGINFO WITH TABLERESULTS, NO_INFOMSGS
명령을 이용해서 볼 수 있습니다. 결과는 다음과 같습니다.
각각의 열은 하나의 VLF를 나타냅니다. FileSize는 KB입니다. Status가 2인 VLF가 현재 사용중이거나 아직 잘리지 않은 로그입니다. Status가 0인 VLF는 비활성 로그입니다. 잘린거죠. 잘린 로그는 재사용되어집니다.
DBCC SHRINKFILE (LogFileName, TRUNCATEONLY)
명령을 사용하면 마지막 활성 로그 이후의 공간을 운영체제에 반환하게 됩니다. 10기가 짜리 로그 파일이 1기가가 되는게 이 상황입니다. 만약 잘린 로그가 없다면 로그 파일은 무한정 커지게 됩니다. 무한용량 하드가 있다면 걱정없겠지만.... 포멧은 언제하나... 앞서 로그백업 후 Checkpoint가 발생하면 로그가 잘린다고 했었는데요. 로그가 잘린 이후 활성로그가 하나만 남는것이 이상적이랍니다. 활성로그가 여러개인 경우는 여러가지 경우가 있을 수 있겠는데 엄청나게 긴 트랜잭션이 VLF 여러개에 로그를 기록중인데 아직 트랜잭션이 끝나지 않은 경우 또는 그리 길지 않은 트랜잭션인데 VLF크기가 너무 작아서 좀 길다... 싶으면 여러개 잡아 먹는 경우가 있을 수 있겠는데요. 긴 트랜잭션이면 짧게 끝나도록 수정하면 되겠지만 VLF가 작은 경우는 문제가 좀 있습니다. 남아 있는 비활성 로그가 없을 경우 트랜잭션 로그를 기록 하기 위해서 로그파일을 늘려야 할텐데요. 이때 OS에 파일 크기를 늘려달라는 요청을 하게 될겁니다. 로그 기록하기도 바쁜데 파일 크기까지 늘려야 하니 쿼리 수행은 당연히 느려질겁니다. 그래서 제일 이상적인 상태는 로그 백업을 하는 주기 동안 더 이상 늘어나지 않아도 될만큼 충분한 로그 파일을 확보하고 그 안에서 VLF의 숫자가 적당히 존재하는게 최적일 겁니다. 적당히... 적당한 연봉은?? 그리고 로그 백업 후에는 활성 로그가 하나만 남아 있는것이 좋을것입니다. VLF의 크기를 수동으로 조절하면 좋겠지만 아쉽게도 그런 옵션은 없습니다. VLF의 크기는 처음 로그 파일을 만들때 결정됩니다. 대략 8기가 짜리 로그 파일을 만들면 512메가로 16개가 생깁니다. 그래서 처음부터 용량 계획을 하고 적당한 크기로 만들어 두는게 좋습니다. 만약 처음부터 적당한 용량으로 만들지 못했다면 일단 무슨 수를 쓰던 모든 로그를 비활성으로 만든 후 (백업을 하고 Checkpoint 수행) 파일을 줄이고 적당한 크기로 늘려놓으면 됩니다. 그래도 처음 두개의 vlf는 크기를 조정할 수 없습니다. 이상은 로그파일에 대한 설명이었고 다음 스크립트는 모든 DB에 대한 VLF의 갯수와 활성VLF 숫자를 조회하는 쿼리입니다.
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
SET NOCOUNT ON

IF OBJECT_ID('tempdb..#LOGINFO', 'U') IS NOT NULL DROP TABLE #LOGINFO
IF OBJECT_ID('tempdb..#LOGINFOTemp', 'U') IS NOT NULL DROP TABLE #LOGINFOTemp

CREATE TABLE #LOGINFO (FileID INTEGER,
                       FileSize DECIMAL(28, 0),
                       StartOffset DECIMAL(28, 0),
                       FSeqNo DECIMAL(28, 0),
                       Status TINYINT,
                       Parity TINYINT,
                       CreateLSN VARCHAR(30),
                       DatabaseName VARCHAR(1000),
                       DatabaseID INTEGER)
CREATE TABLE #LOGINFOTemp (FileID INTEGER,
                           FileSize DECIMAL(28, 0),
                           StartOffset DECIMAL(28, 0),
                           FSeqNo DECIMAL(28, 0),
                           Status TINYINT,
                           Parity TINYINT,
                           CreateLSN VARCHAR(30),
                           DatabaseName VARCHAR(1000),
                           DatabaseID INTEGER)
DECLARE @SQL VARCHAR(MAX)
SET @SQL = ''
SELECT @SQL = @SQL + 'USE [' + NAME + ']' + CHAR(10) +
                     'INSERT INTO #LOGINFOTemp (FileID, FileSize, StartOffset, FSeqNo, Status, Parity, CreateLSN)' + CHAR(10) +
                     'EXEC( ''DBCC LOGINFO WITH TABLERESULTS, NO_INFOMSGS'')' + CHAR(10) +
                     'UPDATE #LOGINFOTemp SET DatabaseName = ''' + NAME + ''', DatabaseID = DB_ID()' + CHAR(10) +
                     'INSERT INTO #LOGINFO SELECT * FROM #LOGINFOTemp' + CHAR(10) +
                     'DELETE #LOGINFOTemp' + CHAR(10)
  FROM SYS.DATABASES WHERE DATABASE_ID != DB_ID('tempdb')
EXECUTE (@SQL)
SELECT DatabaseName, DatabaseID, FileID, FileCount = COUNT(1), ActiveCount = COUNT(CASE WHEN Status > 0 THEN 1 END)
  FROM #LOGINFO
 GROUP BY DatabaseName, DatabaseID, FileID
 ORDER BY CASE WHEN DatabaseID < 5 THEN 0 ELSE 1 END, DatabaseName
결과는 다음과 같습니다.

2014년 4월 28일 월요일

Backup History 조회하기

백업이 잘 수행되었는지 확인하는 것은 DBA의 중요한 업무입니다.
백업이 실패하는 것을 모르고 지나치다가 사고라도 나면 어떻게 될까요? 경위서따위로 순순히 끝나진 않을 듯...
철저한 백업은 사고 예방의 첫걸음입니다.
백업 실패는 로그파일 뷰어를 통해서도 확인할 수 있지만
쿼리를 통해 조회하면 경고시스템을 직접 만드는 등 유연하게 대처할 수 있습니다.

다음 쿼리는 백업실행 History를 조회합니다.
sys.databases와 OUTER JOIN을 하여 백업이 실행되지 않은 Database를 쉽게 파악 할 수 있도록 하였습니다.
DECLARE @Database VARCHAR(1000), @Date1 DATETIME, @Date2 DATETIME

SET NOCOUNT ON

SELECT database_name = A.name
       , B.name
       , B.description
       , B.database_creation_date
       , B.backup_start_date
       , B.backup_finish_date
       , duration = datediff(second, B.backup_start_date, B.backup_finish_date)
       , B.type
       , size = B.backup_size/1024.0/1024.0
       , B.recovery_model
       , B.is_damaged
  FROM sys.databases(NOLOCK) A
       LEFT OUTER JOIN msdb.dbo.backupset(NOLOCK) B ON A.name = B.database_name AND B.backup_start_date BETWEEN @Date1 AND DATEADD(DAY, 1, @Date2)
 WHERE A.name = ISNULL(@Database, A.name)
 ORDER BY A.database_id, B.backup_start_date DESC

Agent Job 실패 조회하기

Agent에 등록된 Job들이 제대로 실행되었는지 확인하는것도 DBA의 중요한 업무입니다.
로그파일 뷰어를 통해 Job의 실행여부를 확인할 수도 있지만
쿼리를 통해 조회하면 경고시스템을 직접 만드는 등 유연하게 대처할 수 있습니다.

다음 쿼리는 Agent Job의 실패를 조회합니다.
DECLARE @Date1 DATETIME, @Date2 DATETIME

SET NOCOUNT ON

SELECT A.name
       , A.description
       , B.step_name
       , message = REPLACE(B.message, '. ', '.' + CHAR(10))
       , B.run_date
       , B.run_time
       , B.run_duration
  FROM msdb.dbo.sysjobs(NOLOCK) A
       INNER JOIN msdb.dbo.sysjobhistory(NOLOCK) B ON A.job_id = B.job_id
 WHERE B.run_status = 0
       AND B.step_id > 0
       AND B.run_date BETWEEN CONVERT(CHAR(8), @Date1, 112) AND CONVERT(CHAR(8), DATEADD(DAY, 1, @Date2), 112)

2014년 4월 15일 화요일

Database File Size 정보 한번에 조회하기

SQL Server의 데이터베이스에는 데이터파일과 로그파일이 여러개로 구성 되어 있을 수 있습니다.
이 파일들의 크기를 파악하고 관리하는것은 DBA의 중요한 업무 중 하나입니다.
서버에 있는 모든 데이터베이스의 모든 파일에 관한 정보를 확인하려면 sys.master_files라는 뷰를 사용하면 되지만
안타깝게도 남은 용량에 관한 정보는 제공되지 않습니다. 좀 해주지...

다음 스크립트는 모든 파일에 대한 정보를 출력합니다.
IF OBJECT_ID('tempdb..#Database_Files', 'U') IS NOT NULL DROP TABLE #Database_Files
GO
CREATE TABLE #Database_Files (
database_id int, file_id int, file_guid uniqueidentifier, type tinyint, type_desc nvarchar(60), data_space_id int, name sysname, physical_name nvarchar(260), state tinyint, state_desc nvarchar(60)
, size int, used_size int, max_size nvarchar(16), growth nvarchar(16), is_media_read_only bit, is_read_only bit, is_sparse bit, is_percent_growth bit, is_name_reserved bit
, create_lsn numeric(25,0), drop_lsn numeric(25,0), read_only_lsn numeric(25,0), read_write_lsn numeric(25,0), differential_base_lsn numeric(25,0), differential_base_guid uniqueidentifier, differential_base_time datetime
, redo_start_lsn numeric(25,0), redo_start_fork_guid uniqueidentifier, redo_target_lsn numeric(25,0), redo_target_fork_guid uniqueidentifier, backup_lsn numeric(25,0))

DECLARE @SCRIPT NVARCHAR(MAX) = 'SET NOCOUNT ON' + CHAR(10)

SELECT @SCRIPT = @SCRIPT + CHAR(10) + 'USE ' + name + ';' + CHAR(10)
       + 'INSERT INTO #Database_Files (database_id, file_id, file_guid, type, type_desc, data_space_id, name, physical_name, state, state_desc, '
       + 'size, used_size, max_size, growth, is_media_read_only, is_read_only, is_sparse, is_percent_growth, is_name_reserved, '
       + 'create_lsn, drop_lsn, read_only_lsn, read_write_lsn, differential_base_lsn, differential_base_guid, differential_base_time, '
       + 'redo_start_lsn, redo_start_fork_guid, redo_target_lsn, redo_target_fork_guid, backup_lsn)' + CHAR(10)
       + 'SELECT database_id = ' + CONVERT(VARCHAR(16), database_id) + ', file_id, file_guid, type, type_desc, data_space_id, name, physical_name, state, state_desc, '
       + 'size, used_size = FILEPROPERTY(name, ''SpaceUsed''), '
       + 'max_size = CASE WHEN max_size = -1 OR (max_size = 268435456 AND type = ''1'') THEN ''UNLIMITED'' ELSE REPLACE(CONVERT(VARCHAR(30), CONVERT(MONEY, (max_size*8.0/1024)), 112), ''.00'', SPACE(0)) END,'
       + 'growth = CASE WHEN is_percent_growth = 1 THEN CONVERT(VARCHAR(8), growth) + ''%'' ELSE CONVERT(VARCHAR(512), CONVERT(INT, growth * 8.0 / 1024)) + ''MB'' END, '
       + 'is_media_read_only, is_read_only, is_sparse, is_percent_growth, is_name_reserved, '
       + 'create_lsn, drop_lsn, read_only_lsn, read_write_lsn, differential_base_lsn, differential_base_guid, differential_base_time, '
       + 'redo_start_lsn, redo_start_fork_guid, redo_target_lsn, redo_target_fork_guid, backup_lsn' + CHAR(10)
       + 'FROM ' + name + '.sys.database_files;' + CHAR(10)
FROM   SYS.DATABASES

EXEC (@SCRIPT)

SELECT [DBID] = A.database_id
       , [DatabaseName] = A.name
       , [State] = A.state_desc
       , [AccessMode] = A.user_access_desc
       , [RecoveryModel] = A.recovery_model_desc
       , [Type] = B.type_desc
       , [LogicalName] = B.name
       , [PhysicalName] = B.physical_name
       , [Size] = CONVERT(NUMERIC(10, 2), B.size * 8.0 / 1024)
       , [UsedSize] = CONVERT(NUMERIC(10, 2), B.used_size * 8.0 / 1024)
       , [FreeSize] = CONVERT(NUMERIC(10, 2), (B.size - B.used_size) * 8.0 / 1024)
       , [FreeSize(%)] = CONVERT(NUMERIC(10, 2), (B.size - B.used_size) * 100.0 / B.size)
       , [max_size] = B.max_size
       , [Growth] = B.growth
FROM   SYS.DATABASES A
       INNER JOIN #Database_Files B ON A.database_id = B.database_id
ORDER BY CASE WHEN A.database_id < 5 THEN 1 ELSE 2 END, A.name

Database 사이즈 정보 한번에 조회하기

Database의 사이즈 계획은 DBA의 중요한 업무중에 하나 입니다.
Database를 자동증가로 설정해 놓으면 파일이 커지는 동안에 생기는 문제 때문에 수동으로 파일크기를 증가시키는 경우도 있는데, 깜박 잊고 크기를 늘려놓지 않았다면 무슨 문제가 생길까요.
여러가지 문제가 생기겠지만 결국 "서비스가 되지 않는다"로 귀결되겠죠.
아마 모든 문제를 해결 했다고 해도 "경위서"를 써야되겠지...

데이터베이스 크기에 관한 정보는 속성을 열면 나와 있지만 그리 친절하진 않습니다.
파일 크기도 mdf와 ldf를 합친 크기를 알려주는가 하면 얼마 남았는지도 친절하게 알려주지 않습니다.
그리고 모든 데이터베이스를 다 열어봐야 하죠. 귀찮게시리...

다음 스크립트는 모든 데이터베이스의 MDF와 LDF의 크기에 관한 정보를 리턴합니다.
DECLARE @SCRIPT VARCHAR(MAX)

SET @SCRIPT = 'SET NOCOUNT ON
DECLARE @FILESTATS TABLE(FileID INTEGER
, FileGroup INTEGER
, TotalExtents INTEGER
, UsedExtents INTEGER
, LogicalFileName VARCHAR(500)
, PhysicalFileName VARCHAR(500))

DECLARE @DBFILE TABLE(DatabaseName VARCHAR(1000)
, DataSize BIGINT
, DataUsed BIGINT)

DECLARE @LOGFILE TABLE(DatabaseName SYSNAME
, LogSize FLOAT
, LogSpaceUsed FLOAT
, Status INT)' + CHAR(10)

SELECT @SCRIPT = @SCRIPT + CHAR(10) + 'INSERT INTO @FILESTATS EXEC (''' + 'USE [' + NAME + '];DBCC SHOWFILESTATS WITH NO_INFOMSGS'')'
       + CHAR(10) + 'INSERT @DBFILE SELECT ''' + NAME + ''', SUM(TotalExtents), SUM(UsedExtents) FROM @FILESTATS'
       + CHAR(10) + 'DELETE @FILESTATS'
FROM SYS.DATABASES

SET @SCRIPT = @SCRIPT + CHAR(10) + 'INSERT INTO @LOGFILE EXEC (''DBCC SQLPERF (LOGSPACE) WITH NO_INFOMSGS'')'

SET @SCRIPT = @SCRIPT + '
SELECT DBID = DB_ID(A.DatabaseName)
       , A.DatabaseName
       , State = C.state_desc
       , AccessMode = C.user_access_desc
       , RecoveryModel = C.recovery_model_desc
       , DataSize = CONVERT(NUMERIC(12,2), A.DataSize * 64.0 / 1024)
       , DataUsed = CONVERT(NUMERIC(12,2), A.DataUsed * 64.0 / 1024)
       , DataFree = CONVERT(NUMERIC(12,2), (A.DataSize - A.DataUsed) * 64.0 / 1024)
       , [DataFree(%)] = CONVERT(NUMERIC(10, 2), (A.DataSize - A.DataUsed) * 100.0 / A.DataSize)
       , LogSize = CONVERT(NUMERIC(12,2), B.LogSize)
       , LogUsed = CONVERT(NUMERIC(12,2), B.LogSize * B.LogSpaceUsed / 100)
       , LogFree = CONVERT(NUMERIC(12,2), B.LogSize * (100.0 - B.LogSpaceUsed) / 100)
       , [LogFree(%)] = CONVERT(NUMERIC(10,2), 100 - B.LogSpaceUsed)
  FROM @DBFILE A
       LEFT OUTER JOIN @LOGFILE B ON A.DatabaseName = B.DatabaseName
       LEFT OUTER JOIN SYS.DATABASES C ON A.DatabaseName = C.name
 ORDER BY 1'

EXECUTE (@SCRIPT)

2014년 4월 9일 수요일

디스크 여유 공간 조사하기

물리적 디스크 공간이 부족하면 어떤일이 벌어질까요?
'PRIMARY' 파일 그룹이 꽉 찼으므로 데이터베이스 'TestDB'의 개체 'dbo.TBL1'에 공간을 할당할 수 없습니다.
필요 없는 파일을 삭제하거나, 파일 그룹의 개체를 삭제하거나, 파일 그룹에 파일을 추가하거나, 파일 그룹의 기존 파일에 대해 자동 증가를 설정하여 디스크 공간을 만드십시오.

데이터베이스 'TestDB'의 트랜잭션 로그가 꽉 찼습니다.
로그의 공간을 다시 사용할 수 없는 이유를 확인하려면 sys.databases의 log_reuse_wait_desc 열을 참조하십시오.
아마 이런 종류의 에러메시지를 보게 될것입니다. 운영중에 이런 일이 벌어지면 어떻게 될까요? 모든 트랜잭션은 실패할 것입니다. 다행히 추가 디스크가 준비되어 있다고 하더라도 서비스의 중단을 막을 수는 없을 겁니다. DBA는 항상 디스크의 남은 용량을 확인하고 용량부족으로 인한 서비스 중단 사태가 일어나지 않도록 대비해야겠죠. 다음 스크립트는 서버의 물리 디스크들의 정보를 가져옵니다. 스크립트가 동작하기 위해서는 Ole Automation Procedures의 사용을 활성화해야 합니다.
SET NOCOUNT ON

DECLARE @HR INTEGER
DECLARE @FSO INTEGER
DECLARE @Drive CHAR(1)
DECLARE @VolumeName NVARCHAR(128)
DECLARE @oDrive INTEGER
DECLARE @TotalSize VARCHAR(20)

DECLARE @Drives TABLE (Drive CHAR(1) PRIMARY KEY
, VolumnName VARCHAR(128)
, TotalSize INTEGER NULL
, FreeSpace INTEGER NULL)

INSERT @Drives(Drive, FreeSpace) EXEC master.dbo.xp_fixeddrives

EXEC @HR=sp_OACreate 'Scripting.FileSystemObject', @FSO OUT

IF @HR <> 0 EXEC sp_OAGetErrorInfo @FSO

DECLARE dCur CURSOR LOCAL FAST_FORWARD FOR
        SELECT Drive from @Drives ORDER by Drive

OPEN dCur

FETCH NEXT FROM dCur INTO @Drive

WHILE @@FETCH_STATUS=0 BEGIN
        EXEC @HR = sp_OAMethod @FSO, 'GetDrive', @oDrive OUT, @Drive
        IF @HR <> 0 EXEC sp_OAGetErrorInfo @FSO
        EXEC @HR = sp_OAMethod @oDrive, 'VolumeName', @VolumeName OUT
        IF @HR <> 0 EXEC sp_OAGetErrorInfo @oDrive
        EXEC @HR = sp_OAGetProperty @oDrive, 'TotalSize', @TotalSize OUT
        IF @HR <> 0 EXEC sp_OAGetErrorInfo @oDrive

        UPDATE @Drives
        SET VolumnName = @VolumeName
                , TotalSize = @TotalSize / (1024.0 * 1024 * 1024)
                , FreeSpace = FreeSpace / 1024
        WHERE Drive = @Drive
        FETCH NEXT FROM dCur INTO @Drive
End

Close dCur
DEALLOCATE dCur

EXEC @HR=sp_OADestroy @FSO

IF @HR <> 0 EXEC sp_OAGetErrorInfo @FSO

 SELECT Drive
        , VolumnName
        , TotalSize
        , UsedSpace = TotalSize - FreeSpace
        , FreeSpace
        , [Free(%)] = CONVERT(NUMERIC(4,2), 1.0 * FreeSpace / TotalSize * 100)
   FROM @Drives
  ORDER BY Drive
스크립트의 실행 결과는 다음과 같습니다.
테이블의 형태로 반환되므로 디스크 용량이 낮아지면 SMS를 보내도록 Agent에 등록하여 사용하면 좋겠습니다.

LOGIN Failed 조사하기

로그인이 실패하게 되면 SQL Server 로그에 다음과 같은 내용이 기록됩니다.
Login failed for user 'DBUser'. 원인: 암호가 제공된 로그인의 암호와 일치하지 않습니다. [클라이언트: 127.0.0.1]
SQL SERVER의 모든 Log파일을 조사하여 Login Failed를 찾는 스크립트 입니다. 모르는 IP에서 잦은 Login Failed가 있다면 해킹이 의심되므로 방화벽에서 차단하던지 LOGON 트리거에서 제한하던지 해야 할 것입니다. 저희 회사는 sa를 Disable 했는데도 불구하고 sa로 로긴 시도가 있었습니다. 이런 IP들은 가차없이 Logon 트리거에서 차단해버려야겠죠. 스크립트는 수정없이 그냥 붙여넣고 실행하면 됩니다.
SET NOCOUNT ON

DECLARE @MaxErrorLogCnt INT, @LogCnt INT

DECLARE @ErrorLogs TABLE (NO INT, DATE DATETIME, SIZE INT)
DECLARE @Errors TABLE (LogDate DATETIME, ProcessInfo NVARCHAR(1000), ErrMsg NVARCHAR(MAX))

INSERT @ErrorLogs EXEC xp_enumerrorlogs

SET @MaxErrorLogCnt = (SELECT MAX(NO) FROM @ErrorLogs)

SET @LogCnt = 0

WHILE @MaxErrorLogCnt > @LogCnt
BEGIN
        INSERT @Errors EXEC sp_readerrorlog @LogCnt, '1', 'Login failed', 'for user'
        SET @LogCnt = @LogCnt + 1
End

UPDATE @Errors SET ErrMsg = REPLACE(ErrMsg, CHAR(10), ' ')
UPDATE @Errors SET ErrMsg = REPLACE(ErrMsg, '[', '')
UPDATE @Errors SET ErrMsg = REPLACE(ErrMsg, ']', '')
UPDATE @Errors SET ErrMsg = REPLACE(ErrMsg, 'Reason', '원인')

SELECT  LogDate
        , Login = SUBSTRING(ErrMsg, 24, PATINDEX('%.%', ErrMsg) - 25)
        , Client = SUBSTRING(ErrMsg, PATINDEX('%클라이언트%', ErrMsg) + 7, 15)
        , Reason = SUBSTRING(ErrMsg, PATINDEX('%원인%', ErrMsg) + 4, PATINDEX('%클라이언트%', ErrMsg) - PATINDEX('%원인%', ErrMsg) - 5)
FROM    @Errors
ORDER BY LogDate DESC

/* 이하는 업무일지용 */
DECLARE @RESULT NVARCHAR(MAX) = ''

SELECT  @RESULT = @RESULT
        + 'LogDate : ' + CONVERT(VARCHAR(20), CONVERT(DATETIME, LogDate), 120) + CHAR(10)
        + 'Login : ' + SUBSTRING(ErrMsg, 24, PATINDEX('%.%', ErrMsg) - 25) + CHAR(10)
        + 'Client : ' + SUBSTRING(ErrMsg, PATINDEX('%클라이언트%', ErrMsg) + 7, 15) + CHAR(10)
        + 'Reason : ' + SUBSTRING(ErrMsg, PATINDEX('%원인%', ErrMsg) + 4, PATINDEX('%클라이언트%', ErrMsg) - PATINDEX('%원인%', ErrMsg) - 5) + CHAR(10)
        + 'DBA 처리 내용 : ' + CHAR(10) + CHAR(10)
FROM    @Errors
WHERE   LogDate >= DATEADD(DAY, -1, CONVERT(CHAR(10), GETDATE(), 120))
ORDER BY LogDate DESC

SELECT LOG = @RESULT
실행 결과는 다음과 같습니다.
결과를 살펴보면 DBUser가 두번의 로그인 실패를 했고 DBReader는 없는 계정인데 누군가가 로그인 시도를 한것입니다. 방화벽 정책을 추가하거나, 로그온 트리거에서 해당 IP를 차단하는 처리가 필요할 것 같습니다.