BigQueryのArrayを理解する。

23 min読了の目安(約20900字TECH技術記事

目的

BigQueryを利用していると、
BillingやinformationSchemaなどでArrayが利用されており、
Arrayを自由に使いこなせるとBigQueryをさらに楽しめるのではないかと思っています。

ただ、
Arrayは正直よくわからないので、
理解するための軌跡を記載しようと思いました。

参考資料

BQ公式ドキュメント(Array)

Arrayとは

公式ドキュメントを引用します。

名前 説明
ARRAY ARRAY型ではないゼロ以上の要素の順序付きリスト。

制限

  • NULL要素を含むARRAYはクエリ内で使用できますが、
    そのようなARRAYがクエリ結果にあると、BigQueryでエラーが発生します。
  • クエリ内ではNULLと空のARRAYは2つの別個の値ですが、
    クエリ結果でBigQueryはNULL ARRAYを空のARRAYに変換します。

使ってみる

SELECT [1, 2, 3] as numbers;

image-1.png

レコードを見ると、1レコードしか出力していないことがわかります。

#ARRAYの型宣言
公式ドキュメントを引用します。

型の宣言 説明
ARRAY<INT64> 64 ビット整数のシンプルなARRAY。
ARRAY<STRUCT<INT64, INT64>> STRUCTからなるARRAY。各STRUCT には64 ビット整数が2 つ含まれます。
ARRAY<ARRAY<INT64>> サポート対象外
ARRAY<STRUCT<ARRAY<INT64>>> 64 ビット整数のARRAYからなるARRAY。ARRAYには直接他のARRAYを含めることができないため、2 つのARRAYの間にSTRUCTが挿入されています。

ここでのポイントは、ARRAYに直接ARRAYを保持することできない。
それをするためにはSTRUCTを利用しましょうってことですね。

ここからはARRAYの使い方の解説をします。

ARRAYの作成

[]を利用した作成

SELECT
  ARRAY<FLOAT64>[1, 2, 3] as floats,
  [1, 2, 3] as numbers;

型を指定することができることを確認しましょう。
同じ値を使っても型を指定することで、結果が変わっていることがわかります。
image-2.png

生成した結果をARRAYで作成

GENERATE_ARRAYを利用する。

GENERATE_ARRAY(start_expression, end_expression[, step_expression])

SELECT
  -- 11 - 33のすべての奇数の整数で構成された配列を生成
  GENERATE_ARRAY(11, 33, 2) AS odds,
  -- 負のステップ値を指定して、降順の値で構成された配列を生成
  GENERATE_ARRAY(21, 14, -1) AS countdown;

image-3.png

日付の配列の生成

GENERATE_DATE_ARRAYを利用する。

GENERATE_DATE_ARRAY(start_date, end_date[, INTERVAL INT64_expr date_part])

  • INT64_expr には INT64 にする必要があります。
  • date_part は、DAY、WEEK、MONTH、QUARTER または YEAR にする必要があります。
SELECT
  GENERATE_DATE_ARRAY('2017-11-21', '2017-12-31', INTERVAL 1 WEEK) AS date_array;

image-4.png

次にアクセスです!!(作ったらアクセスしないと)

こちらも公式を参考にしています。

こちらがアクセスするためのベースとなる一時テーブルとなります。

WITH sequences AS
  (SELECT [0, 1, 1, 2, 3, 5] AS some_numbers
   UNION ALL SELECT [2, 4, 8, 16, 32] AS some_numbers
   UNION ALL SELECT [5, 10] AS some_numbers)

こんな感じでデータがはいっています。
+---------------------+
| some_numbers |
+---------------------+
| [0, 1, 1, 2, 3, 5] |
| [2, 4, 8, 16, 32] |
| [5, 10] |
+---------------------+

OFFSET と ORDINA

SELECT 
  some_numbers,
  -- OFFSETは、0から初めて2番目の値を返します。
  some_numbers[OFFSET(1)] AS offset_1,
  -- ORDINAL、1から初めて1番目の値を返します。
  some_numbers[ORDINAL(1)] AS ordinal_1
FROM 
  sequences;

image-5.png

■説明
OFFSET と ORDINALの詳細説明は公式で

下記を理解すれば問題なさそうです。

位置によって ARRAY 要素にアクセスし、その要素を返します。
OFFSETは番号付けがゼロから始まることを意味し、
ORDINALは番号付けが 1 から始まることを意味します。

ARRAY_LENGTH

SELECT
  some_numbers,
  ARRAY_LENGTH(some_numbers) AS len
FROM
  sequences;

image-6.png

■説明
ARRAY_LENGTH)の詳細説明は公式で

下記を理解すれば問題なさそうです。

配列のサイズを返します。
空の配列の場合は 0 を返します。array_expression が NULL である場合、NULL を返します。

配列のフラット化

ここを使いこなせれば。。。といつも思っています。
なので、今回はしっかり理解したいと思っています。

SELECT
  *
FROM
  UNNEST(['foo', 'bar', 'baz', 'qux', 'corge', 'garply', 'waldo', 'fred']) AS element
WITH OFFSET AS OFFSET
ORDER BY OFFSET;

image-7.png

■説明

  • UNNESTARRAYを取得し、ARRAYに含まれる各要素を1行にしたテーブルを返します。
  • WITH OFFSET句を使用して各要素のオフセットを含む追加の列を取得し、
    ORDER BY 句でそれぞれのオフセットを使用して行を並べ替えることができます。
    *デフォルトではUNNESTは順序を無視するため、順序を保証する場合は利用しましょう。

CROSS JOIN

WITH sequences AS
  (SELECT 1 AS id, [0, 1, 1, 2, 3, 5] AS some_numbers
   UNION ALL SELECT 2 AS id, [2, 4, 8, 16, 32] AS some_numbers
   UNION ALL SELECT 3 AS id, [5, 10] AS some_numbers)

SELECT
  id,
  flattened_numbers
FROM
  sequences
CROSS JOIN UNNEST(sequences.some_numbers) AS flattened_numbers;

-- UNNESTを省略して、CROSS JOINを,で表しています。
-- 個人的には上記の方が、可読性的にはいいかなと思っています。
SELECT
  id, f
  lattened_numbers
FROM
  sequences,
  sequences.some_numbers AS flattened_numbers;

image-8.png

■説明
まずは公式の記載を引用します。

各行の他の列の値を維持したまま ARRAYの列全体をフラット化するには、
CROSS JOINを使用して、ARRAY列を含むテーブルとそのARRAY列のUNNEST出力を結合します。

上記クエリでは下記のようなことが起きています。

  1. UNNESTで指定しているARRAYの列から各要素を1行ごとに分けて返します。
  2. ①で返した値とsequencesCROSS JOINを行います。

上記のクエリにsome_numbersSELECTに追加してみました。
これをすることで、個人的にはCROSS JOINをしているイメージがつきます。
sequencesのテーブルは全ての値を返していることがわかるため。

image-9.png

ネストされた配列のクエリ

WITH races AS (
  SELECT "800M" AS race,
    [STRUCT("Rudisha" as name, [23.4, 26.3, 26.4, 26.1] as splits),
     STRUCT("Makhloufi" as name, [24.5, 25.4, 26.6, 26.1] as splits),
     STRUCT("Murphy" as name, [23.9, 26.0, 27.0, 26.0] as splits),
     STRUCT("Bosse" as name, [23.6, 26.2, 26.5, 27.1] as splits),
     STRUCT("Rotich" as name, [24.7, 25.6, 26.9, 26.4] as splits),
     STRUCT("Lewandowski" as name, [25.0, 25.7, 26.3, 27.2] as splits),
     STRUCT("Kipketer" as name, [23.2, 26.1, 27.3, 29.4] as splits),
     STRUCT("Berian" as name, [23.7, 26.1, 27.0, 29.3] as splits)]
       AS participants)

SELECT
  race,
  participant
FROM
  races r
CROSS JOIN UNNEST(r.participants) as participant;

出力例です。
公式ドキュメントから引用しています。

race participant
800M {Rudisha, [23.4, 26.3, 26.4, 26.1]}
800M {Makhloufi, [24.5, 25.4, 26.6, 26.1]}
800M {Murphy, [23.9, 26, 27, 26]}
800M {Bosse, [23.6, 26.2, 26.5, 27.1]}
800M {Rotich, [24.7, 25.6, 26.9, 26.4]}
800M {Lewandowski, [25, 25.7, 26.3, 27.2]}
800M {Kipketer, [23.2, 26.1, 27.3, 29.4]}
800M {Berian, [23.7, 26.1, 27, 29.3]}
お題
800m競走で最速の参加者を返してください。

考え方

  1. まずはARRAYUNNSETして、利用できるようにする。
  2. 次に、1のカラムにもARRAYがあるのでUNNSETして値を利用できるようにする。
  3. このタイミングで、全ての値を操作できるので、コメントアウトしているロジックを記載する。
WITH races AS (
  SELECT "800M" AS race,
  [
    STRUCT("Rudisha" as name, [23.4, 26.3, 26.4, 26.1] as splits),
    STRUCT("Makhloufi" as name, [24.5, 25.4, 26.6, 26.1] as splits),
    STRUCT("Murphy" as name, [23.9, 26.0, 27.0, 26.0] as splits),
    STRUCT("Bosse" as name, [23.6, 26.2, 26.5, 27.1] as splits),
    STRUCT("Rotich" as name, [24.7, 25.6, 26.9, 26.4] as splits),
    STRUCT("Lewandowski" as name, [25.0, 25.7, 26.3, 27.2] as splits),
    STRUCT("Kipketer" as name, [23.2, 26.1, 27.3, 29.4] as splits),
    STRUCT("Berian" as name, [23.7, 26.1, 27.0, 29.3] as splits)
  ]
  AS participants
)

SELECT
  race,
  (
    -- 各参加者ごとに分けた上で、
    -- レース記録の合計値を昇順にした上で、
    -- 一番最初の名前を取得する。
    SELECT
      name
    FROM
      UNNEST(participants)
    ORDER BY (
      -- 書く参加者ごとの記録を合計する。
      SELECT
        SUM(duration)
      FROM
        UNNEST(splits) AS duration
    ) ASC
    LIMIT 1
  ) AS fastest_racer
FROM races;

STRUCT内のARRAY型フィールドのクエリ

ネストされた繰り返しフィールドの情報も取得できます。
たとえば、次のステートメントでは800m競走で最速ラップの参加者が返されます。
公式ドキュメント

お題
800m競走で最速ラップの参加者を返してください。
WITH races AS (
 SELECT "800M" AS race,
  [
    STRUCT("Rudisha" as name, [23.4, 26.3, 26.4, 26.1] as splits),
    STRUCT("Makhloufi" as name, [24.5, 25.4, 26.6, 26.1] as splits),
    STRUCT("Murphy" as name, [23.9, 26.0, 27.0, 26.0] as splits),
    STRUCT("Bosse" as name, [23.6, 26.2, 26.5, 27.1] as splits),
    STRUCT("Rotich" as name, [24.7, 25.6, 26.9, 26.4] as splits),
    STRUCT("Lewandowski" as name, [25.0, 25.7, 26.3, 27.2] as splits),
    STRUCT("Kipketer" as name, [23.2, 26.1, 27.3, 29.4] as splits),
    STRUCT("Berian" as name, [23.7, 26.1, 27.0, 29.3] as splits)
  ] AS participants)

-- CROSS JOINをこちらは省略しています。
SELECT
  race,
  (
    SELECT
      name
    FROM
      UNNEST(participants),
      UNNEST(splits) AS duration
    ORDER BY duration ASC
    LIMIT 1
  ) AS runner_with_fastest_lap
FROM
  races;

-- CROSS JOINを明記しています。(上記と結果は変わりません。)
SELECT
  race,
  (
    SELECT
      name
    FROM
      UNNEST(participants)
      CROSS JOIN UNNEST(splits) AS duration
    ORDER BY duration ASC
    LIMIT 1
  ) AS runner_with_fastest_lap
FROM races;
race runner_with_fastest_lap
800M Kipketer

ちょっと次に行く前に練習

WITH races AS (
 SELECT "800M" AS race,
  [
    STRUCT("Rudisha" as name, [23.4, 26.3, 26.4, 26.1] as splits),
    STRUCT("Makhloufi" as name, [24.5, 25.4, 26.6, 26.1] as splits),
    STRUCT("Murphy" as name, [23.9, 26.0, 27.0, 26.0] as splits),
    STRUCT("Bosse" as name, [23.6, 26.2, 26.5, 27.1] as splits),
    STRUCT("Rotich" as name, [24.7, 25.6, 26.9, 26.4] as splits),
    STRUCT("Lewandowski" as name, [25.0, 25.7, 26.3, 27.2] as splits),
    STRUCT("Kipketer" as name, [23.2, 26.1, 27.3, 29.4] as splits),
    STRUCT("Berian" as name, [23.7, 26.1, 27.0, 29.3] as splits),
    STRUCT("Nathan" as name, ARRAY<FLOAT64>[] as splits),
    STRUCT("David" as name, NULL as splits)
  ] AS participants
)

-- まずは全てのレコードが1行で配列を利用していることを確認しましょう。
SELECT
  *
FROM
  races

-- 次にCROSS JOINを利用することで、ARRAYのカラムに対してクエリができることを確認します。
-- ただ、splitsはまだ配列のままです。
SELECT
  name,
  splits
FROM
  races,
  races.participants ;

-- 下記2つの動きは少し違いがあります。
-- LEFT JOINを利用することでNULLや空を残すことができます。
-- CROSS JOINをい利用するとNULLや空を残しません。
SELECT
  duration AS finish_time
FROM
  races,
  races.participants
  LEFT JOIN participants.splits duration

SELECT
  duration AS finish_time
FROM
  races,
  races.participants,
  participants.splits duration

CROSS JOIN を使用して配列をフラット化すると、
空の配列または NULL 配列を含む行は除外されます。

WITH races AS (
 SELECT "800M" AS race,
  [
    STRUCT("Rudisha" as name, [23.4, 26.3, 26.4, 26.1] as splits),
    STRUCT("Makhloufi" as name, [24.5, 25.4, 26.6, 26.1] as splits),
    STRUCT("Murphy" as name, [23.9, 26.0, 27.0, 26.0] as splits),
    STRUCT("Bosse" as name, [23.6, 26.2, 26.5, 27.1] as splits),
    STRUCT("Rotich" as name, [24.7, 25.6, 26.9, 26.4] as splits),
    STRUCT("Lewandowski" as name, [25.0, 25.7, 26.3, 27.2] as splits),
    STRUCT("Kipketer" as name, [23.2, 26.1, 27.3, 29.4] as splits),
    STRUCT("Berian" as name, [23.7, 26.1, 27.0, 29.3] as splits),
    STRUCT("Nathan" as name, ARRAY<FLOAT64>[] as splits),
    STRUCT("David" as name, NULL as splits)
  ] AS participants
)

SELECT
  name,
  sum(duration) AS finish_time
FROM
  races,
  races.participants
  LEFT JOIN participants.splits duration
GROUP BY
  name;
name finish_time
Murphy 102.9
Rudisha 102.19999999999999
David NULL
Rotich 103.6
Makhloufi 102.6
Berian 106.1
Bosse 103.4
Kipketer 106
Nathan NULL
Lewandowski 104.2

サブクエリからの配列の作成

ARRAYの中身だけを変更して、
ARRAYで最終的に返す場合の対応方法です。

WITH sequences AS (
  SELECT [0, 1, 1, 2, 3, 5] AS some_numbers
  UNION ALL SELECT [2, 4, 8, 16, 32] AS some_numbers
  UNION ALL SELECT [5, 10] AS some_numbers
)

-- UNNESTをして配列の値を処理後に再度ARRAYに変換
SELECT
  some_numbers,
  ARRAY(
    SELECT
      x * 2
    FROM
      UNNEST(some_numbers) AS x
  ) AS doubled
FROM
  sequences;
some_numbers doubled
[0, 1, 1, 2, 3, 5] [0, 2, 2, 4, 6, 10]
[2, 4, 8, 16, 32] [4, 8, 16, 32, 64]
[5, 10] [10, 20]

配列のフィルタ処理

考え方としては、配列から値を取得と処理をSELECT句でやる感じですね。

WITH sequences AS
  (SELECT [0, 1, 1, 2, 3, 5] AS some_numbers
   UNION ALL SELECT [2, 4, 8, 16, 32] AS some_numbers
   UNION ALL SELECT [5, 10] AS some_numbers)

-- 基準値(5)未満の値だけを2倍する
SELECT
  ARRAY(
    SELECT
      x * 2
    FROM
      UNNEST(some_numbers) AS x
    WHERE
      x < 5
  ) AS doubled_less_than_five
FROM
  sequences;
doubled_less_than_five
[0, 2, 2, 4, 6]
[4, 8]
[]

ARRAY内の重複も簡単に対応できますね。
ちょっとネストが深くなっていやですがw

WITH sequences AS (SELECT [0, 1, 1, 2, 3, 5] AS some_numbers)
SELECT
  ARRAY(
    SELECT
      DISTINCT x
    FROM
      UNNEST(some_numbers) AS x
  ) AS unique_numbers
FROM
  sequences;
unique_numbers
[0, 1, 2, 3, 5]

例えばINを利用することで、
対象の値が含まれているARRAYのカラムを取得することもできます。
*正直コードでもよく書きます。

WITH sequences AS
  (SELECT [0, 1, 1, 2, 3, 5] AS some_numbers
   UNION ALL SELECT [2, 4, 8, 16, 32] AS some_numbers
   UNION ALL SELECT [5, 10] AS some_numbers)

SELECT
  ARRAY(
    SELECT
      x
    FROM
      UNNEST(some_numbers) AS x
    WHERE
      2 IN UNNEST(some_numbers)
  ) AS contains_two
FROM
  sequences;
contains_two
[0, 1, 1, 2, 3, 5]
[2, 4, 8, 16, 32]
[]

配列のスキャン

ARRAYの中に対象の値が含まれているかの確認として使います。

SELECT 2 IN UNNEST([0, 1, 1, 2, 3, 5]) AS contains_value;
contains_value
true

INを使えばARRAYに含まれている値があるかどうか一発で確認できますね。
これってよくコードでも書きますよね。。。
BQ側で対応ができるならコードにするよりも意外に楽なのではないかと思いました。

WITH sequences AS
(
  SELECT 1 AS id, [0, 1, 1, 2, 3, 5] AS some_numbers
  UNION ALL SELECT 2 AS id, [2, 4, 8, 16, 32] AS some_numbers
  UNION ALL SELECT 3 AS id, [5, 10] AS some_numbers
)

SELECT
  id AS matching_rows
FROM
  sequences
WHERE
  2 IN UNNEST(sequences.some_numbers)
ORDER BY matching_rows;
matching_rows
1
2

条件を満たす値のスキャン

こちらも先ほどと同様に、値を探し当てます。

WITH sequences AS
(
  SELECT 1 AS id, [0, 1, 1, 2, 3, 5] AS some_numbers
  UNION ALL SELECT 2 AS id, [2, 4, 8, 16, 32] AS some_numbers
  UNION ALL SELECT 3 AS id, [5, 10] AS some_numbers
)

-- ARRAYの値に5を超える値がある場合のみidを返す。
SELECT
  id AS matching_rows
FROM
  sequences
WHERE EXISTS (
  SELECT
    *
  FROM
    UNNEST(some_numbers) AS x
  WHERE
    x > 5
);
matching_rows
2
3

条件を満たすSTRUCTフィールド値のスキャン

もはやSQLを書いているというよりは。。。コードを書いている感じですね。。。
*SQLもコードですがw

WITH sequences AS
(
  SELECT1 AS id, [STRUCT(0 AS a, 1 AS b)] AS some_numbers
  UNION ALL SELECT 2 AS id, [STRUCT(2 AS a, 4 AS b)] AS some_numbers
  UNION ALL SELECT 3 AS id, [STRUCT(5 AS a, 3 AS b), STRUCT (7 AS a, 4 AS b)] AS some_numbers
)

SELECT
  id AS matching_rows
FROM
  sequences
WHERE EXISTS (
  SELECT
    1
  FROM
    UNNEST(some_numbers)
  WHERE
    b > 3
);
matching_rows
2
3

配列と集約

ARRAY_AGG()

順序を保証することなくARRAYに値を集約させる。

WITH fruits AS
(
  SELECT "apple" AS fruit
  UNION ALL SELECT "pear" AS fruit
  UNION ALL SELECT "banana" AS fruit
)

-- 出力カラムを一つのARRAYに集約させます。
SELECT
  ARRAY_AGG(fruit) AS fruit_basket
FROM
  fruits;
fruit_basket
[apple, pear, banana]

ARRAY_AGG自体は順序を保証するわけではないので、
もしなにかしらの順序を設けたい場合はORDER BYを利用するとよい。

WITH fruits AS
(
  SELECT "apple" AS fruit
  UNION ALL SELECT "pear" AS fruit
  UNION ALL SELECT "banana" AS fruit
)

SELECT
  ARRAY_AGG(fruit ORDER BY fruit) AS fruit_basket
FROM
  fruits;
fruit_basket
[apple, banana, pear]

例えばARRAYの合計値とARRAYの値を一緒に表示させたい場合などには、
下記のようにSUMを使うこともできます。

WITH sequences AS
(
  SELECT [0, 1, 1, 2, 3, 5] AS some_numbers
  UNION ALL SELECT [2, 4, 8, 16, 32] AS some_numbers
  UNION ALL SELECT [5, 10] AS some_numbers
)

SELECT
  some_numbers,
  (
    SELECT
      SUM(x)
    FROM
      UNNEST(s.some_numbers) x
  ) AS sums
FROM
  sequences s;
some_numbers sums
[0, 1, 1, 2, 3, 5] 12
[2, 4, 8, 16, 32] 62
[5, 10] 15

ARRAY_CONCAT_AGG()

こちらの関数を利用すると、
ARRAYのカラムがレコードをまたいで値を集約することだできます。

WITH aggregate_example AS
(
  SELECT [1,2] AS numbers
  UNION ALL SELECT [3,4] AS numbers
  UNION ALL SELECT [5, 6] AS numbers
)

SELECT
  ARRAY_CONCAT_AGG(numbers) AS count_to_six_agg
FROM
  aggregate_example;
count_to_six_agg
[1, 2, 3, 4, 5, 6]

配列から文字列への変換

ARRAY_TO_STRING

ARRAYを文字列として連結したい場合にはARRAY_TO_STRINGが便利です。

WITH greetings AS (SELECT ["Hello", "World"] AS greeting)

SELECT
  ARRAY_TO_STRING(greeting, " ") AS greetings
FROM
  greetings;
greetings
Hello World

ARRAY_TO_STRINGには引数が利用できます。

  • 第一引数: 対象のARRAY
  • 第二引数: 関数が出力を生成するために入力項目間に挿入する区切り文字
  • 第三引数: NULLの置き換えを指定(省略すると無視します。)
SELECT
  ARRAY_TO_STRING(arr, ".", "N") AS non_empty_string,
  ARRAY_TO_STRING(arr, ".", "") AS empty_string,
  ARRAY_TO_STRING(arr, ".") AS omitted
FROM
  (SELECT ["a", NULL, "b", NULL, "c", NULL] AS arr);
non_empty_string empty_string omitted
a.N.b.N.c.N a..b..c. a.b.c

配列の結合

SELECT ARRAY_CONCAT([1, 2], [3, 4], [5, 6]) as count_to_six;
count_to_six
[1, 2, 3, 4, 5, 6]

配列の圧縮

UNNESTWITH OFFSETを使用するとARRAYを圧縮することができます。

WITH OFFSETとは
UNNESTARRAYから生成される行ごとに、
「オフセット」値(カウントはゼロから始まります)を含む別の列を返します。
*Javaなどの配列のindexで値を取得ができるようになります。

WITH combinations AS
(
  SELECT
    ['a', 'b'] AS letters,
    [1, 2, 3] AS numbers
)

SELECT
  ARRAY_AGG(STRUCT(letter, numbers[OFFSET(letters_offset)] AS number)) AS pairs
FROM
  combinations,
  UNNEST(letters) AS letter WITH OFFSET AS letters_offset;
pairs
[{ letter: "a", number: 1 },
{ letter: "b", number: 2 }]

配列の配列の作成

ARRAYARRAYは作成することがBigQueryではできない。
*2020/10時点

そのためARRAYARRAYをするにはSTRUCTを利用する必要がある。

WITH points AS
(
  SELECT [1, 5] as point
  UNION ALL SELECT [2, 8] as point
  UNION ALL SELECT [3, 7] as point
  UNION ALL SELECT [4, 1] as point
  UNION ALL SELECT [5, 7] as point
)

SELECT
  ARRAY(
    SELECT
      STRUCT(point)
    FROM
      points
  ) AS coordinates;
coordinates
[{point: [1,5]},
{point: [2,8]},
{point: [5,7]},
{point: [3,7]},
{point: [4,1]}]

ちょっとした遊び

平仮名の組み合わせを簡単に作りたいなーと思い、
下記のような配列を作ってみました。

正直3単語の組み合わせの数が9万を超えるので、
2単語の組み合わせでいいかなとは思いましたが、
コードを書くよりも比較的簡単に作ることができました。

よって、コードでなくSQLで書こうとはならないのですが。。。
ものによっては、GO,Pythonなどを利用するよりも、
SQLが非常に楽で安全な場合もあるのだなと感じました。

ちなみに、もっといい書き方があるとは思います。
*教えて欲しい!!

WITH sequences AS
  (SELECT [
  'あ','い','う','え','お',
  'か','き','く','け','こ',
  'さ','し','す','せ','そ',
  'た','ち','つ','て','と',
  'な','に','ぬ','ね','の',
  'は','ひ','ふ','へ','ほ',
  'ま','み','む','め','も',
  'や','ゆ','よ',
  'ら','り','る','れ','ろ',
  'わ','を','ん'] AS n)

-- 2単語の組み合わせ
SELECT
 na1 as firstL,
 na2 as lastL,
 concat(na1, na2) as name
FROM
  sequences
CROSS JOIN UNNEST(sequences.n) AS na1
CROSS JOIN UNNEST(sequences.n) AS na2
where
na1 not in ('を','ん')
and na1 != na2

-- 3単語の組み合わせ
SELECT
 na1 as firstL,
 na2 as secondL,
 na3 as lastL,
 concat(na1, na2, na3) as name
FROM
  sequences
CROSS JOIN UNNEST(sequences.n) AS na1
CROSS JOIN UNNEST(sequences.n) AS na2
CROSS JOIN UNNEST(sequences.n) AS na3
where
na1 not in ('を','ん')
and na1 != na2
and na2 != na3