[삽질기] MySQL 밀리세컨드 저장 및 Go ORM에서 처리

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

go_mysql

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)

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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에는 다음과 같이 상수 정의가 되어 있습니다.

1
2
3
4
5
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로 변경하는 코드를 다음과 같이 자동으로 추가합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 드라이버를 사용해도 이슈가 있기는 한데 해결되었다고 합니다. 낮은 버전 사용할 때는 주의해야 할 것 같습니다.

Popit은 페이스북 댓글만 사용하고 있습니다. 페이스북 로그인 후 글을 보시면 댓글이 나타납니다.