[삽질기] MySQL 밀리세컨드 저장 및 Go ORM에서 처리
중국내 1위 쇼핑몰인 TMall API를 이용하여 매출 데이터를 정산하는 서비스를 만들고 있습니다. 이 API를 호출하기 위해 시간 범위를 입력하게 되는데 이때 이전 작업 시간 이후 부터 처리하기 위해 이전 처리 시간을 DB에 저장하고 이를 이용하여 다음번 호출하는 형태로 만들고 있습니다. 이 서비스 개발중에 밀리세컨드 처리와 관련하여 삽질한 내용을 공유합니다.
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 드라이버를 사용해도 이슈가 있기는 한데 해결되었다고 합니다. 낮은 버전 사용할 때는 주의해야 할 것 같습니다.