Goのruneの最低限知っておいた方が良いかもしれない知識

自分が現時点で思うGoのruneの最低限の知識です。(なお、業務ではrune使ったことない)

いつか必要になった時にこれだけは知っておいた方が良いかなーと思って自分用にまとめました。

  • Unicodeコードポイントを表す
    • Unicodeは、文字コードの規格
      • 文字コードは、文字、記号に識別番号を与えて区別できるようにした規則
    • コードポイントは文字に割り当てられた整数値
  • 整数値
  • runeリテラル'で括る
  • ゼロ値は0
  • 改行とエスケープされていない'は不可
  • []runestringは相互変換可能
  • stringrangeで回すと2つめの返り値はrune型の値
import "fmt"

func main() {
    var r rune
    fmt.Println(r)
    r = 'A'
    fmt.Println(r)

    for _, v := range "ABC" {
        fmt.Println(v)
    }

    fmt.Println(string([]rune{65, 66, 67}))
}
0
65
65
66
67
ABC

参考

www.spinute.org

ja.wikipedia.org

gigazine.net

blog.knightso.co.jp

text.baldanders.info

go.dev

データレイク、データウェアハウス、データマート

数年前に一度調べたが、完全に頭から消えているので再度調べてみた。

  • データマート
    • 小規模
    • あるスコープに限定された情報
      • 部門とか
  • データマートの種類
    • 従属型
      • データウェアハウスのサブセットを保存
      • データウェアハウスからクエリしたデータを保存するので、データウェアハウスへの依存が強い
    • 独立型
      • データソースから抽出、処理したデータを保存
    • ハイブリッド型
      • データウェアハウスおよびデータソースから収集したデータを保存
  • データウェアハウス
    • ビジネス全体の情報を格納
    • 広範囲
    • 構造化されている
    • 生データを収集してデータが作られる
  • データレイク
    • 生データ
    • 非構造化
    • 重複データや意味のないデータが含まれる可能性あり

参考

aws.amazon.com

www.ibm.com

speakerdeck.com

go-sql-driver/mysql の Scan の挙動にハマる

Go の MySQL ドライバーとして go-sql-driver/mysql を使用した際の挙動にハマったので、解決策を記しておきます。

クエリを発行し、取得した整数型の値をany型の値で受け取る際に予期しない結果となることがあります。

package main

import (
    "database/sql"
    "fmt"
    "os"

    "github.com/go-sql-driver/mysql"
)

func main() {
    db, err := sql.Open("mysql", (&mysql.Config{
        User:                 "root",
        Passwd:               "password",
        DBName:               "rdb",
        Addr:                 "localhost:3000",
        AllowNativePasswords: true,
    }).FormatDSN())
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }

    var val any
    if err := db.QueryRow("SELECT 1").Scan(&val); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    fmt.Printf("type is %T\n", val)

    var val2 any
    if err := db.QueryRow("SELECT ?", 1).Scan(&val2); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    fmt.Printf("type is %T\n", val2)
}
go run main.go
type is []uint8
type is int64

2つのSELECT文は同じ値を返しているはずですが、プレースホルダを用いているか否かで、値の型が異なってしまいます。

期待するのは、どちらもint64になることです。

解決策

プレースホルダへのバインドが不要であってもプリペアドステートメントを使うよう実装することができます。

https://github.com/go-sql-driver/mysql/issues/407#issuecomment-172583652

下記が実装例です。

package main

import (
    "database/sql"
    "fmt"
    "os"

    "github.com/go-sql-driver/mysql"
)

func main() {
    db, err := sql.Open("mysql", (&mysql.Config{
        User:                 "root",
        Passwd:               "password",
        DBName:               "rdb",
        Addr:                 "localhost:3000",
        AllowNativePasswords: true,
    }).FormatDSN())
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }

    var val any
    stmt, err := db.Prepare("SELECT 1")
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    defer stmt.Close()
    if err := stmt.QueryRow().Scan(&val); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    fmt.Printf("type is %T\n", val)

    var val2 any
    if err := db.QueryRow("SELECT ?", 1).Scan(&val2); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    fmt.Printf("type is %T\n", val2)
}
go run main.go
type is int64
type is int64

解説

MySQL にはバイナリプロトコル、テキストプロトコルという2つのプロトコルがあります。

go-sql-driver/mysql は、プリペアドステートメントを使用する際にはバイナリプロトコルを使用し、そうでない場合はテキストプロトコルを使用します。

テキストプロトコルでデータを受信した場合には[]uint8で結果を取得し、バイナリプロトコルの場合にはカラムの型に応じた Go の型で結果を取得します。

上記を踏まえると、テキストプロトコルが使用されるケースでScan()any型の値を渡した際に[]uint8型の値がアサインされるのは自然です。

また、テキストプロトコルが使用されるケースであってもint64型の値を渡せばint64型の値がアサインされるので、型を強制したいのであればany型を渡すなよということです。

こちらの記事が詳しいです。

go-sql-driver/mysql の次期バージョンを待ちましょう

今回、解決策を示していますが、実は go-sql-driver/mysql 次期バージョンにおいて上記の問題は解決されます。

テキストプロトコルが使用されるケースであっても、整数、浮動小数点をint64float64にパースするPRがマージされています。

Parse numbers on text protocol too by methane · Pull Request #1452 · go-sql-driver/mysql · GitHub

v1.8 のリリースを待ちましょう。

参考

MySQL5.7でJSONを行に変換する

val
{"Bob": {"A": 10, "B": 5}, "Alice": {"A": 10, "C": 5}}
{"Bob": {"D": 10, "E": 5}, "Mike": {"A": 5, "F": 5}}

上記のようなJSON型の値を持った行から下記のような結果を得たいが、MySQL 5.7には JSON_TABLE が実装されていない。

name type val
Bob A 10
Bob B 5
Bob D 10
Bob E 5
Alice A 10
Alice C 5
Mike A 5
Mike F 5

JSON系の関数を使って無理やりSQLを書いてみた。

SELECT
    t.k AS name,
    JSON_UNQUOTE(JSON_EXTRACT(JSON_KEYS(t.v), CONCAT('$[', idx_list_2.idx, ']'))) AS type,
    JSON_UNQUOTE(JSON_EXTRACT(t.v, CONCAT('$.', JSON_EXTRACT(JSON_KEYS(t.v), CONCAT('$[', idx_list_2.idx, ']'))))) AS val
FROM (
SELECT
    JSON_UNQUOTE(JSON_EXTRACT(JSON_KEYS(val), CONCAT('$[', idx_list.idx, ']'))) AS k,
    JSON_EXTRACT(val, CONCAT('$.', JSON_EXTRACT(JSON_KEYS(val), CONCAT('$[', idx_list.idx, ']')))) AS v
FROM json
CROSS JOIN (
    SELECT @idx := @idx + 1 AS idx
    FROM (SELECT @idx := -1) AS init_idx
    CROSS JOIN information_schema.`COLUMNS`
) AS idx_list
WHERE JSON_EXTRACT(JSON_KEYS(val), CONCAT('$[', idx_list.idx, ']')) IS NOT NULL
) AS t
CROSS JOIN (
    SELECT @idx2 := @idx2 + 1 AS idx
    FROM (SELECT @idx2 := -1) AS init_idx
    CROSS JOIN information_schema.`COLUMNS`
) AS idx_list_2
WHERE JSON_EXTRACT(JSON_KEYS(t.v), CONCAT('$[', idx_list_2.idx, ']')) IS NOT NULL;

メンテナンス性が終わっているなあ...。

MySQL5.7 でも順位を付けたい

MySQLDBMSとして使用している環境において、取得結果をカラムの値で順位付けしたいことがあると思います。

バージョンが 8.0 であればウインドウ関数 RANK() を使用すれば実現できますが、5.7 を使用している場合は別の方法を取る必要があります。

動作確認用のテーブル準備

CREATE TABLE
    IF NOT EXISTS tbl (
        `id` INT AUTO_INCREMENT NOT NULL,
        `name` VARCHAR(20) NOT NULL,
        `count` INT NOT NULL,
        PRIMARY KEY (`id`)
    );
INSERT INTO tbl(name, count)
        VALUES('ユーザA', 1), ('ユーザB', 2), ('ユーザC', 2), ('ユーザD', 0), ('ユーザE', 10);

順位を付けて取得

自己非等値結合を使用し、自分よりcountの値が大きいレコードの数を数えて順位を導き出しています。

SELECT
    tbl.*,
    (SELECT COUNT(*)+1 FROM tbl AS tmp WHERE tmp.count > tbl.count) AS rank
FROM tbl
ORDER BY rank;
id name count rank
5 ユーザE 10 1
2 ユーザB 2 2
3 ユーザC 2 2
1 ユーザA 1 4
4 ユーザD 0 5

同じ順位の値が続いた際に、次の順位を前の順位にレコード数を加算した値にするのではなく、続きの値にする場合は DISTINCT を使用して count の値が同じレコードを1件と数えます。

SELECT
    tbl.*,
    (SELECT COUNT(DISTINCT tmp.count)+1 FROM tbl AS tmp WHERE tmp.count > tbl.count) AS rank
FROM tbl
ORDER BY rank;
id name count rank
5 ユーザE 10 1
3 ユーザC 2 2
2 ユーザB 2 2
1 ユーザA 1 3
4 ユーザD 0 4

参考

www.shoeisha.co.jp

週報(08/28~09/03)

QuickSight

  • Analysis から Template を作って Template から Dashboard を作っている場合、Analysis を更新しても Template は自動更新されないし、Template を更新しても Dashboard は自動更新されない。
    • Template、Dashboard はバージョンを保持しているため、それぞれを更新してバージョンを更新しないといけない。
    • IaC 化している場合は、VersionDescription をデプロイ時の Unix タイムスタンプとかにして無理やり毎回更新するとうまくいく。
    • Analysis と同じ Definition を使いたいだけなら、単純に Definition をコピーしたら良い。
  • データセットをコントロールの検索項目設定に使える。カスタムSQLの中でパラメータを使っている場合、パラメータのデフォルト値が埋められた状態で検索項目が抽出されるのに注意。

Serverless Framework

  • file プロパティで別ファイルを参照できる。
  • param プロパティで stage ごとのパラメータを指定できる。
    • ssm プロパティで値参照したい場合は、当該ステージで参照しないパラメータも登録済みにしておかないといけない。(resolve できるようにしていないといけない)

Unix コマンド

  • grep-q オプションで対象が見つかった時点で探索を終了できる。

週報(08/21~08/27)

QuickSight

フィルター、コントロールの選択値の挙動を確認した。カスタムSQLを使っている場合、データセットパラメータがデフォルト値の状態で選択値の検索をするためのクエリが走るようだ。カスケードフィルターを使っていても、デフォルト値の埋まった SQL がサブクエリで包まれて、そのサブクエリの結果に対して上流のフィルターの値で検索するので、デフォルト値次第で結果が変わってくる。実際に SQL のログ見ないとわからない仕様。

Terraform

先週、aws_quicksight_data_source の更新時の挙動がおかしい気がしてissueを立てたのだが、それに対応するPRを作った。既存の実装の挙動を見ながらそれらしくコーディングしつつ、貢献ガイドを参照するという進め方で行った。ACCテストも書いたが、QuickSight のサブスクリプションそろそろやめたいので今後、QuickSight のACCテスト書く時どうしよう。

github.com

Serverless Framework

resources ブロックに CFn をそのまま書ける事を知った。TS で書くようにすれば、Yaml よりも構造的に書けるのでは。

CloudFormation

インポート機能を知った。先に対象のリソースと同じ記述をする必要があるのは大変だなあ。