> ## Content Index
> Fetch the complete content index at: https://www.popit.kr/llms.txt
> Use this file to discover other available public pages before exploring further.

# [삽질기] MySQL 밀리세컨드 저장 및 Go ORM에서 처리
- URL: https://www.popit.kr/mysql-millisecond-storage-go-orm/
- Published: 2017-06-21T15:17:55.000Z
- Updated: 2017-06-27T16:37:41.000Z
- Author: 김형준
- Tags: #소프트웨어, mysql, Go, Datetime, 밀리세컨드, nvarchar

중국내 1위 쇼핑몰인 TMall API를 이용하여 매출 데이터를 정산하는 서비스를 만들고 있습니다. 이 API를 호출하기 위해 시간 범위를 입력하게 되는데 이때 이전 작업 시간 이후 부터 처리하기 위해 이전 처리 시간을 DB에 저장하고 이를 이용하여 다음번 호출하는 형태로 만들고 있습니다. 이 서비스 개발중에 밀리세컨드 처리와 관련하여 삽질한 내용을 공유합니다.

![go_mysql](https://www.popit.kr/content/images/wp/2017/06/go_mysql-600x258.png)

## MySQL의 datetime 타입

이 작업의 상태 정보를 저장하기 위해 MySQL의 "datetime" 타입을 사용하였는데 기본 사이즈로 컬럼을 지정하면 밀리세컨드를 저장하지 않는다고 되어 있습니다.

> A DATETIME or TIMESTAMP value can include a trailing fractional seconds part in up to microseconds (6 digits) precision. In particular, any fractional part in a value inserted into a DATETIME or TIMESTAMP column is stored rather than discarded. With the fractional part included, the format for these values is 'YYYY-MM-DD HH:MM:SS\[.fraction\]', the range for DATETIME values is '1000-01-01 00:00:00.000000' to '9999-12-31 23:59:59.999999', and the range for TIMESTAMP values is '1970-01-01 00:00:01.000000' to '2038-01-19 03:14:07.999999'. The fractional part should always be separated from the rest of the time by a decimal point; no other fractional seconds delimiter is recognized.  
>  
>  
>  
> ([https://dev.mysql.com/doc/refman/5.7/en/datetime.html](https://dev.mysql.com/doc/refman/5.7/en/datetime.html?ref=popit.kr))

밀리세컨드까지 저장하려면 datetime(6) 를 사용해야 합니다.

## Golang의 ORM에서 밀리세컨드 지원

MySQL로 삽질하는 중에 더 미궁속으로 빠지게 만든 놈이 Golang의 ORM 인데 주로 두가지 종류의 ORM 을 사용하고 있습니다. 하나는 xorm이고 하나는 beego orm 입니다. 이들 ORM은 기본적으로 밀리세컨드 단위로 포맷 변환을 지원하지 않습니다. 엄밀하게 말하면 지원하지 않는다라기 보다는 MySQL 과 같이 사용할 때 지원되지 않는다라고 볼 수 있습니다. xorm의 formatTime() 함수는 다음과 같이 구현되어 있습니다.  

```go
func (engine *Engine) formatTime(sqlTypeName string, t time.Time) (v interface{}) {
  switch sqlTypeName {
  case core.Time:
    s := t.Format("2006-01-02 15:04:05") //time.RFC3339
    v = s[11:19]
  case core.Date:
    v = t.Format("2006-01-02")
  case core.DateTime, core.TimeStamp:
    v = t.Format("2006-01-02 15:04:05")
  case core.TimeStampz:
    if engine.dialect.DBType() == core.MSSQL {
      v = t.Format("2006-01-02T15:04:05.9999999Z07:00")
    } else {
      v = t.Format(time.RFC3339Nano)
    }
  case core.BigInt, core.Int:
    v = t.Unix()
  default:
    v = t
  }
  return
}
```

위 코드에서 보면 Type이 "TIMESTAMPZ" 인 경우 nano seconds까지 지원하고 있습니다. 하지만 MySQL에는 TIMESTAMPZ 타입이 없습니다(제가 구글링 해본 결론인데 혹시 있다면 알려주세요.)

또 다른 ORM인 beego의 ORM에는 다음과 같이 상수 정의가 되어 있습니다.  

```go
const (
  formatTime     = "15:04:05"
  formatDate     = "2006-01-02"
  formatDateTime = "2006-01-02 15:04:05"
)

```

따라서 MySQL + Go의 ORM 조합인 경우 밀리세컨드 표현에 있어 주의해야 할 것 같습니다.

## SQLServer의 NVARCAHR 와 Golang

또 하나 삽질은 SQLServer와의 Go ORM의 조합입니다. SQLServer의 데이터 타입에는 varchar 이외에 nvarchar가 있습니다. UTF8 문자를 저장하기 위해 사용하는 타입이라고 합니다.

문제는 이 nvarchar 타입에 index가 잡혀 있고, go의 sqlserver 드라이버를 이용하여 where 조건에 해당 컬럼을 지정하는 경우 문제가 발생할 수 있습니다. go sqlserver 드라이버는 string 타입을 nvarchar로 변경하는 코드를 다음과 같이 자동으로 추가합니다.  

```go
func makeStrParam(val string) (res Param) {
  res.ti.TypeId = typeNVarChar
  res.buffer = str2ucs2(val)
  res.ti.Size = len(res.buffer)
  return
}

func (s *MssqlStmt) makeParam(val driver.Value) (res Param, err error) {
  ...
  switch val := val.(type) {
  case int64:
    res.ti.TypeId = typeIntN
    res.buffer = make([]byte, 8)
    res.ti.Size = 8
    binary.LittleEndian.PutUint64(res.buffer, uint64(val))
  case string:
    res = makeStrParam(val)
  ...
}
```

이렇게 되면 index가 잡혀 있다 하더라도 index를 충분히 사용하지 못하는 문제가 있습니다. SQLServer와 GO 사용 시 주의해서 사용해야 할 것 같습니다.

## Update:

- 2017/06/28
  - 이 건과 관련된 삽질이 마무리가 된 것 같았는데 다시 몇시간 고생했습니다. 원인은 스테이징 장비는 MySQL이 아니라 MariaDB 였습니다. MariaDB를 사용하면서 MySQL JDBC 드라이버를 사용하면 여전히 밀리세컨드를 저장하지 못하는 문제가 있습니다.
  - MariaDB의 JDBC 드라이버를 사용해도 이슈가 있기는 한데 해결되었다고 합니다. 낮은 버전 사용할 때는 주의해야 할 것 같습니다.
    - [https://mariadb.atlassian.net/browse/CONJ-107](https://mariadb.atlassian.net/browse/CONJ-107?ref=popit.kr)