Log

いろいろ

UNION ALLとソート

ORDER BY句の無いクエリは結果セットの順序を保証しないと100万回学んだのに、UNION ALLの結果がソートされないことを期待してしまいました。

※PostgreSQLです。また、「期待していたこと」と「旅は続く」のクエリおよび結果は、実際に実行したものを改変したイメージです。

期待していたこと

CREATE TABLE table1 AS (SELECT 1 AS col1);
CREATE TABLE table2 AS (SELECT 2 AS col2);

SELECT col1 FROM table1
UNION ALL
SELECT col2 FROM table2;

期待した結果。

col1
1
2

実際に得られた結果。

col1
2
1

とても悲しい。以下、このような結果を期待した経緯が続きます。

UNIONはソートする(まちがっている)

まずはUNIONとUNION ALLのおさらい。

クエリ1 UNION [ALL] クエリ2

UNIONはクエリ1の結果にクエリ2の結果を付加します。ALLが指定されていない場合は、その結果から重複行を削除します。

クエリ1の結果とクエリ2の結果に重複行が存在しないのであればUNION ALLを使うべきです。UNIONは重複行を排除するためにコストの高いソートを実行します。その副作用として結果セットはソートされた状態で返却されます。

CREATE TABLE table1 AS (SELECT generate_series(2, 1, -1) AS col1);
CREATE TABLE table2 AS (SELECT generate_series(3, 2, -1) AS col2);

SELECT col1 FROM table1
UNION
SELECT col2 FROM table2;

テーブル1の内容。

col1
3
2
1

テーブル2の内容。

col2
4
3
2

結果。

col1
2
4
1
3

ソートされてないじゃん!!

、、、落ち着いて実行計画を見ましょう。

-- UNIONの実行計画
EXPLAIN
SELECT col1 FROM table1
UNION
SELECT col2 FROM table2;
HashAggregate  (cost=160.25..211.25 rows=5100 width=4)
  Group Key: table1.col1
  ->  Append  (cost=0.00..147.50 rows=5100 width=4)
        ->  Seq Scan on table1  (cost=0.00..35.50 rows=2550 width=4)
        ->  Seq Scan on table2  (cost=0.00..35.50 rows=2550 width=4)

HashAggregateが選択されていました。都合が悪いのでoffにしておきましょう。

-- オプティマイザがHash Aggregateを選びにくくする
set enable_hashagg = 'off';

-- UNIONの実行計画(リベンジ)
EXPLAIN
SELECT col1 FROM table1
UNION
SELECT col2 FROM table2;
Unique  (cost=461.57..487.07 rows=5100 width=4)
  ->  Sort  (cost=461.57..474.32 rows=5100 width=4)
        Sort Key: table1.col1
        ->  Append  (cost=0.00..147.50 rows=5100 width=4)
              ->  Seq Scan on table1  (cost=0.00..35.50 rows=2550 width=4)
              ->  Seq Scan on table2  (cost=0.00..35.50 rows=2550 width=4)

ソートしてますね。結果も以下のとおり。

col1
1
2
3
4

今まさに直面したように、明示的にORDER BYを指定しない限り、順序は保証されません。PostgreSQLのキモチ次第です。UNIONの結果がソートされるなんて考えは浅はかなのです。

UNION ALLはソートしない(まちがっている)

「UNIONは重複行を排除するためにソートする」
→「UNION ALLは重複行を排除しない」
→「UNION ALLはソートしない」

浅はかな考えでしょう?

でも、私がソートしないと考えたのは他に理由がありそうです。UNION ALLは2つのクエリをガッチャンコするというイメージに支配されていたんですよ。↓が私の脳内です。

-- クエリ1
SELECT col1 FROM table1
UNION ALL
-- クエリ2
SELECT col2 FROM table2;

それぞれ独立のクエリと考えていたようで、クエリ1の結果とクエリ2の結果が混ざることにモヤッとしたのかもしれません。SELECTの構文が答えを与えてくれます。

[ WITH [ RECURSIVE ] with_query [, ...] ]
SELECT [ ALL | DISTINCT [ ON ( expression [, ...] ) ] ]
[ * | expression [ [ AS ] output_name ] [, ...] ]
[ FROM from_item [, ...] ]
[ WHERE condition ]
[ GROUP BY grouping_element [, ...] ]
[ HAVING condition ]
[ WINDOW window_name AS ( window_definition ) [, ...] ]
[ { UNION | INTERSECT | EXCEPT } [ ALL | DISTINCT ] select ]
[ ORDER BY expression [ ASC | DESC | USING operator ] [ NULLS { FIRST | LAST } ] [, ...] ]
[ LIMIT { count | ALL } ]
[ OFFSET start [ ROW | ROWS ] ]
[ FETCH { FIRST | NEXT } [ count ] { ROW | ROWS } ONLY ]
[ FOR { UPDATE | NO KEY UPDATE | SHARE | KEY SHARE } [ OF table_name [, ...] ] [ NOWAIT | SKIP LOCKED ] [...] ]

UNIONいた。ORDER BYの手前にいた。脳内イメージを書き換えます。

-- クエリ1
SELECT col1 FROM table1
-- クエリ1のUNION句
UNION ALL SELECT col2 FROM table2;

完全に理解した。

つまり↓は構文エラーになる。

-- クエリ1
SELECT col1 FROM table1 ORDER BY col1
-- クエリ1のUNION句
UNION ALL SELECT col2 FROM table2;

過去の私ならやっていそうですね。言ってやりましょう。

「「「目を醒ませ 僕らの世界が何者かに侵略されてるぞ」」」

完全に理解した現在の私ならば以下のように書きます。

-- クエリ1
SELECT col1 FROM table1
-- クエリ1のUNION句
UNION ALL SELECT col2 FROM table2
-- クエリ1のORDER BY句
ORDER BY col1;

スッキリしました。普通のSELECT文と認識できれば、UNION ALLが含まれていようと順序が保証されないことに納得です。とても当たり前のことを言っています。

何度でも自分に言い聞かせますが、ORDER BY句の無いクエリは結果セットの順序を保証しないし、UNIONは聴いたほうがいい。

UNION

UNION

  • OxT
  • アニメ
  • ¥255
  • provided courtesy of iTunes

旅は続く

冒頭に戻ります。これでいいんです。

CREATE TABLE table1 AS (SELECT 1 AS col1);
CREATE TABLE table2 AS (SELECT 2 AS col2);

SELECT col1 FROM table1
UNION ALL
SELECT col2 FROM table2
-- 明示的にソートする
ORDER BY col1;

期待した結果。

col1
1
2

実際に得られた結果。

col1
2
1

は?


追記。↑超嘘つきました。

冒頭で本章のクエリと結果はイメージと書きましたが、ソートで指定していたカラムが実際には文字列でした。サンプルのように数値であれば期待したとおりに並びます。当たり前だ。文字列が思ってたとおりにソートされなかった原因はロケール関連。メモ程度ですが内容はこちら。

文字列ソート - Log

psycopg2のLambdaレイヤーを作成する

タイトル通りです。Lambdaで外部ライブラリを使うためにレイヤーをつくります。レイヤーとはLambda関数で利用できる共通ライブラリのようなものです。多分。

Lambda レイヤーでの作業 - AWS Lambda

レイヤーは他人へ共有可能です。psycopg2を公開している方もいました。お好きなARNを指定してレイヤーを追加すれば、Lambda関数内でpsycopg2をimportできるようになります。

GitHub - jetbridge/psycopg2-lambda-layer: psycopg2 Lambda Layer

自分でつくる場合も簡単です。公式にバッチリと手順が載っています。

Docker でシミュレートされた環境を使用して Lambda レイヤーを作成する | AWS re:Post

実際にzipファイルの作成まで試してみたコード。

$ mkdir psycopg2-3.9
$ cd psycopg2-3.9
$ echo 'psycopg2-binary' > requirements.txt
$ docker run -v "$PWD":/var/task "public.ecr.aws/sam/build-python3.9" /bin/sh -c "pip install -r requirements.txt -t python/lib/python3.9/site-packages/; exit"
$ zip -r psycopg2-3.9.zip python > /dev/null

なお、インストールしているパッケージはpsycopg2ではなくスタンドアロン版のpsycopg2-binaryです。

GitHub - psycopg/psycopg2: PostgreSQL database adapter for the Python programming language

You can also obtain a stand-alone package, not requiring a compiler or external libraries, by installing the psycopg2-binary package from PyPI:

通常のpsycopg2では静的リンクに設定を変更してビルドする必要があるため、そうした手間の発生しないpsycopg2-binaryが便利です。psycopg2の静的リンクを試みる場合は下記を参考に。

AWS Lambda Pythonからpsycopg2でRDS PostgreSQLに接続する | DevelopersIO

こうして作成したzipファイルをレイヤーに登録して完了です。

マネジメントコンソールを触っていて気づいたのですが、コンテナイメージを元にLambda関数を作成できるようになっていました。単に外部ライブラリを使いたいだけであればこちらが便利そうです。おわり。

とらドラ!の内容を覚えてないYO

あけましておめでとうございました。

帰省した

先日、2年ぶりに帰省しました。

久しぶりでも実家は暇すぎますね。いにしえのマンガを読むくらいしかやることがないです。そうして本棚で目についた『おまえをオタクにしてやるから、俺をリア充にしてくれ!』というマンガを読みました。お互いの恋を成就させるために、タイトル通りの協定を結ぶという内容です。

おまえをオタクにしてやるから、俺をリア充にしてくれ! | 書籍情報 | ファンタジア文庫

恋の共同戦線というと他にもあったな〜と考えていたら、ご丁寧にその隣にとらドラ!が並べられていました。懐かしい。久しぶりにくぎゅの声が聴きたいし、アニメを見返して寅年の幕開けです🎍🐯

アニメ見た

1話は無料で配信されていたニコニコ動画、残りはおなじみdアニメストアです。dアニメストアは毎月納税を続けているだけだったので、今年はちゃんと使っていきたい所存。ニコニコ動画のプレミアム会員はとっくに辞めてしまいました。といっても現実でアニメガタリをする友達はほとんどいないので、今でもニコニコ動画のコメントがソウルメイツです。

25話かけてキャラクターの心情変化を追い、甘酸っぱいお気持ちになりました。最近はヘラヘラと萌え萌えアニメーションばかりを見ていたので、違う意味で顔面(表情)で訴えかけられると辛いです。キャラクターに幼さがある点もよかったですね。なんじゃそりゃという言動もご愛嬌。

印象深い回は19話です。

anime.dmkt-sp.jp

が、記憶になかったので不思議でした。タイガで頭に浮かんだのが誠凛高校一年火神大我ですし、私の脳内ではとらドラ!の影が薄いようです。ミスディレクション

私は2010年頃に『けいおん!』で生まれたオタク団塊世代なので、2008年放送の本作品をリアタイしていません。大学生になってから見たんだっけ。いずれにせよ、幼い私と成年した大人なそれとでは、感じ方が全然違うでしょうね。

タイムマシンがあるとしたら本屋のポイントカードを作りに戻ろうと思っていましたが、とらドラ!をリアタイするというのも悪くないかもしれません。

アニメビジュアル絵画展

そうしてアニメ視聴後特有の喪失感に苛まれる私。ここぞとばかりに90周年アニメビジュアル絵画展を開催するキングレコード

https://kingrecords90th.com/

1月7日(金)~ 1月16日(日)の期間に池袋のアニメイトで開催されています。さっそく観てきました。ちゃんと額に入った本物は撮影NGでしたので以下はプリントですが↓

実物は絵がとても綺麗に見えました。実際、すごい技術でなんだかすごいらしいです。

■キャラファイングラフとは
高品位美術印刷技術「キャラファイン印刷」により高品質・高細に再現した高級複製絵画。
デジタルリトグラフ印刷手法・12色顔料による高級美術プリントにより複製された作品を、額縁に入れてお届けいたします。

強そうな額縁もビデオ会議の背景映えしそうで欲しいですね。税込み13,200円で受注販売されるとのこと。やっぱり財力のある大人が正義だな。ガハハ。

「キングレコード90周年展示」特設ページ | アニメイト

帰省したい

そういえばマンガはまだ完結していませんでした。とりあえず実家の既刊を読みたいので、次の帰省は早くなりそうです。

ユビゲーム

飽きっぽいので、やったことはその瞬間にアウトプットしておきたいなと思いました。調べたことなどを書きます。

お休みなので勉強がてらあのゲームを作りました。

f:id:mtzml:20220101205103p:plain
あのゲーム

「戦争」や「いっせーのせ」にはバラエティに富んだ名前があるのに、コイツの名前は聞いたことがないですね。とりあえずYubiGameとして開発を進めます。

こういうった遊びは名前もいろいろですがルールもいろいろ。私の地元では自分の両手の本数を分配できるルールがありました。が、これは実装していません。作るの飽きちゃったので。

WebSocket

Webの対戦ゲームなのでWebScoketを使います。

mtzml.hatenablog.com

アーキテクチャは↑と同じですね。フロントエンド200行、バックエンド150行くらいでした。

素材

Canvasで動的に手の画像を生み出そうかと思いましたが、パターンも少ないので全通り自分で書きました。昨年Apple Pencilを購入したおかげで、こういった素材を自分で用意できるようになったことは進歩。素材だけでなく設計などの構想を描くのにも使えて便利。

ibisPaintを使ってみているのですがオススメがあったら知りたいです。

ドラッグ&ドロップ

「直感的に操作できたらオシャレじゃね?」と思ったのでドラッグ&ドロップで操作できるようにしました。

f:id:mtzml:20220101214947g:plain
ドラッグ&ドロップで操作

実装にはReact DNDを使います。手のコンポーネントがこんな感じ。

const Hand = ({ id, hand, name, canDrag, canDrop, callback }) => {
  // ドラッグ設定
  const [, drag] = useDrag({
    type: 'HAND',
    canDrag: (_) => canDrag,
    end: (_, monitor) => {
      const dropResult = monitor.getDropResult();
      if (dropResult && name !== dropResult.name && id !== dropResult.id) {
        callback({
          fromHand: hand, 
          target: dropResult.id,
          toHand: dropResult.hand
        });
      }
    }
  });
  // ドロップ設定
  const [, drop] = useDrop({
    accept: 'HAND',
    canDrop: (_,) => canDrop,
    drop: () => ({ id, name, hand }),
  });

  return (
    <div ref={drop}>
      <img ref={drag} alt={name} src={`/image/${name}.png`} />
    </div>
  );
};

今回のケースではドラッグする対象とドロップする対象が同一なので、このコンポーネントにどちらの定義も記載します。2021年9月のver14でuseDrag()の引数に変更があったので注意。

Release 14.0.0 · react-dnd/react-dnd · GitHub

// v14より前
const [, drag] = useDrag({
  item: {
    type: 'HAND'
  }
});
// v14以降
const [, drag] = useDrag({
  type: 'HAND'
});

長押しアクションを無効化

スマホでドラッグ&ドロップしようとすると、長押しメニューが表示されてしまって煩わしい。これを無効化します。今回のケースとは逆ですが、コンテキストメニューの有効化などは過去に試していました。

mtzml.hatenablog.com

ひとまずCSSはこれで。

img {
  -webkit-touch-callout: none;
}

-webkit-touch-calloutWebKit用のプロパティであり、Safari専用の非標準機能です。使わない方がいいですね。Safari用と書きましたが、iOSのブラウザのレンダリングエンジンは(多分)すべてWebKitです。ChromeもBlinkではなくWebKit。全人類iPhoneの世界を想定しているのでこれで大丈夫でしょう。

Tailwind CSS

今さら感はいなめないですが触ったことがなかったので導入。公式のサンプルどおりにログインフォームを作ってみます。

const Login = () => {
  const [ room, setRoom ] = useState(null);

  return (
    <div className="text-center flex flex-col justify-center items-center">
      <img alt="logo" src="/image/logo.png" />
      <div className="flex items-center border-b border-blue-500 py-2">
        <input
          className="appearance-none bg-transparent border-none w-full text-gray-700 mr-3 py-1 px-2 leading-tight focus:outline-none"
          placeholder="Room Name"
          onChange={(e) => setRoom(e.target.value)}
        />
        <button
          className="flex-shrink-0 bg-blue-500 hover:bg-blue-700 border-blue-500 hover:border-blue-700 text-sm border-4 text-white py-1 px-2 rounded"
          disabled={!room}
          onClick={() => socket.emit(LOGIN, { room })}
        >
          Join
        </button>
      </div>
    </div>
  )
};

CSSクラスがカオスですね。「汎用的なクラスを組み合わせてデザインをカスタマイズするのがTailwindのやり方だ!」ってこと。コンポーネントにデザインをまとめられる点は好き。

Tailwindから逸れますが、socketが関数外部に依存しているのでヨクナイ。。

それから、あぁ、シンタックスハイライトが。スマホでは横スクロールもなくて読みづらい。この辺りのブログの設定を今年やりたいな。

roomを空にする

v4であればsocketsLeaveというメソッドが用意されているらしい。StackOverflowですがコピペしていないので大丈夫ですよ。*1

node.js - how to delete a room in socket.io - Stack Overflow

io.in("room1").socketsLeave("room1");

公式はこの辺ですね。

The Server instance | Socket.IO

Some utility methods were added in Socket.IO v4.0.0 to manage the Socket instances and their rooms:

v4万歳。修正コミットはこちら

集合型: Set

socket.roomsに参加しているroomの一覧が格納されているらしい。 この一覧にはsocket.idも格納されているので、それを省いてroomを取得しようとしたらエラー。

// NG
const room = socket.rooms.find(room => room !== socket.id);

このプロパティ、配列ではなく集合型でした。いろいろ参考。

// for
for (const room of socket.rooms) {/*  */}
// forEach
socket.rooms.forEach(room => {/*  */});

競プロで「Set使おうねー」って学んだ気がするけど使った試しがないです。

おわりに

ゲームでもつくってバズろうと思ったんですが、このゲームってタイミングをあわせる必要もないしzoomなどで手軽にできるんですよね。対人ならレートが必要だし、強いCPUを作ったほうが良さそうだなと思いました。

2021年好きな楽曲1選

共感覚というと大げさですが、一時期ハマっていた曲を聴くと当時の匂いがします。そんな匂う曲があったので今年も1選を書くことにしました。

楽曲オリンピック、やっていきましょう。

DECO*27 - ヴァンパイア feat. 初音ミク

(詞・曲:DECO*27 編:Rockwell


www.youtube.com

今年は『ヴァンパイア』に金メダルを進呈します🏅

小悪魔かわいいダンスチューン。韻を踏んだリズミカルな歌詞がかなり好き。ちょうど3分間の疾走感が病みつき。

イラストも曲の象徴となっていてキュートですよね。小悪魔だか地雷だか知らんけど、赤のアイシャドウが素敵。ご時世柄のマスクも添えて。初見でエヴァンゲリオンの顎の装甲みたいだと思ってマジでごめん。

お詫びに彼女の紹介。

曲に話を戻すと「あたしヴァンパイア↑」が癖になるね。一気に音が跳ね上がるので、歌ってみたなどを聴いていて気持ちいいポイントでもあります。1オクターブ下げるといい感じの音域なので自分で歌うのも好きです。絶叫。

お次は歌詞。

割り切れないけど余りじゃない

割り切れない気持ちは大事なもので余りじゃない。多分深い。

きみもヴァンパイア

大サビは「きみ」もヴァンパイアに?「あたし」に噛まれちゃったのでしょうか。ヤンデレは怖いですね。

試したいな いっぱいで吐きたい まだ絶いけるよ

タイタイタイタイ(CV.野原ひまわり

「いっぱいで吐きたい」これはもう完全に酒のことですね。酒飲もう酒。酒のアテも用意した。

明日使えるトリビア。あたしヴァンパイアと辛子明太子は似ている。共感覚で『ヴァンパイア』を聴いたら明太子の味がする日も近い…?

それではまた来年〜👋