Microsoft MVP성태의 닷넷 이야기
.NET Framework: 236. SqlDbType - DateTime, DateTime2, DateTimeOffset의 차이점 [링크 복사], [링크+제목 복사],
조회: 21069
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 1개 있습니다.)

SqlDbType - DateTime, DateTime2, DateTimeOffset의 차이점

SQL 서버 입장이 아닌, 닷넷 개발자 입장에서의 차이점을 알아볼까요?

예제 DB 에 다음과 같은 테이블을 정의하고,

CREATE TABLE [dbo].[TestTable](
    [Id] [int] NOT NULL,
    [Time1] [datetime] NULL,
    [Time2] [datetime2](7) NULL,
    [TimeOffset] [datetimeoffset](7) NULL
) ON [PRIMARY]

테스트를 위해 다음과 같은 예제 코드를 작성합니다.

using (SqlConnection sqlConnection =
    new SqlConnection(@"Integrated Security=SSPI;Persist Security Info=False;Initial Catalog=TestDB;Data Source=."))
{
    sqlConnection.Open();

    // Delete
    SqlCommand deleteCommand = new SqlCommand();
    deleteCommand.Connection = sqlConnection;
    deleteCommand.CommandText = "DELETE FROM TESTTABLE";

    deleteCommand.ExecuteNonQuery();

    // Create
    SqlCommand insertCommand = new SqlCommand();
    insertCommand.Connection = sqlConnection;
    insertCommand.CommandText = "INSERT INTO TESTTABLE(Id, Time1, Time2, TimeOffset) VALUES (@Id, @Time1, @Time2, @TimeOffset)";

    insertCommand.Parameters.Add("@Id", SqlDbType.Int);
    insertCommand.Parameters.Add("@Time1", SqlDbType.DateTime);
    insertCommand.Parameters.Add("@Time2", SqlDbType.DateTime2);
    insertCommand.Parameters.Add("@TimeOffset", SqlDbType.DateTimeOffset);

    insertCommand.Parameters[0].Value = Guid.NewGuid().GetHashCode();

    DateTime now = DateTime.UtcNow;
    insertCommand.Parameters[1].Value = now;
    insertCommand.Parameters[2].Value = now;
    insertCommand.Parameters[3].Value = now;

    int affected = insertCommand.ExecuteNonQuery();
    Console.WriteLine("Now - Ticks: " + now.Ticks + ", Kind = " + now.Kind);

    // Select - DataTable
    DataSet ds = new DataSet();
    SqlDataAdapter da = new SqlDataAdapter("SELECT * FROM TESTTABLE", sqlConnection);
    da.Fill(ds, "testTable");

    DataTable dt = ds.Tables["testTable"];
    System.DateTime Time1= System.DateTime.MinValue;
    System.DateTime Time2 = System.DateTime.MinValue;
    System.DateTimeOffset Time3 = System.DateTimeOffset.MinValue;

    unsafe
    {
        Console.WriteLine("Time1/Time2 size: " + sizeof(System.DateTime));
        Console.WriteLine("TimeOffset size: " + sizeof(System.DateTimeOffset));
    }

    foreach (DataRow dr in dt.Rows)
    {
        int id = Convert.ToInt32(dr["Id"]);
        Time1 = (System.DateTime)(dr["Time1"]);
        Time2 = (System.DateTime)(dr["Time2"]);
        Time3 = (System.DateTimeOffset)(dr["TimeOffset"]);

        Console.WriteLine(string.Format("Ticks = {0}, Kind = {1}", Time1.Ticks, Time1.Kind));
        Console.WriteLine(string.Format("Ticks = {0}, Kind = {1}", Time2.Ticks, Time2.Kind));
        Console.WriteLine(string.Format("Ticks = {0}, Kind = {1}", Time3.Ticks, Time3.Offset));
    }
}

실행해 보면, 출력 결과는 다음과 같습니다.

Now - Ticks: 634491664775545133, Kind = Utc
Time1/Time2 size: 8
TimeOffset size: 12
Ticks = 634491664775530000, Kind = Unspecified
Ticks = 634491664775545133, Kind = Unspecified
Ticks = 634491664775545133, Kind = 00:00:00

차이점이 잘 드러나고 있지요!

DateTime, DateTime2가 Timezone에 대한 정보를 보관하고 있지 않기 때문에 만약 이에 대한 민감한 프로그램을 개발하고 있다면 DateTimeOffset으로 지정하는 것이 좋습니다.

DateTime의 경우 DateTime2/DateTimeOffset과의 주요 차이점은 바로 3.33...ms(밀리세컨드) 단위 까지만 데이터를 보관한다는 점이고.

닷넷 개발자 입장에서는 저 정도만 알아두면 개발하는 데 크게 무리는 없겠지만, SQL 서버상의 차이점까지 알아두시면 좀 더 도움이 될 수 있겠지요? ^^

[The ultimate guide to the datetime datatypes에서 인용]
  • DateTime
    • 8byte
    • 3.33ms 정확성
    • 1753-01-01 ~ 9999-12-31일까지의 날짜 표현

  • DateTime2
    • 6~8byte
    • 100ns 정확성
    • 0001-01-01 ~ 9999-12-31 일까지의 날짜 표현

  • DateTimeOffset
    • 8~10byte
    • 100ns 정확성
    • 0001-01-01 ~ 9999-12-31 일까지의 날짜 표현, Timezone 표현 가능


(근데 위의 표를 보니... 한 가지 이해 안되는 것이 있는데요. DateTime과 DateTime2는 점유 공간 면에서 동일하게 8바이트가 소모되는데 어째서 정확성도 떨어지고 표현이 가능한 시작 년도도 1753년으로 해야만 했을까요?)




요즘 들어서, 국제화가 중요해지다 보니 Timezone에 대한 염두를 하지 않을 수 없습니다. 그런데, 이를 고려하기 위해 DateTimeOffset을 사용하게 되면 닷넷 프로그래밍에서 DateTime 자료형이 아니게 되어 처리가 다소 번거로운 단점이 있습니다. 따라서, SQL Server에서는 무조건 데이터를 UTC 형식으로 저장한다는 원칙을 세운다면 이후의 프로그램에서 좀 더 쉽게 접근하는 것이 가능합니다.

그런데, DateTime/DateTime2에 대해서 애당초 Kind 속성을 Unspecified가 아닌 Utc로 SELECT 단계에서부터 반환받고 싶다면 어떻게 해야 할까요?

음... 방법이 없습니다. ^^; 다음과 같이 한번 더 DateTime 타입을 생성해 주는 것으로 부가적인 처리를 해주어야 합니다.

foreach (DataRow dr in dt.Rows)
{
    ...[생략]...
    System.DateTime time2 = (System.DateTime)(dr["Time2"]);
    System.DateTime time2_with_zone = new DateTime(time2.Ticks, DateTimeKind.Utc);
    ...[생략]...
}




여기서 잠깐, Firebird의 Timestamp도 짚고 넘어가볼까요? ^^

테스트를 위해 간단하게 다음과 같은 형식의 DB/Table을 만들고,

create database 'c:\temp\test.fdb' user 'SYSDBA' password 'masterkey';
commit;
connect 'c:\temp\test.fdb' user 'SYSDBA' password 'masterkey';
CREATE TABLE TESTTABLE (NAME VARCHAR(50) NOT NULL PRIMARY KEY, Created TIMESTAMP NOT NULL);

UTC 형식의 DateTime 값을 넣고 출력하는 예제를 만들어 봅니다.

using (FbConnection fbConnection =
    new FbConnection(@"DataSource=localhost;Port=3050;Database=c:\temp\test.fdb;User=SYSDBA;Password=masterkey"))
{
    fbConnection.Open();

    // Create
    FbCommand insertCommand = new FbCommand();
    insertCommand.Connection = fbConnection;
    insertCommand.CommandText = "INSERT INTO TESTTABLE(NAME, Created) VALUES (@NAME, @Created)";

    insertCommand.Parameters.Add("@NAME", FbDbType.VarChar, 50);
    insertCommand.Parameters.Add("@Created", FbDbType.TimeStamp);

    string nameValue = "Name" + Guid.NewGuid().ToString(); ;
    insertCommand.Parameters[0].Value = nameValue;
    insertCommand.Parameters[1].Value = DateTime.UtcNow;

    int affected = insertCommand.ExecuteNonQuery();
    Console.WriteLine("# of affected row: " + affected);

    // Select - DataTable
    DataSet ds = new DataSet();
    FbDataAdapter da = new FbDataAdapter("SELECT * FROM TESTTABLE", fbConnection);
    da.Fill(ds, "testTable");

    DataTable dt = ds.Tables["testTable"];
    foreach (DataRow dr in dt.Rows)
    {
        string name = dr["NAME"] as string;
        DateTime created = Convert.ToDateTime(dr["Created"]);

        Console.WriteLine(string.Format("Kind = {0}, Ticks = {1}", created.Kind, created.Ticks));
    }
}

실행 후 출력 결과는 다음과 같습니다.

[실행 예]

Kind = Unspecified, Ticks = 634491803025870000

척 보니... SQL 서버의 DateTime 타입과 동일한 결과를 보여주고 있습니다. Timezone에 대한 기록 방법이 없으니, 역시 무조건 UTC 기준으로 시간 관리를 하시는 것이 좋습니다.

첨부된 파일은 위의 코드를 포함한 예제 프로젝트입니다.




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 7/10/2021]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 




... [16]  17  18  19  20  21  22  23  24  25  26  27  28  29  30  ...
NoWriterDateCnt.TitleFile(s)
13247정성태2/7/20235223VS.NET IDE: 180. Visual Studio - 닷넷 소스 코드 디버깅 중 "Decompile source code"가 동작하는 않는 문제
13246정성태2/6/20234421개발 환경 구성: 664. Hyper-V에 설치한 리눅스 VM의 VHD 크기 늘리는 방법 - 두 번째 이야기
13245정성태2/6/20234991.NET Framework: 2093. C# - PEM 파일을 이용한 RSA 개인키/공개키 설정 방법파일 다운로드1
13244정성태2/5/20234382VS.NET IDE: 179. Visual Studio - External Tools에 Shell 내장 명령어 등록
13243정성태2/5/20235222디버깅 기술: 190. windbg - Win32 API 호출 시점에 BP 거는 방법 [1]
13242정성태2/4/20234674디버깅 기술: 189. ASP.NET Web Application (.NET Framework) 프로젝트의 숨겨진 예외 - System.UnauthorizedAccessException
13241정성태2/3/20234068디버깅 기술: 188. ASP.NET Web Application (.NET Framework) 프로젝트의 숨겨진 예외 - System.IO.FileNotFoundException
13240정성태2/1/20234207디버깅 기술: 187. ASP.NET Web Application (.NET Framework) 프로젝트의 숨겨진 예외 - System.Web.HttpException
13239정성태2/1/20233901디버깅 기술: 186. C# - CacheDependency의 숨겨진 예외 - System.Web.HttpException
13238정성태1/31/20236071.NET Framework: 2092. IIS 웹 사이트를 TLS 1.2 또는 TLS 1.3 프로토콜로만 운영하는 방법
13237정성태1/30/20235765.NET Framework: 2091. C# - 웹 사이트가 어떤 버전의 TLS/SSL을 지원하는지 확인하는 방법
13236정성태1/29/20235217개발 환경 구성: 663. openssl을 이용해 인트라넷 IIS 사이트의 SSL 인증서 생성
13235정성태1/29/20234887개발 환경 구성: 662. openssl - 윈도우 환경의 명령행에서 SAN 적용하는 방법
13234정성태1/28/20236000개발 환경 구성: 661. dnSpy를 이용해 소스 코드가 없는 .NET 어셈블리의 코드를 변경하는 방법 [1]
13233정성태1/28/20237352오류 유형: 840. C# - WebClient로 https 호출 시 "The request was aborted: Could not create SSL/TLS secure channel" 예외 발생
13232정성태1/27/20235011스크립트: 43. uwsgi의 --processes와 --threads 옵션
13231정성태1/27/20234042오류 유형: 839. python - TypeError: '...' object is not callable
13230정성태1/26/20234427개발 환경 구성: 660. WSL 2 내부로부터 호스트 측의 네트워크로 UDP 데이터가 1개의 패킷으로만 제한되는 문제
13229정성태1/25/20235454.NET Framework: 2090. C# - UDP Datagram의 최대 크기
13228정성태1/24/20235582.NET Framework: 2089. C# - WMI 논리 디스크가 속한 물리 디스크의 정보를 얻는 방법 [2]파일 다운로드1
13227정성태1/23/20235209개발 환경 구성: 659. Windows - IP MTU 값을 바꿀 수 있을까요? [1]
13226정성태1/23/20234891.NET Framework: 2088. .NET 5부터 지원하는 GetRawSocketOption 사용 시 주의할 점
13225정성태1/21/20234084개발 환경 구성: 658. Windows에서 실행 중인 소켓 서버를 다른 PC 또는 WSL에서 접속할 수 없는 경우
13224정성태1/21/20234536Windows: 221. Windows - Private/Public/Domain이 아닌 네트워크 어댑터 단위로 방화벽을 on/off하는 방법
13223정성태1/20/20234673오류 유형: 838. RDP 연결 오류 - The two computers couldn't connect in the amount of time allotted
13222정성태1/20/20234360개발 환경 구성: 657. WSL - DockerDesktop.vhdx 파일 위치를 옮기는 방법
... [16]  17  18  19  20  21  22  23  24  25  26  27  28  29  30  ...