Bigtable용 GoogleSQL로 애플리케이션 개발

Bigtable용 GoogleSQL을 사용하여 애플리케이션에서 쿼리를 실행하여 컴퓨팅 기능을 Bigtable로 오프로드할 수 있습니다. 이렇게 하면 클라이언트 측 처리가 필요하지 않습니다. GoogleSQL은 Bigtable 및 기타 Google Cloud 서비스를 위해 구현된 ANSI 호환 구조화된 쿼리 언어입니다. Bigtable용 GoogleSQL의 기타 사용 사례에 대한 자세한 내용은 Bigtable용 GoogleSQL 개요를 참고하세요.

시작하기 전에

다음 항목이 준비되었는지 확인하세요.

  • Bigtable 인스턴스 및 테이블입니다.
  • 사용 중인 언어의 Bigtable 클라이언트 라이브러리 Bigtable용 클라이언트 라이브러리를 설치하고 사용하는 방법에 관한 자세한 내용은 Bigtable 클라이언트 라이브러리를 참고하세요.
  • 애플리케이션 기본 사용자 인증 정보 (ADC)를 통해 설정된 인증 자세한 내용은 로컬 개발 환경의 인증 설정을 참고하세요.

예시 시나리오

이 가이드의 예에서는Google Cloud 콘솔의 무료 체험판 인스턴스에서 액세스할 수 있는 샘플 weather-data 테이블을 사용합니다. station_id로 식별되는 여러 기상 관측소의 정보를 테이블에 저장한다고 가정해 보겠습니다. 각 관측소는 temp_f이라는 열에 화씨 단위의 온도를 기록합니다. 애플리케이션에서 온도를 계산하는 대신 SQL 쿼리를 사용하여 결과를 애플리케이션에 다시 보내기 전에 Bigtable에서 이러한 온도를 섭씨로 직접 변환할 수 있습니다. 날씨 데이터 테이블 쿼리에 대한 자세한 내용은 샘플 데이터 쿼리를 참고하세요.

권장사항

성능과 리소스 사용량을 최적화하려면 애플리케이션을 개발할 때 다음 권장사항을 고려하세요.

  • 클라이언트 재사용: 애플리케이션에 대해 단일 BigtableDataClient 생성 클라이언트는 gRPC 채널 관리를 처리하며 높은 동시성을 위해 설계되었습니다. 애플리케이션이 시작된 후 이 클라이언트를 한 번 만들고 모든 쿼리에 재사용하는 것이 좋습니다.
  • 매개변수화된 쿼리 사용: PreparedStatement를 사용하여 쿼리 로직을 데이터 값과 분리합니다. 이렇게 하면 Bigtable에서 실행 계획을 캐시하고 SQL 삽입을 방지할 수 있습니다.
  • 준비된 문 재사용: SQL 문자열당 PreparedStatement을 한 번만 호출합니다. 여러 요청에서 명령문을 저장하고 재사용합니다.

Bigtable 클라이언트 만들기

애플리케이션과 Bigtable 간의 연결을 처리하려면 BigtableDataClient를 만듭니다. 다음 샘플은 애플리케이션 전체에서 단일 클라이언트 인스턴스를 공유하여 연결을 효율적으로 관리하는 방법을 보여줍니다.

BigtableDataSettings settings = BigtableDataSettings.newBuilder()
    .setProject(PROJECT_ID)
    .setInstance(INSTANCE_ID)
    .build();

// The client is thread-safe and should be reused.
try (BigtableDataClient dataClient = BigtableDataClient.create(settings)) {
    System.out.println("Connected to: " + INSTANCE_ID);
}

쿼리 준비

데이터를 SQL 문자열에 직접 넣는 대신 매개변수화된 쿼리를 사용하는 것이 좋습니다. Bigtable용 GoogleSQL에서 파라미터화된 쿼리는 준비된 문과 동일합니다. 이렇게 하면 쿼리 로직을 데이터 값과 분리할 수 있습니다.

다음 샘플에서는 @sid와 같은 매개변수 자리표시자를 사용하여 GoogleSQL 쿼리를 정의하는 방법을 보여줍니다. 그런 다음 SQL 문자열과 각 매개변수의 유형을 지정하여 PreparedStatement를 만듭니다. 이렇게 하면 다음에 쿼리를 더 빠르게 실행할 수 있습니다.

Map<String, SqlType<?>> paramTypes = new HashMap<>();
paramTypes.put("sid", SqlType.string());

String sql = "SELECT " +
             "ROUND((CAST(CAST(weather['temp_f'] AS STRING) AS INT64) - 32) * 5 / 9, 2) AS temp_celsius " +
             "FROM " + TABLE_NAME + " WHERE station_id = @sid LIMIT 1";

// Create and reuse the PreparedStatement.
PreparedStatement preparedStatement = dataClient.prepareStatement(sql, paramTypes);

쿼리 실행

매개변수 값을 제공하고 쿼리를 실행합니다. 다음 샘플에서는 PreparedStatement를 사용하여 값을 매개변수에 바인딩하여 BoundStatement를 만드는 방법을 보여줍니다. 그런 다음 쿼리를 실행하고 결과를 통해 루프를 실행하여 ResultSet(섭씨 온도)를 출력합니다.

BoundStatement boundStatement = preparedStatement.bind()
    .setStringParam("sid", stationId)
    .build();

try (ResultSet resultSet = dataClient.executeQuery(boundStatement)) {
    while (resultSet.next()) {
        System.out.println("Temp: " + resultSet.getDouble("temp_celsius"));
    }
}

데이터 구조 확인

SQL을 개발하거나 탐색할 때는 테이블과 연결된 메타데이터를 탐색하여 데이터 구조를 파악하는 것이 좋습니다. Bigtable은 유연한 스키마를 사용하므로 SELECT * 쿼리를 사용하는 경우 데이터에 따라 쿼리 결과가 달라질 수 있습니다. 따라서 프로덕션 애플리케이션에서는 SELECT * 쿼리를 사용하지 않는 것이 좋습니다.

다음 샘플에서는 결과 집합의 메타데이터 (예: 열 이름 및 유형)를 살펴본 다음 데이터와는 별개로 클라이언트에 다시 전송합니다. 이렇게 하면 먼저 열 정보를 가져온 다음 애플리케이션에서 데이터를 처리하는 데 필요한 데이터 구조를 만들 수 있습니다.

ResultSetMetadata metadata = resultSet.getMetadata();
System.out.print("Columns: ");
metadata.getColumnsList().forEach(col -> System.out.print(col.getName() + " "));
System.out.println();

열 그룹 작업

Bigtable은 데이터를 column family에 저장합니다. GoogleSQL은 이러한 패밀리를 열 한정자 및 값의 으로 알려진 이름 및 값 목록으로 반환합니다. column family를 읽을 때 Bigtable은 SqlType.Map 유형을 반환합니다. 기록이 있는 테이블(예: SELECT column FROM my_table(with_history=>true))에서 column family를 읽으면 Bigtable은 column qualifier와 셀 타임스탬프 및 값의 배열인 SqlType.historicalMap() 유형을 반환합니다. SqlType 유형의 더 많은 예는 SqlType를 참고하세요.

다음 샘플은 한 번에 하나의 열을 가져오는 대신 열 그룹 전체를 한 번에 가져오는 방법을 보여줍니다. 코드는 모든 이름과 값을 반복할 수 있도록 열 패밀리를 맵에 저장합니다.

Map<String, SqlType<?>> paramTypes = new HashMap<>();
paramTypes.put("keyPrefix", SqlType.bytes());
String sql = String.format("SELECT weather FROM %s WHERE STARTS_WITH(_key, @keyPrefix)", TABLE_NAME);
PreparedStatement preparedStatement = dataClient.prepareStatement(sql, paramTypes);
BoundStatement boundStatement = preparedStatement.bind()
   .setBytesParam("keyPrefix", ByteString.copyFromUtf8(key))
   .build();


try (ResultSet resultSet = dataClient.executeQuery(boundStatement)) {
   while (resultSet.next()) {
     SqlType.Map<ByteString, ByteString> mapType = SqlType.mapOf(SqlType.bytes(), SqlType.bytes());
     Map<ByteString, ByteString> weatherValues = resultSet.getMap("weather", mapType);
     for (Map.Entry<ByteString, ByteString> entry : weatherValues.entrySet()) {
       System.out.printf("%s = %s\n",
         entry.getKey().toStringUtf8(),
         entry.getValue().toStringUtf8());
   }
 }
}

다음 단계