Golang으로 Redshift 접속 후 쿼리실행까지

생각해보니 저번에 글을 쓰고 생성만 보여주고 정작 중요한 보낸Query의 Return값을 받는 방법을 안 썼다는 것이 생각났다

지금은 저번 Quick Start코드와는 완전 다른 무언가가 되어있지만 Quick Start 의 코드로도 충분하니 Quick Start 에 적힌 코드를 참고해보자

주목해야 할 곳은 104번째 줄 116번째 줄 그리고 130번째 줄에 적혀있는

  • redshiftclient.ExecuteStatement
  • redshiftclient.DescribeStatement
  • redshiftclient.GetStatementResult

이 3종류의 함수이다. 우선 ExecuteStatement는 Redshift에 Query 의 실행을 요청한다. 이 쿼리에는 Redshift가 Id를 부여하여 관리한다. 다만 Query는 처리 시간이 필요하다. 이 처리 상태와 처리 이후 결과의 상태를 확인할 수 있는 것이 DescribeStatement이다.

DescribeStatement의 Response Syntax 를 보면 정말 다양한 정보가 확인이 가능하다.

{
   "ClusterIdentifier": "string",
   "CreatedAt": number,
   "Database": "string",
   "DbUser": "string",
   "Duration": number,
   "Error": "string",
   "HasResultSet": boolean,
   "Id": "string",
   "QueryParameters": [
      {
         "name": "string",
         "value": "string"
      }
   ],
   "QueryString": "string",
   "RedshiftPid": number,
   "RedshiftQueryId": number,
   "ResultRows": number,
   "ResultSize": number,
   "SecretArn": "string",
   "Status": "string",
   "SubStatements": [
      {
         "CreatedAt": number,
         "Duration": number,
         "Error": "string",
         "HasResultSet": boolean,
         "Id": "string",
         "QueryString": "string",
         "RedshiftQueryId": number,
         "ResultRows": number,
         "ResultSize": number,
         "Status": "string",
         "UpdatedAt": number
      }
   ],
   "UpdatedAt": number
}

어느 Redshift cluster에 보냈는지 언제 보냈는지 유저, 실행시간 에러여부, 상태 등등… 그리고 Return할 Result가 있는지와 그 Result에대한 정보가 적혀있다.

133번째 줄에서는 이 중에서 status정보를 활용하여 Query의 성공여부를 1차적으로 판단한다.

만약 status가 FINISHED이면 142번째 줄에서는 HasResultSet을통해 Query의 Return이 있는지 확인한다.

저번 포스트의 Create Table같은 경우에는 Return이 없지만 전형적인 SELECT FROM WHERE 문에서는 Return이 있을 것이다.

이 Return의 상세한 정보를 받아오는 것이 143번째 줄의 GetStatementResult함수이다.

GetStatementResult의 Response Syntax 는 다음과 같다

{
   "ColumnMetadata": [
      {
         "columnDefault": "string",
         "isCaseSensitive": boolean,
         "isCurrency": boolean,
         "isSigned": boolean,
         "label": "string",
         "length": number,
         "name": "string",
         "nullable": number,
         "precision": number,
         "scale": number,
         "schemaName": "string",
         "tableName": "string",
         "typeName": "string"
      }
   ],
   "NextToken": "string",
   "Records": [
      [
         {
            "blobValue": blob,
            "booleanValue": boolean,
            "doubleValue": number,
            "isNull": boolean,
            "longValue": number,
            "stringValue": "string"
         }
      ]
   ],
   "TotalNumRows": number
}

독특해 보이면서도 생각해보면 당연한 구조를 하고 있다. 우선 ColumnMetadata에는 해당 Column에대한 정보를 제공한다. 이름, 소속table, type, Default값이 있는가 CaseSensitive한가 등등의 정보를 제공한다.

다음 Records에 2D array형태로 row들의 정보가 적혀있다. 이 때 가로는 위의 ColumnMetadata와 같은 순서의 Column이며 세로로는 row들이다.

즉 참고할 때는 Records[n번째].ColumnName이 아니라 Records[a][b]같은 식으로 구현을 해야한다. 이 방식의 장점은 column name에 신경쓰지않고 데이터를 빼낼 수 있다는 점이다.

그렇게 지정을 하면 Go에서는 Field 라고 불리는 type의 데이터에 접근이 가능하다.

이 Field의 특이한 점은 해당하는 곳에만 데이터가 있는 구조체라는 점이다.

즉 만약 해당 column이 string이면 stringValue에만 string이 적혀있고 나머지는 null이다. 어느 Query에 대해 return중 한 줄을 출력시키면 아래와 같이 나온다.

[{\n  LongValue: 155590\n} {\n  StringValue: \"t1\"\n} {\n  LongValue: 2200\n} {\n  StringValue: \"AUTO(ALL)\"\n} {\n  LongValue: 0\n} {\n  StringValue: \"true\"\n} {\n  StringValue: \"2021-11-16 15:23:23.779821\"\n}]

이것이 결과의 0번째 줄이라고 하고 여기서 처음의 LongValue155590이 필요하다면 (*redshiftdataapiservice.GetStatementResultOutput).Records[0][0].LongValue 와 같은 식으로 구현할 수 있다.

복잡하게 느껴질 수 있지만 TotalNumRows를 활용한 for문으로 변수를 잘 정리해두면 복잡하지는 않다.

다만 각각의 Query 전용 Parser를 구성해야한다는 점은 피해갈 수 없다.

ColumnMetadata와도 합의를 잘 보면 좀 더 간단하게 가능 할 수도??