전산이야기

원격지 서버의 테이블에 대한 갱신 작업을 위해서 다음과 같이 Linked Server의 테이블을 JOIN 하여 UPDATE 하였습니다.

SET XACT_ABORT ON;

DECLARE @ano VARCHAR(30) = 'S201910250010'

BEGIN TRAN

UPDATE Trgt
SET
    AcptYn = 'N',
    AcptEmpCode = '', 
    AcptDeptCode = '',
    AcptDateTime = NULL
FROM
    RemoteServer.RemoteDB.dbo.FIAccSlipDisappMaster AS Trgt
        JOIN RemoteServer.RemoteDB.dbo.FIAccSlipAggreationNo AS A
            ON a.SlipNo = Trgt.SlipNo
            AND A.AggregationNo = @ano

SET XACT_ABORT OFF;

COMMIT

그러나 동작은 잘 되지만 성능이 너무 나오지 않았습니다.

실행계획을 보니 원격지 테이블을 모두 읽어와서 JOIN 시키다 보니 실제 UPDATE 시킬 행은 1개 인데 수십만 건의 데이터를 읽고 있었습니다.

그래서 OpenQuery로 변경 하려고 했으나, 문제는 OpenQuery의 SQL문이 순수한 문자열만 가능 하도록 되어 있어서 파라미터(변수)를 사용할 수가 없었습니다.

SET XACT_ABORT ON;

DECLARE @ano VARCHAR(30) = 'S201910250010'

BEGIN TRAN

UPDATE x
SET
    AcptYn = ''N'',
    AcptEmpCode = '''', 
    AcptDeptCode = '''',
    AcptDateTime = NULL
FROM
    OpenQuery (RemoteServer,
        '
        SELECT
            Trgt.*
        FROM RemoteDB.dbo.FIAccSlipAggreationNo AS A
            JOIN RemoteDB.dbo.FIAccSlipDisappMaster AS Trgt
                ON a.SlipNo = Trgt.SlipNo
        WHERE
            A.AggregationNo = ''' + @ano + '''
        '
    ) x

SET XACT_ABORT OFF;

COMMIT

그래서 최종적으로 OpenQuery를 포함한 전체 쿼리 문장을 문자열 변수에 넣고 EXEC()로 처리 하였습니다.

SET XACT_ABORT ON;

DECLARE @ano VARCHAR(30) = 'S201910250010'
DECLARE @sql varchar(max)

BEGIN TRAN

set @sql = 
    '
    UPDATE x
    SET
        AcptYn = ''N'',
        AcptEmpCode = '''', 
        AcptDeptCode = '''',
        AcptDateTime = NULL
    FROM
        OpenQuery (RemoteServer,
            ''
            SELECT
                Trgt.*
            FROM RemoteDB.dbo.FIAccSlipAggreationNo AS A
                JOIN RemoteDB.dbo.FIAccSlipDisappMaster AS Trgt
                    ON a.SlipNo = Trgt.SlipNo
            WHERE
                A.AggregationNo = ''''' + @ano + '''''
            ''
        ) x
    '
EXEC(@sql)

SET XACT_ABORT OFF;

COMMIT

성능도 좋고, 파라미터(변수)도 처리할 수 있어서 만족할 만한 결과가 나왔습니다.

 

원격지 서버에 대한 갱신작업이 필요하실 때 참고 하시기 바랍니다.

Comment +0

SSMS 에서 Ctrl + M 을 눌러서 '실제 실행 계획 포함' 이 활성화 된 상태에서

쿼리를 실행 시키고 나면 실행계획이 함께 나타납니다.

실행계획 상의 특정 노드에 마우스를 가져가면 상세한 내용이 나타 납니다.

위 그림에서 '예상 행 수' 가 2.77684로 되어 있어 옵티마이저가 2.7행을 예상 했다고 생각할 수 있으나

실제 예상 행 수는 '예상 실행 횟수'를 곱한 값이 됩니다.

따라서 해당 노드에서 반환되는 총 예상 행 수는 2.77684 × 6985.3104807 = 19397.08956 입니다.

이 총 예상 행 수 값이 노드 하단의 분모 값 입니다.

그리고 분자 값은 '실제 행 수' 입니다. 

괄호 안의 %값은 총 예상 행 수 대비 실제 행 수의 백분율 입니다. 이 백분율 값이 100%에 가까울 수록

통계값에 의한 적절한 실행계획이 수립되었다고 볼 수 있습니다.

 

그러나 아래의 경우 처럼 이 값이 과도하게 적거나 많을 경우 통계가 제대로 갱신되지 않아서 엉뚱한 실행계획이

수립되었을 가능성이 있습니다.

이 경우 UPDATE STATISTICS 로 최신 통계로 업데이트가 필요 합니다.

Comment +0

홈택스 전자세금계산서 연동 기능을 제공해주는 업체의 API를 테스트 하기 위하여 제공된 DLL을 참조 시키고

테스트 코드를 작성한 후 컴파일을 시도 했으나 다음과 같은 오류가 발생 하였다.

 

참조된 어셈블리 "Popbill.dll"이(가) 현재 대상 프레임워크 ".NETFramework,Version=v4.0,Profile=Client"에 없는 "System.ServiceModel.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"에 종속되어 있기 때문에 참조된 어셈블리를 확인할 수 없습니다. 이 문제를 해결하려면 (1) 이 프로젝트의 대상 프레임워크를 변경하거나 (2) 프로젝트에서 참조된 어셈블리를 제거하십시오.

해결을 위하여 시행착오를 겪다가 인터넷을 통해서 원인을 찾았다.

프로젝트 속성 중 대상 프로임워크가 .NET Framework Client Profile 로 되어 있었기 때문 이었다. Client Profile은 사용자를 위한 런타임 이기 때문에 Visual Studio 에서 컴파일 시에 사용할 수가 없다고 한다.

대상 프레임워크를 .NET Framework 4 로 변경 하고 저장 후, 프로젝트를 다시 오픈 한 다음 컴파일 하니 정상적으로 되었다.

동일한 증상 있으신 분은 참조 하시기 바랍니다.

Comment +0

SSMS 이전버전에서는 로그인 암호 파일이 별도로 있어서 해당 파일만 삭제 하면 해결 되었으나,

(SQL 서버 2008 SSMS 로그인시 암호저장이 잘 안될때 참조)

SSMS v.18에서는 해당파일 자체가 존재 하지 않아서 기존방법으로는 자동로그인 된 정보를

삭제할 수가 없습니다.

 

해결방법은 의외로 간단한데, 

로그인 화면에서 '로그인(L):' 드롭박스를 클릭해서 나오는 리스트에서 원하는 로그인명을 Del  키로 삭제하면 됩니다.

삭제 후 다시 로그인과 암호를 입력하고 '암호 저장'에 체크를 해서 성공적으로 로그인이 되면 암호저장이 정상적으로 됩니다.

저는 처음에 이걸 몰라서 좀 애를 먹었다는...^^

Comment +0

아시는 바와 같이 임시 테이블은 #(로컬) 및 ##(전역)이 prefix로 붙은 테이블 입니다.
그리고 테이블변수는 TABLE 타입으로 DECLARE된 로컬변수 입니다.
 
둘 간에 여러가지 차이점이 있지만 여기서는 성능적인 이슈만 언급 하겠습니다.
테이블변수의 문제는 SQL서버가 통계정보를 생성하지 않기 때문에 적절한 실행계획 수립이 안될 수 있다는 점 입니다.
 
아래는 테이블변수를 사용한 특정 SP의 실행계획 입니다 (처리속도는 14초).

NL JOIN 으로 처리되어 과도한 Execute, Rows가 나왔습니다. SQL 서버가 NL을 선택한 것은 예상 행수가 모두 1 이기 때문 입니다.
예상 행수가 1:1 이기 때문에 NL로 처리해도 아무 문제가 없다고 판단이 되었습니다.
실제 테이블변수의 행은 1067 입니다.
 
이제 다른것은 그대로 두고 테이블변수를 임시테이블로만 변경한 후, 다시 실행계획을 확인 해봅니다 (처리속도는 6초).

이번에는 HASH JOIN 으로 처리 되었습니다. 이유는 예상행수가 1067, 617 이기 때문 입니다.
1067건과 617건을 NL JOIN 으로 처리하면 부하가 크다고 판단하고 HASH로 JOIN 전략을 변경한 겁니다.
 
결론적으로 일반적으로 행수가 적은 경우는 테이블변수를 사용해도 무관하지만, 크기가 큰 경우는 임시테이블을 사용해야 성능이슈가 발생하지 않게 됩니다.
 
 

 

Comment +0

SP는 최초(처음) 실행 하는 시점에 실행계획을 수립하고 이후에는 실행계획을 계속 재사용 하게 됩니다

(이것이 SP를 사용하는 중요한 이유 중 하나 입니다).

이 최초 실행계획 수립시, 인수로 넘겨진 파라미터(매개변수)를 기준으로 통계를 통해 예상 행수를 추측하게 됩니다.

그런데 넘겨진 파라미터가 아닌 로컬 변수를 이용해서 쿼리를 실행하면 옵티마이저가 행수 추측이 실패하게 되어

원하지 않는 실행 계획이 생성 될 수 있습니다.

이로 인하여 SP 성능이 저하 될 수 있습니다.

 

예를 들어 다음과 같이 SP 가 되어 있을 때,

create proc Proc1 (
@p_date varchar(8)
)
as
SELECT
   *
FROM 
   SDSaleMaster a
WHERE
   a.SaleDate = @p_date

SP를 호출 하면,

EXEC Proc1 @p_date = '20190601'

실행계획에서 행수를 1326으로 예측 합니다.

이는 통계와 일치하는 제대로 된 예상 행 수 이기 때문에, 실행계획이 적합하게 나올 가능성이 높습니다.

그런데 다음과 같이 매개변수가 아닌 로컬 변수를 사용 하게 되어 있을 때,

create proc proc1 (
@p_date varchar(8)
)
as
declare
   @v_date varchar(8)

set @v_date = convert(varchar(8), dateadd(d, -1, @p_date), 112)  -- 하루 전

SELECT
   *
FROM 
   SDSaleMaster a
WHERE
   a.SaleDate = @v_date

SP를 호출 하면,

 EXEC Proc1 @p_date = '20190602'

옵티마이저가 예상 행수를 제대로 추정할 수가 없어서 549로 나오는 데, 이는 해당 테이블의 평균 밀도값을 이용한 것 입니다.

평균밀도

예상 행수 = 밀도 * 테이블 전체 row 수

 

이 글이 도움이 되셨으면 좋아요 눌러 주시거나 댓글을 남겨 주세요.

Comment +0

지난 글에 이어서 실제 C# 으로 SOAP API를 호출 해 보는 소스를 작성 해 보겠습니다.

일반적으로 웹 서비스 제공업체에서 제공해 주는 기술문서를 참고로 해서 사용되는 함수 및 자료형을 확인해야 합니다.
여기서는 용마에서 제공되는 문서를 바탕으로 작성 되었습니다.

또한 처리방법만을 남길 목적인 테스트용 코드 이므로, 예외 처리등은 하지 않았습니다.
실제 환경에서는 반드시 리턴값 확인 및 예외 처리를 해야 합니다.

(1) define

1. 해당하는 클래스가 정의된 Reference.cs의 namespace 및 Linq를 using에 추가 합니다.

using WindowsFormsApplication1.kr.co.yongmalogis.www;
using System.Xml.Linq;

2. 호출될 메소드를 가지고 있는 객체를 전역변수로 선언 합니다.

TestBPService testBPService;

3. 나중에 사용할 List를 전역변수로 생성 합니다.

List<Dictionary<string, string>> array = new List<Dictionary<string, string>>();

 

(2) 등록번호 추출

간단히 getEDINum() 메소드를 호출하면 string 으로 리턴 됩니다.

private void btngetEDINum_Click(object sender, EventArgs e)
{
    txtgetEDINum.Text = testBPService.getEDINum();
}

 

(3) 반품정보 등록

주어진 기술문서를 참조하여 Key, Value 쌍을 Dictonary에 추가하고, 이를 다시 List에 추가 합니다.

Dictionary<string, string> dic = new Dictionary<string, string>();

dic.Add("regno", txtgetEDINum.Text);
dic.Add("ymd", "20190624");
dic.Add("Cons", "XXXXXX");
dic.Add("Seq", "1");
dic.Add("Ordno", "");
dic.Add("Vencode", "");
dic.Add("Venname", "왕내과");
dic.Add("Zip1", "123");
dic.Add("Zip2", "45");
dic.Add("Addr1", "울릉도 동남쪽 뱃길따라 2백리");
dic.Add("Addr2", "");
dic.Add("Contact", "이사부");
dic.Add("Phone1", "011-123-4567");
dic.Add("Phone2", "");
dic.Add("Descry", "");
dic.Add("Itemcode", "");
dic.Add("Itemname", "");
dic.Add("Qty", "1");
dic.Add("Memo1", "");
dic.Add("Sname", "");
dic.Add("sphone", "");

array.Add(dic);

기술문서에 파라미터 데이터 타입이 XML 문자열로 되어 있어서, List 데이터 전체를 ediInsert() 메소드의 인자로 넘길 XML 문자열로 변환 합니다.

XElement xmlElements = new XElement("TEBLE", array.Select(i => new XElement("item",
                    i.Select(j => new XElement(j.Key, j.Value))
                    )));

실제 SOAP API를 호출 합니다. 

string result = testBPService.ediInsert(xmlElements.ToString());
MessageBox.Show(result);

나머지 반품 확정 및 삭제는 등록번호 추출과 유사한 형태라 생략 합니다.

 

이 글이 도움이 되셨으면 좋아요 눌러 주시거나 댓글을 남겨 주세요.

Comment +0

최근 사내 ERP에서 용마택배로 반품정보를 SOAP API 호출을 해야 할 일이 생겨서 이를 정리 합니다.

우선 Visual Studio 에서 새로운 프로젝트를 시작하고, 솔루션 탐색기에서 서비스참조 추가를 합니다.

하단의 『고급』 버튼을 눌러서 서비스 참조 설정 창이 뜨면 다시 『웹 참조 추가』 버튼을 누릅니다.

 

웹 참조 추가 창이 나타나면, 알려 준 asmx URL을 입력하고 우측의 작은 화살표를 클릭 하면
자동으로 제공되는 메소드 목록이 나타 납니다.
확인 후 『참조추가』 버튼을 누르면 해당 메소드를 호출 할 수 있는 프록시 클래스가 생성 됩니다.

 

위의 웹 참조 이름을 더블클릭 하면 개체 브라우저를 통해 제공되는 메소드와 데이터타입 등을 확인 할 수 있습니다.

실제 소스는 Reference.cs 라는 파일로 생성 됩니다.

다음 글에서는 실제 API 호출을 하는 소스를 c# 으로 작성 하도록 하겠습니다.

Comment +0

연말정산 때문에 가족관계증명서 발급을 위해 법원 사이트에 갔다.

그런데 이만큼을 설치 해야 한단다.

고작 프린트 한장 하는데 뭔 놈의 잡다한 프로그램을 이렇게나 많이 깔라는 건지....

이게 IT 강국이라는 대한민국 정부 사이트의 슬픈 현실이다.

울며겨자먹기로 꾸역꾸역 설치 한 후, 증명서를 인쇄 하고 난 다음

다시 주민등록등본 발급을 위해 민원24 사이트로 갔다.

그런데 또 설치를 하란다.

아 진짜.. 짜증.

정부 사이트 간에는 좀 표준화를 해서 한번만 설치할 수 있게 하든지 해야지

사이트 마다 재각각 이면 어쩌겠다는 건지... 




'생각' 카테고리의 다른 글

프린트 한 장 하자고...  (0) 2019.01.24
델파이의 상승세  (2) 2017.03.10
Microsoft 사의 Wunderlist 인수를 보며  (0) 2015.06.04
IT업체선정의 중요성  (0) 2011.02.10
역시 오너...  (0) 2010.04.28
JD Edwards의 문제점  (4) 2010.04.09

Comment +0

SQL 서버에서 서로 다른 데이터 타입간에 연산을 하게 되면
연산 전에 데이터 형식을 묵시적으로 형변환하게 되며,
이는 최종 결과 값의 데이터 형식이 된다.
그런데 여기에는 우선순위가 있기 때문에 때때로 연산결과 값에서 소수점이 사라지거나 오차가 발생 할 수 있으므로 주의해야 한다.

우선순위는 다음과 같다 (참조: https://docs.microsoft.com/ko-kr/sql/t-sql/data-types/data-type-precedence-transact-sql?view=sql-server-2017)

  1. 사용자 정의 데이터 형식(가장 높음)
  2. sql_variant
  3. xml
  4. datetimeoffset
  5. datetime2
  6. datetime
  7. smalldatetime
  8. date
  9. time
  10. float
  11. real
  12. decimal
  13. money
  14. smallmoney
  15. bigint
  16. int
  17. smallint
  18. tinyint
  19. bit
  20. ntext
  21. text
  22. image
  23. timestamp
  24. uniqueidentifier
  25. nvarchar(nvarchar(max) 포함)
  26. nchar
  27. varchar (varchar(max) 포함)
  28. char
  29. varbinary (varbinary(max) 포함)
  30. binary(가장 낮음)

그리고 피연산자에 숫자 상수값이 들어가는 경우 상수의 표현 방식에 따라 데이터형이 결정 되므로 역시 주의 해야 한다.

  1. 이진상수 : 0x 접미사 (16진수)
  2. integer 상수: 소수점 없는 정수
  3. 10진수(decimal, numeric) 상수: 숫자에 소수점이 포함
  4. float 및 real 상수: 과학 표기법 (예: 0.5E-1)

함수의 결과 값도 원래 컬럼의 데이터타입과 다른 경우가 있으므로 주의 해야 한다.
  1. SUM() :  decimal에 대한 계산 결과는 무조건 decimal(38, s) 가 된다.
                 s는 원래 decimal 타입 값의 소수점이하 자리 수.
                 전체 자리수가 무조건 38이 되므로 주의!
  2. CEILING(), FLOOR() : decimal에 대한 결과는 decimal(p, 0)이 된다.
                                p는 원래 decimal 타입 값의 전체 자리 수.

decimal 타입간의 연산 결과는 두 decimal 타입 중 더 넓은 범위의 값을 저장할 수 있는 타입이 우선순위를 가지는 것으로 보인다.
예를 들어 decimal(38, 5) 와 decimal(38, 2) 의 결과는 decimal(38, 2)로 된다.
소수점 이하 정밀도 보다는 정수 부분이 더 큰/작은 넓은 범위의 숫자를 저장할 수 있는 타입에 우선순위가 있는  것 같다.
특히 decimal에 대한 SUM()의 결과는 무조건 decimal(38, s)이 되고 CEILING()은 decimal(p, 0)이 되므로
만약 SUM()에 의하여 decimal(38, 5)가 되고, CEILING()에 의해 decimal(38, 0)이 된 경우 그 결과는 decimal(38, 0)이 되어
데이터에서 소수점이하 값이 사라질 수 있으므로 주의 해야 한다.

SELECT * 
INTO   #c 
FROM   (SELECT Sum(CONVERT(DECIMAL(38, 5), 123.45)) AS x 
        UNION 
        SELECT Ceiling(CONVERT(DECIMAL(38, 5), 456.78)) AS x) a 
go 

SELECT * 
FROM   #c 



Comment +0