BigQueryのARRAYとSTRUCTを理解して使いこなす

記事タイトルとURLをコピーする

G-gen の杉村です。BigQuery は通常の RDBMS と異なり分析用データベースであることから、非正規化したテーブルを扱うことが多くなります。そのための独特のデータ型として、ARRAY (配列) と STRUCT (構造体) があります。これらについて解説します。

概要

BigQuery は Google Cloud (旧称 GCP) の提供するサーバーレスな分析用データベースのサービスです。列志向 (カラムナ) の特徴を持ち、料金プランの選択にもよりますが、デフォルトではスキャンしたデータ量に応じた従量課金です。

クラウド上の分析用データベースのベストプラクティスとして、テーブルを非正規化することでデータ読み取りや結合のコストを下げ、パフォーマンス向上を図ることがあります。今回ご紹介する ARRAY (配列) と STRUCT (構造体) は、いずれもデータを非正規化して持つ型です。これらのデータ型には、Google Analytics 4 や Google Analytics for Firebase、Google Cloud の Billing Export などでデータを BigQuery にエクスポートするとよく出会います。

その独特のクエリの仕方が分からずエラーになり、苦しめられた人も多いのではないでしょうか。当記事ではこれらについて解説し、また組み合わせて使う ARRAY<STRUCT> についてもご紹介します。

また、最後には Google Analytics 4 のデータを模したテーブルに対するクエリの方法も例示します。

ARRAY (配列)

ARRAY とは

ARRAY (配列) とは「ゼロ個以上の同じデータ型の値で構成された順序付きリスト」のことです。以下のようなものを指します。

# INT64 の配列
[2, 3, 5, 7, 11, 13, 17, 19]
  
# STRING の配列
["国語", "算数", "理科", "社会"]

配列の型を持つテーブルは以下のようなものになります。

(行番号) name subjects
1 田中 太郎 国語
算数
理科
社会
2 鈴木 二郎 国語
算数

※ (行番号) 列は一行のまとまりを示すために便宜的に割り振ったものであり、テーブル内のデータではありません。

上記のテーブルの subjects 列は ARRAY であり、1行に2つ以上の値が入っています。

これはリレーションの第1正規形に違反しており、通常のリレーショナル・データベースではこのようなデータの持ち方をしません。しかし BigQuery のような分析用データベースではこのような配列型を使いリレーションを維持したまま、マスタとトランザクションを分けない大福帳的なテーブル構成にすることで、JOIN のコストが下がりパフォーマンス向上を図ることができます。

サンプルテーブル

ここからは実際の SQL を紹介しながらイメージを掴んでいきます。以下の WITH 句を仮想的なサンプルテーブルとして使います。

name 列が通常の STRING 型で、subjects 列は STRING 型の値を要素に持つ配列型です。

WITH t0 AS (
  SELECT
    "田中 太郎" AS name,
    ["国語", "算数", "理科", "社会"] AS subjects
  UNION ALL
  SELECT
    "鈴木 二郎" AS name,
    ["国語", "算数"] AS subjects
)
  
SELECT
  *
FROM
  t0;
(行番号) name subjects
1 田中 太郎 国語
算数
理科
社会
2 鈴木 二郎 国語
算数

SELECT

まず SELECT 文の例を見ることでイメージを掴みます (以降、WITH 句は省略します。先ほどの SQL 文後半の SELECT 以降を書き換えてお試しください)。

SELECT
  name,
  subjects
FROM
 t0;

上記のように、SELECT 文は通常の列をクエリするときと何ら代わりありません。出力結果は以下のようになり、配列内の全ての値が返ります。

(行番号) name subjects
1 田中 太郎 国語
算数
理科
社会
2 鈴木 二郎 国語
算数

SELECT 〜 WHERE

WHERE 句でフィルタをしようと考えたとき、最初の壁に当たります。

SELECT
  name,
  subjects
FROM
  t0
WHERE
  subjects = "理科";

この SQL は、以下のようなエラーになります。

No matching signature for operator = for argument types: ARRAY<STRING>, STRING. Supported signature: ANY = ANY at [17:3]

= の左辺と右辺で異なる型 (左辺は ARRAY である subjects 列、右辺は STRING) を指定していることからエラーになっています。なお右辺を ["国語", "算数"] のように配列にしてもエラーになります (Equality is not defined for arguments of type ARRAY<STRING> at [7:5])。= 演算子はそもそも配列型をサポートしていません。

一方で、以下のクエリは成功します。

SELECT
  name,
  subjects
FROM
  t0
WHERE
  "理科" IN UNNEST(subjects);
(行番号) name subjects
1 田中 太郎 国語
算数
理科
社会

ここでは UNNEST 関数を用いています。UNNEST() は配列内の全要素を各行に割り振った「テーブル」を返します。以下の例を見てください。

SELECT
  *
FROM
  UNNEST([1, 2, 3]);
(行番号) f0_
1 1
2 2
3 3

このように配列が UNNEST() されると、各要素が1行1行に割り振られた「テーブル形式」になることが分かります。先程の WHERE 句では UNNEST() で subjects 列の中身を分解してテーブル化し、その結果の中に "理科" が含まれているかを IN 演算子で検査して、TRUE となった行だけを返したのです。

SELECT 〜 CROSS JOIN

配列型を含むテーブルに自在にフィルタを行うには、UNNEST() と CROSS JOIN を使いこなす必要があります。

一例を示します。

SELECT
  name,
  unnested_subject
FROM
  t0
CROSS JOIN
  UNNEST(subjects) AS unnested_subject;
(行番号) name unnested_subject
1 田中 太郎 国語
2 田中 太郎 算数
3 田中 太郎 理科
4 田中 太郎 社会
5 鈴木 二郎 国語
6 鈴木 二郎 算数

上記のクエリでは元テーブルの全行に対して1行ずつ「subjects を UNNEST して返ってきたテーブル」を CROSS JOIN しています。これにより、配列が分解されて通常の第1正規化されたテーブルのようになり、WHERE でフィルタできます。

SELECT (SELECT ~ UNNEST)

以下のようなクエリも用いることができます。

SELECT
  name,
  (SELECT * FROM UNNEST(subjects) AS u WHERE u = "理科") AS subject_science
FROM
  t0;
(行番号) name subject_science
1 田中 太郎 理科
2 鈴木 二郎 null

SELECT 句内のサブクエリの SELECT は、UNNEST() した subjects 列を対象としており、その結果は WHERE で "理科" だけに絞られています。

subjects 列が "理科" の値を持っていない行は null として返ります。

ただしこの例は、(SELECT * FROM UNNEST(subjects) AS u WHERE u = "理科") の結果が単一行だけ返る (スカラ値である) ことが前提です。もし配列に "理科" が複数ある場合、Scalar subquery produced more than one element エラーが生じてしまいます。

例えば以下のように、データが重複してしまっているテーブルの場合です。

(行番号) name subjects
1 田中 太郎 国語
算数
理科
社会
2 鈴木 二郎 国語
算数
3 三中 三郎 国語
理科
理科

三中 三郎の subjects 列に理科が2行あるため、UNNEST に対する SELECT 結果が2行になってしまい、クエリが失敗します。

このようなデータの不整合がある可能性に対応するには、以下のようにします。

SELECT
  name,
  (SELECT DISTINCT * FROM UNNEST(subjects) AS u WHERE u = "理科") AS subject_science
FROM
  t0;
(行番号) name subjects
1 田中 太郎 理科
2 鈴木 二郎 null
3 三中 三郎 理科

DISTINCT することで返却結果が一意に絞られ、エラーになりません。

CREATE TABLE / INSERT

順序としてはあべこべですが、ARRAY を持つ表の CREATE TABLE および INSERT 文も紹介します。

CREATE TABLE はシンプルです。ARRAY の列を ARRAY<(型名)> として定義するのみです。

CREATE TABLE `my-project.my_dataset.t0`
(
  name STRING,
  subjects ARRAY<STRING>
);

INSERT 文は以下です。

INSERT
  `my-project.my_dataset.t0`
VALUES
  ("鈴木 二郎", ["国語", "算数"]);

上記の ["国語", "算数"]ARRAY["国語", "算数"]ARRAY<STRING>["国語", "算数"] のように表現することもできます (意味は変わりません)。

制限

配列の配列を作ることはできません。つまり、以下のようにネストした配列は作れません。

SELECT
  ["hogehoge", ["foo", "bar"]];

Cannot construct array with element type ARRAY<STRING> because nested arrays are not supported at [2:16]

一方で配列の中に、後に紹介する STRUCT 型は入れることができます。これが Google Analytics 4 の BigQuery Export 等で見られる独特な構成です。これについては後に解説します。

SELECT [
  STRUCT("hoge01" AS f1, "fuga01" AS f2),
  STRUCT("hoge02", "fuga02")
] AS arrayed_struct;
(行番号) arrayed_struct.f1 arrayed_struct.f2
1 hoge01 fuga01
hoge02 fuga02

STRUCT (構造体)

STRUCT とは

BigQuery の STRUCT (構造体) 型は、構造を持った型で、一つ以上のフィールドを持つことができます。

例えば以下の表では、item という列が STRUCT 型で、id という INT64 型のフィールドと、name という STRING 型のフィールドを持っています。

price item.id item.name
3000 10001 T-shirt
8000 20001 jacket

STRUCT はデータを構造的に整理するために有用です。

サンプルテーブル

STRUCT 型を持つテーブルの例として、以下の WITH 句を仮想的なサンプルテーブルとして使います。

school 列が STRUCT 型で、フィールドとして year (INT64 型) と class (STRING 型) を持っています。

WITH t1 AS (
  SELECT
    "田中 太郎" AS name,
    STRUCT(1 AS year, "B" AS class) AS school
  UNION ALL
  SELECT
    "鈴木 二郎" AS name,
    STRUCT(2 AS year, "C" AS class) AS school
)
  
SELECT
  *
FROM
  t1;
(行番号) name school.year school.class
1 田中 太郎 1 B
2 鈴木 二郎 2 C

SELECT

STRUCT 型に対する SELECT は、他の通常の型と大きく変わらないため、直感的に理解できます。

このようなテーブルに対するクエリは以下のようになります。

SELECT
  name,
  school
FROM
  t1;
(行番号) name school.year school.class
1 田中 太郎 1 B
2 鈴木 二郎 2 C

school という列名を指定するだけで、配下のフィールドの全てが選択されます。

以下のように、フィールド名を明示して SELECT することも可能です。

SELECT
  name,
  school.year
FROM
  t1;
(行番号) name year
1 田中 太郎 1
2 鈴木 二郎 2

なおこの場合、返却結果の列名はフィールド名だけになります。

SELECT 〜 WHERE

ARRAY と異なり、STRUCT 型への WHERE 句の使用は直感的です。

SELECT
  name,
  school
FROM
  t1
WHERE
  school.year = 1;
(行番号) name school.year school.class
1 田中 太郎 1 B

WHERE 句でフィールドに対して条件を指定するだけで、フィルタをすることができます。

CREATE TABLE / INSERT

STRUCT 型を持つテーブルの CREATE TABLE 文は、以下のように記述します。STRUCT<(フィールド名) (型名), (フィールド名) (型名), ...> のように各フィールドの名前と型を指定します。

CREATE TABLE `my-project.my_dataset.t1`
(
  name STRING,
  school STRUCT<year INT64, class STRING>
);

INSERT 文は以下です。

INSERT INTO
    `my-project.my_dataset.t1`
VALUES
    ("鈴木 二郎", STRUCT(2 AS year, "C" AS class));

なお AS year AS class のようなフィールド名指定は省略可能で、実は可読性を高める以外の意味はありません。フィールド名を指定したところで記載順番が優先されます。すなわち上記を "C" AS class, 2 AS year と書き換えると、型エラーになります。逆に、型が同じだと意図しない列に値が入ってしまいます。

以下のように簡易的に記述することもできます。

INSERT INTO
    `my-project.my_dataset.t1`
VALUES
    ("鈴木 二郎", (2 , "C"));

制限

STRUCT 型はネストすることができますが、最大で15段階までです。

ネストした STRUCT 型の例は以下です。

SELECT
  STRUCT(
    1001 AS f_int,
    "hoge" AS f_string,
    STRUCT(
      "foo" AS f1,
      "bar" AS f2
    ) AS f_struct
  ) AS root;
(行番号) root.f_int root.f_string root.f_struct.f1 root.f_struct.f2
1 1001 hoge foo bar

ARRAY<STRUCT> (ネストされた繰り返し列)

ARRAY<STRUCT> とは

ARRAY<STRUCT> (ネストされた繰り返し列) とはここまで紹介した ARRAY と STRUCT を組み合わせたものです。

ARRAY の列の中に、要素として STRUCT 型が入っているものです。Google Analytics 4 (Firebase) や Google Cloud の Billing Export でこの形式が見られます。

実例としては、以下のようなものです。

(行番号) name classes.subject classes.teacher
1 田中 太郎 国語 斉藤 三郎
英語 伊東 四朗
2 鈴木 二郎 英語 伊東 四朗

classes 列は ARRAY (配列) 型ですが、その各要素は subject と teacher というフィールドを持つ STRUCT 型です。

サンプルテーブル

サンプルテーブルとして、以下の WITH 句を使って試してください。

classes 列が「ネストされた繰り返し列」です。配列であり、各要素は subject という STRING 型フィールドと teacher という STRING 型フィールドを持った STRUCT です。

WITH t2 AS (
  SELECT
    "田中 太郎" AS name,
    [
      STRUCT("国語" AS subject, "斉藤 三郎" AS teacher),
      STRUCT("英語" AS subject, "伊東 四朗" AS teacher)
    ] AS classes
  UNION ALL
  SELECT
    "鈴木 二郎" AS name,
    [
      STRUCT("英語" AS subject, "伊東 四朗" AS teacher)
    ] AS classes
)
  
SELECT
  *
FROM
  t2;
(行番号) name classes.subject classes.teacher
1 田中 太郎 国語 斉藤 三郎
英語 伊東 四朗
2 鈴木 二郎 英語 伊東 四朗

SELECT

classes 列を指定して SELECT することは可能です。

SELECT
  name,
  classes
FROM
  t2;

しかし、以下のように classes 列のフィールドを指定してクエリしようとすると、エラーになります。

SELECT
  name,
  classes.subject
FROM
  t2;

Cannot access field subject on a value with type ARRAY<STRUCT<subject STRING, teacher STRING>> at [18:11]

classes は ARRAY です。classes 列の subject だけを取り出したい場合、以下のように UNNEST() してテーブル化し、そこから subject 列だけを取り出すことができます。ARRAY を先頭につけないと、単体の値 (スカラ値) が返ってくることが期待され Scalar subquery produced more than one element になってしまうため、ARRAY() として戻りが配列であることを明示します。

SELECT
  name,
  ARRAY(SELECT subject FROM UNNEST(classes)) AS subject
FROM
  t2;
(行番号) name subject
1 田中 太郎 国語
英語
2 鈴木 二郎 英語

SELECT 〜 WHERE (エラー)

WHERE を使ったフィルタも独特です。以下のクエリはエラーになります。

SELECT
  name,
  classes
FROM
  t2
WHERE
  classes.subject = "国語";

Cannot access field subject on a value with type ARRAY<STRUCT<subject STRING, teacher STRING>> at [22:11]

先程と同じ理由で、配列である classes の一要素の subject フィールドにはアクセスできません。

ARRAY<STRUCT> をフィルタするには、次に示すようなクエリを用います。

SELECT 〜 CROSS JOIN

以下のクエリは通ります。

SELECT
  name,
  c.subject,
  c.teacher
FROM
  t2
CROSS JOIN
  UNNEST(classes) AS c
WHERE
  c.subject = "国語";
(行番号) name subject teacher
1 田中 太郎 国語 斉藤 三郎

ARRAY である classes 列を毎行 unnest して CROSS JOIN し、第1正規化された状態のテーブルに WHERE でフィルタをかけています。

SELECT (SELECT ~ UNNEST)

以下ようなクエリもできます。

SELECT
  name,
  (
    SELECT AS STRUCT
      subject, teacher
    FROM
      UNNEST(classes)
    WHERE
      subject = "国語"
  ) AS class
FROM
  t2;
(行番号) name class.subject class.teacher
1 田中 太郎 国語 斉藤 三郎
2 鈴木 二郎 null null

こちらもやっていることは似ており、SELECT 句の中のサブクエリで毎行 classes を UNNEST して subject と teacher を取り出し、その結果を WHERE 句で絞っています。

SELECT で指定する列は単一の値 (スカラ値) を期待するので SELECT AS STRUCT を使うことで、戻りが STRUCT であることを明示的に指示しています。AS STRUCT をつけないと以下のようなエラーになります。

Scalar subquery cannot have more than one column unless using SELECT AS STRUCT to build STRUCT values at [18:3]

もしくは AS STRUCT をつけなくても、SELECT する列が subject か teacher の一つだけであればエラーになりません。

CREATE TABLE / INSERT

ARRAY<STRUCT> の CREATE TABLE 文は以下のようになります。

CREATE TABLE `my-project.my_dataset.t2`
(
  name STRING,
  classes ARRAY<
    STRUCT<
      subject STRING,
      teacher STRING
    >
  >
);

INSERT 文は以下です。

INSERT
  `my-project.my_dataset.t2`
VALUES(
  "田中 太郎", [
    STRUCT(
      "国語" AS subject,
      "斉藤 三郎" AS teacher
    ),
    STRUCT(
      "英語" AS subject,
      "伊東 四朗" AS teacher
    )
  ]
);

なお通常の STRUCT 型と同じで AS subject AS teacher のようなフィールド名の指定は省略可能で、むしろスキーマ定義と同じ順番で記載する必要があります。

実践 : Google Analytics 4 データへのクエリ

概要

Google Analytics 4 (GA4) や Firebase ではアクセス情報を BigQuery にエクスポートすることができます。簡単な設定で BigQuery に定期的にデータを吐き出してくれるので便利ですが、そのスキーマには当記事で紹介した ARRAY<STRUCT> が使われており、クエリの仕方が独特です。

当項目では、GA4 のエクスポートデータのテーブルを模したテーブルに対するクエリの方法を簡単にご紹介します。

テーブル例

以下のような模擬テーブルを用います。

フィールド名 種類 モード
event_timestamp INTEGER NULLABLE
event_name STRING NULLABLE
event_params RECORD (=STRUCT) REPEATED (=ARRAY)
├ key STRING NULLABLE
└ value RECORD (=STRUCT) NULLABLE
 ├ string_value STRING NULLABLE
 ├ int_value INTEGER NULLABLE
 ├ float_value FLOAT NULLABLE
 └ double_value FLOAT NULLABLE

データは以下のように入っています。

GA4 データを模したテーブル

クエリ例1: 特定ページの PV 数を集計

特定ページの PV 数を計測するため event_name"page_view" で、かつ page_location"https://blog.g-gen.co.jp/entry/deploy-preview-using-cloud-run-tagged-revision" という文字列となっているレコードを選択したいとします。

以下のようにしたいところですが、エラーになります。

SELECT
  COUNT(*) AS pv
FROM
  `my-project.my_dataset.ga4_mock`
WHERE
  event_name = "page_view"
AND
  event_params.value.string_value = "https://blog.g-gen.co.jp/entry/deploy-preview-using-cloud-run-tagged-revision";

Cannot access field value on a value with type ARRAY<STRUCT<key STRING, value STRUCT<string_value STRING, int_value INT64, float_value FLOAT64, ...>>> at [8:16]

フィルタ対象の event_params は ARRAY であり、その要素である event_params.value は STRUCT 型でありそのフィールドの一つが string_value です。どのようにクエリすれば良いのか、頭がこんがらがってしまいます。

以下のようなクエリが通ります。

SELECT
  COUNT(*) AS pv
FROM
  `my-project.my_dataset.ga4_mock`
WHERE
  event_name = "page_view"
AND
  "https://blog.g-gen.co.jp/entry/deploy-preview-using-cloud-run-tagged-revision"
  IN (SELECT value.string_value FROM UNNEST (event_params) WHERE key = "page_location")
(行番号) pv
1 1

このクエリでは、event_name 列が "page_view" の行に対し、配列である event_param 列を UNNEST して平準化テーブルとし、その中で key が "page_location" である行の value.string_value フィールドを IN で検査しています。

また別案として、以下のようなビューを作ってしまうのも手です。

SELECT
  event_timestamp,
  event_name,
  (
    SELECT
      value.string_value
    FROM
      UNNEST(event_params)
    WHERE
      key = "page_location"
  ) AS page_location
FROM
  `my-project.my_dataset.ga4_mock`
WHERE
  event_name = "page_view";
(行番号) event_timestamp event_name page_location
1 1695781266341693 page_view https://blog.g-gen.co.jp/entry/deploy-preview-using-cloud-run-tagged-revision

このように page_view イベントの page_location 値のビューだけを作ってしまえば、あとは一般的な SQL でクエリできます。CREATE VIEW AS でビューを作るか、一時的・探索的なクエリなら WITH 句を使っても良いでしょう。

WITH unnested_view AS (
  SELECT
    event_timestamp,
    event_name,
    (
      SELECT
        value.string_value
      FROM
        UNNEST(event_params)
      WHERE
        key = "page_location"
    ) AS page_location
  FROM
    `my-project.my_dataset.ga4_mock`
  WHERE
    event_name = "page_view"
)
  
SELECT 
  COUNT(*) AS pv
FROM
  unnested_view
WHERE
  page_location = "https://blog.g-gen.co.jp/entry/deploy-preview-using-cloud-run-tagged-revision";
(行番号) pv
1 1

上記のように UNNEST して平坦化した状態をデータマートやビューとして持っておけば、BI ツールや Connected Sheets からも利用しやすくなります。

このように ARRAY 型や ARRAY<STRUCT> 型は、SELECT 句または WHERE 句のサブクエリで UNNEST() 関数を使うことで、自在に扱うことができます。

クエリ例2 : string_value を取り出す関数を作る

前述の例だとサブクエリが長く、可読性に劣ります。UNNEST() する処理を関数に切り出して可読性を高めてみます。

CREATE TEMP FUNCTION getStringValue(col ANY TYPE, col_key STRING)
AS (
  (SELECT c.value.string_value FROM UNNEST(col) c WHERE c.key = col_key)
);
  
SELECT
  event_timestamp,
  event_name,
  getStringValue(event_params, "page_title") AS page_title,
  getStringValue(event_params, "page_location") AS page_location
FROM
  `my-project.my_dataset.ga4_mock`

結果は以下のようになります。

(行番号) event_timestamp event_name page_title page_location
1 1695781266341693 page_view プルリクエストをトリガとするCloud Runのプレビュー環境自動デプロイを実装してみた - G-gen Tech Blog https://blog.g-gen.co.jp/entry/deploy-preview-using-cloud-run-tagged-revision
2 1695781266341693 first_visit プルリクエストをトリガとするCloud Runのプレビュー環境自動デプロイを実装してみた - G-gen Tech Blog https://blog.g-gen.co.jp/entry/deploy-preview-using-cloud-run-tagged-revision

スキーマ表記

概要

BigQuery でのスキーマ表記は「SQL 上の表記」と「Web コンソールや JSON 形式での表記」が異なる場合があります。

例えば前述の ARRAY<STRUCT> は「タイプが RECORD でモードが REPEATED」のように表現されます。

以下に参考情報として記載します。

ARRAY

BigQuery コンソール上や JSON 形式でのスキーマ表現では、ここまで扱ったような ARRAY は以下のように「種類が (配列内のデータの型名) でモードが REPEATED」として表現されます。

配列は「モード」が REPEATED になっている

{
〜略〜
  "schema": {
    "fields": [
      {
        "name": "name",
        "type": "STRING"
      },
      {
        "mode": "REPEATED",
        "name": "subjects",
        "type": "STRING"
      }
    ]
  },
〜略〜
}

STRUCT

BigQuery コンソール上や JSON 形式でのスキーマ表現では、STRUCT は以下のように「種類が RECORD でモードが NULLABLE/REQUIRED」として表現されます。

STRUCT は RECORD という種類として表記

{
〜略〜
  "schema": {
    "fields": [
      {
        "name": "name",
        "type": "STRING"
      },
      {
        "fields": [
          {
            "name": "year",
            "type": "INTEGER"
          },
          {
            "name": "class",
            "type": "STRING"
          }
        ],
        "name": "school",
        "type": "RECORD"
      }
    ]
  },
〜略〜
}

ARRAY<STRUCT>

BigQuery コンソール上や JSON 形式でのスキーマ表現では、ARRAY<STRUCT> は以下のように「種類が RECORD でモードが REPEATED」として表現されます。RECORD は STRUCT と、REPEATED は ARRAY と同義です。

「種類」が RECORD (=STRUCT)、「モード」が REPEATED (=ARRAY)

JSON では以下のようになります。

{
〜略〜
  "schema": {
    "fields": [
      {
        "name": "name",
        "type": "STRING"
      },
      {
        "fields": [
          {
            "name": "subject",
            "type": "STRING"
          },
          {
            "name": "teacher",
            "type": "STRING"
          }
        ],
        "mode": "REPEATED",
        "name": "classes",
        "type": "RECORD"
      }
    ]
  },
〜略〜
}

杉村 勇馬 (記事一覧)

執行役員 CTO / クラウドソリューション部 部長

元警察官という経歴を持つ現 IT エンジニア。クラウド管理・運用やネットワークに知見。AWS 12資格、Google Cloud認定資格11資格。X (旧 Twitter) では Google Cloud や AWS のアップデート情報をつぶやいています。