GitリモートリポジトリをGitHubからCodeCommitに引っ越しする



Githubリモートリポジトリをブランチやコミット履歴を含めてまるごとCodeCommitにお引っ越しする手順。
引っ越しにはリポジトリのブランチ・タグ、コミット履歴など全てのリファレンスを含んだバックアップであるbareリポジトリを利用する。


bareリポジトリとは




リポジトリの構成管理


  • ブランチ・タグ、コミット履歴など全てのリファレンスを含む、サーバーサイドの構成管理リポジトリであり完全バックアップである。管理対象のファイル実態は持たない。
  • git clone --bareオプションでbareリポジトリをクローンできる。
  • git clone --mirrorオプションでもbareリポジトリをクローンできる。--bareとの違いは文字通りリモートリポジトリと同期するように設計されていて、.git/configはmirror=trueである。よってデフォルトでは全リファレンスをpushするし、git remote updateでは全リファレンスを追跡する

--mirror Set up a mirror of the source repository. This implies --bare. Compared to --bare, --mirror not only maps local branches of the source to local branches of the target, it maps all refs (including remote-tracking branches, notes etc.) and sets up a refspec configuration such that all these refs are overwritten by a git remote update in the target repository.


ソースリポジトリのミラーを設定します。これは --bare を含意します。--bare と比較して、--mirror はソースのローカルブランチをターゲットのローカルブランチにマッピングするだけでなく、すべてのリファレンス(リモート追跡ブランチ、ノートなども含む)をマッピングし、これらのリファレンスがターゲットリポジトリの git remote update によって上書きされるように refspec 設定を行います。

Git - git-clone Documentation


non-bareリポジトリとは




リポジトリのワークスペース


  • チェックアウト済のブランチ・タグのコミット履歴とファイル実態で構成されるクライアントサイドのワークスペースである。
  • いつものgit clonenon-bareリポジトリ。

引っ越し手順




CodeCommitでリポジトリを作成して、IAMユーザにCodeCommitのプッシュできるポリシーとSSH公開鍵を登録しておく


# GitHubからbareリポジトリをクローンする
git clone --mirror git@github.com:path/to/reponame.git

#  push先をCodeCommitのリポジトリに変更
cd ./reponame.git && git remote set-url --push origin ssh://git-codecommit.ap-northeast-1.amazonaws.com/path/to/reponame

# 全てのリファレンスを移行先にプッシュする(--mirror)
# (clone --mirror により、configのmirror=true状態のため、オプションなしでも全リファレンスはプッシュされる)
git push --mirror

S3に格納されたマネージドログをAthenaで解析する



AthenaでS3バケットにストックしているマネージドのログに対してクエリ検索する。
 

データベースを作成する

Athena でのデータベースの作成 - Amazon Athena コンソール上にファンクションはない。クエリエディタで作成する。  

CREATE DATABASE db-name;

 

テーブルを作成する

 

パーティション化

クエリ対象のスキャン量は料金コストに直結するため、パーティションでデータを絞り込んでコスト節約する。 パーティション化するためにS3のログの格納ルールとテーブルDDLに定義が必要。  

データをパーティションすることで、各クエリによってスキャンされるデータの量を制限できるようになるため、パフォーマンスが向上し、コストが削減されます。任意のキーでデータをパーティションに分割することができます。一般的な方法では、時間に基づいてデータをパーティションします。

Athena でのデータのパーティション化 - Amazon Athena

S3格納ルール

  対象ログが日付等の一定ルールに基づいたパーティションに格納されていること。フォーマットには「Hive形式」と「Hiveではない形式」がある。 Hive形式はパーティションが追加される度にテーブルのリペアが必要なので、ログのエクスポート設定はHiveではない方式にする。

Hiveではない形式

s3://path/to/2021/01/26/

Athena では、Hive 以外のスタイルのパーティション化スキームを使用することも可能です。例えば、 CloudTrail ログと Firehose 配信ストリームは、 などの日付部分に対して個別のパスコンポーネントを使用しますdata/2021/01/26/us/6fc7845e.json。これらの Hive スタイルではないパーティションの場合、ALTER TABLE ADD PARTITION を使用して手動でパーティションを追加します。

Athena でのデータのパーティション化 - Amazon Athena

Hive形式

  • ディレクトリが「key=value」形式
  • パーティションが追加される度にテーブルのリペアが必要
s3://path/to/year=2021/month=01/day=26/

Athena では Apache Hive スタイルのパーティションを使用できます。このパーティションのデータパスには、等号で連結されたキーと値のペア (例えば country=us/... または year=2021/month=01/day=26/...) が含まれています。つまり、それぞれのパスにより、パーティションのキーと値、両方の名前が表されます。新しい Hive パーティションをパーティションされたテーブルにロードするには、(Hive スタイルのパーティションのみで機能する) MSCK REPAIR TABLE コマンドを使用します。

Athena でのデータのパーティション化 - Amazon Athena

テーブル

【初心者向け】Amazon Athenaのパーティションとは  Amazon AthenaのPartition Projectionを使ったALBのアクセスログ解析環境をTerraformで構築する – PSYENCE:MEDIA

テーブル作成のDDLにPARTITIONED BY句とTBLPROPERTIES句を追加して、パーティション射影を有効にする

  • LOCATION句にS3のパーティション構造の上位ディレクトリまで指定する
  • PARTITIONED句にパーティション名を定義する
  • TBLPROPERTIES句にパーティションルールを定義する。projection.{パーティション名}.*にパーティションのルールを定義する。
CREATE EXTERNAL TABLE `table-name`( ... )
PARTITIONED BY (`log_date` string)
LOCATION 's3://{バケット名}/AWSLogs/{アカウントID}/elasticloadbalancing/{リージョン}'
TBLPROPERTIES (
  -- パーティション有効
  'projection.enabled'='true', 
  -- パーティションのデータ型 enum,integer,date,injectedをサポートしている
  'projection.log_date.type'='date',
  -- パーティションのフォーマット=S3のパーティション
  'projection.log_date.format'='yyyy/MM/dd',
  -- パーティション間隔 = 1日おき
  'projection.log_date.interval'='1',
  'projection.log_date.interval.unit'='DAYS',
  -- パーティションのレンジ
  'projection.log_date.range'='NOW-1YEARS,NOW',
  -- パーティションを含むS3バケットのロケーション
  'storage.location.template'='s3://{バケット名}/AWSLogs/{アカウントID}/elasticloadbalancing/{リージョン}/${log_date}'
)

Application Load Balancer ログのクエリ - Amazon Athena

テーブル作成のスニペット

 

ALBアクセスログの場合

 Application Load Balancer ログのクエリ - Amazon Athena

CREATE EXTERNAL TABLE `alb-access-log`(
  `type` string COMMENT '',
  `time` string COMMENT '',
  `elb` string COMMENT '',
  `client_ip` string COMMENT '',
  `client_port` int COMMENT '',
  `target_ip` string COMMENT '',
  `target_port` int COMMENT '',
  `request_processing_time` double COMMENT '',
  `target_processing_time` double COMMENT '',
  `response_processing_time` double COMMENT '',
  `elb_status_code` string COMMENT '',
  `target_status_code` string COMMENT '',
  `received_bytes` bigint COMMENT '',
  `sent_bytes` bigint COMMENT '',
  `request_verb` string COMMENT '',
  `request_url` string COMMENT '',
  `request_proto` string COMMENT '',
  `user_agent` string COMMENT '',
  `ssl_cipher` string COMMENT '',
  `ssl_protocol` string COMMENT '',
  `target_group_arn` string COMMENT '',
  `trace_id` string COMMENT '',
  `domain_name` string COMMENT '',
  `chosen_cert_arn` string COMMENT '',
  `matched_rule_priority` string COMMENT '',
  `request_creation_time` string COMMENT '',
  `actions_executed` string COMMENT '',
  `redirect_url` string COMMENT '',
  `lambda_error_reason` string COMMENT '',
  `target_port_list` string COMMENT '',
  `target_status_code_list` string COMMENT '',
  `classification` string COMMENT '',
  `classification_reason` string COMMENT '')
PARTITIONED BY (
  `log_date` string)
ROW FORMAT SERDE
  'org.apache.hadoop.hive.serde2.RegexSerDe'
WITH SERDEPROPERTIES (
  'input.regex'='([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*):([0-9]*) ([^ ]*)[:-]([0-9]*) ([-.0-9]*) ([-.0-9]*) ([-.0-9]*) (|[-0-9]*) (-|[-0-9]*) ([-0-9]*) ([-0-9]*) \"([^ ]*) ([^ ]*) (- |[^ ]*)\" \"([^\"]*)\" ([A-Z0-9-]+) ([A-Za-z0-9.-]*) ([^ ]*) \"([^\"]*)\" \"([^\"]*)\" \"([^\"]*)\" ([-.0-9]*) ([^ ]*) \"([^\"]*)\" \"([^\"]*)\" \"([^ ]*)\" \"([^s]+?)\" \"([^s]+)\" \"([^ ]*)\" \"([^ ]*)\"')
STORED AS INPUTFORMAT
  'org.apache.hadoop.mapred.TextInputFormat'
OUTPUTFORMAT
  'org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat'
LOCATION
  's3://{バケット名}/AWSLogs/{アカウントID}/elasticloadbalancing/{リージョン}'
TBLPROPERTIES (
  'projection.enabled'='true',
  'projection.log_date.format'='yyyy/MM/dd',
  'projection.log_date.interval'='1',
  'projection.log_date.interval.unit'='DAYS',
  'projection.log_date.range'='NOW-1YEARS,NOW',
  'projection.log_date.type'='date',
  'storage.location.template'='s3://{バケット名}/AWSLogs/{アカウントID}/elasticloadbalancing/{リージョン}/${log_date}'
)

 

SELECT
  *
FROM "alb-access-log"
WHERE
  -- パーティションを選択
  log_date BETWEEN '2023/09/01' AND '2023/09/24'
  -- 条件を実装
  AND elb_status_code = '504'

 

VPCフローログの場合

Amazon VPC フローログのクエリ - Amazon Athena  

CREATE EXTERNAL TABLE `vpc-flow-log`(
  `version` int,
  `accountid` string,
  `interfaceid` string,
  `srcaddress` string,
  `dstaddress` string,
  `srcport` int,
  `dstport` int,
  `protocol` int,
  `packets` int,
  `bytes` bigint,
  `timefrom` int,
  `timeto` int,
  `action` string,
  `logstatus` string,
  `instanceid` string,
  `subnetid` string,
  `vpcid` string)
PARTITIONED BY (
  `log_date` string
)
ROW FORMAT DELIMITED
FIELDS TERMINATED BY ' '
STORED AS INPUTFORMAT
  'org.apache.hadoop.mapred.TextInputFormat'
OUTPUTFORMAT
  'org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat'
LOCATION
  's3://{バケット名}/AWSLogs/{アカウントID}/vpcflowlogs/{リージョン}'
TBLPROPERTIES (
  'projection.enabled'='true',
  'projection.log_date.format'='yyyy/MM/dd',
  'projection.log_date.interval'='1',
  'projection.log_date.interval.unit'='DAYS',
  'projection.log_date.range'='NOW-1YEARS,NOW',
  'projection.log_date.type'='date',
  'skip.header.line.count'='1',
  'storage.location.template'='s3://{バケット名}/AWSLogs/{アカウントID}/vpcflowlogs/{リージョン}/${log_date}'
)

 

SELECT
  *
FROM vpc-flow-log
WHERE 
  -- パーティションを選択
  log_date >= '2023/05/09'
  -- 条件を実装
  AND dstport = 22

 

WAFログの場合

AWS WAF ログのクエリ - Amazon Athena  

CREATE EXTERNAL TABLE `waf_acl-log`(
  `timestamp` bigint COMMENT 'from deserializer',
  `formatversion` int COMMENT 'from deserializer',
  `webaclid` string COMMENT 'from deserializer',
  `terminatingruleid` string COMMENT 'from deserializer',
  `terminatingruletype` string COMMENT 'from deserializer',
  `action` string COMMENT 'from deserializer',
  `terminatingrulematchdetails` array<struct<conditiontype:string,location:string,matcheddata:array<string>>> COMMENT 'from deserializer',
  `httpsourcename` string COMMENT 'from deserializer',
  `httpsourceid` string COMMENT 'from deserializer',
  `rulegrouplist` array<struct<rulegroupid:string,terminatingrule:struct<ruleid:string,action:string>,nonterminatingmatchingrules:array<struct<action:string,ruleid:string>>,excludedrules:array<struct<exclusiontype:string,ruleid:string>>>> COMMENT 'from deserializer',
  `ratebasedrulelist` array<struct<ratebasedruleid:string,limitkey:string,maxrateallowed:int>> COMMENT 'from deserializer',
  `nonterminatingmatchingrules` array<struct<action:string,ruleid:string>> COMMENT 'from deserializer',
  `httprequest` struct<clientip:string,country:string,headers:array<struct<name:string,value:string>>,uri:string,args:string,httpversion:string,httpmethod:string,requestid:string> COMMENT 'from deserializer')
PARTITIONED BY (
  `date` string
)
ROW FORMAT SERDE
  'org.openx.data.jsonserde.JsonSerDe'
STORED AS INPUTFORMAT
  'org.apache.hadoop.mapred.TextInputFormat'
OUTPUTFORMAT
  'org.apache.hadoop.hive.ql.io.IgnoreKeyTextOutputFormat'
LOCATION
  's3://{バケット名}/${date}'
TBLPROPERTIES (
  'classification'='csv',
  'compressionType'='gzip',
  'delimiter'='|',
  'projection.date.format'='yyyy/MM/dd',
  'projection.date.interval'='1',
  'projection.date.interval.unit'='DAYS',
  'projection.date.range'='NOW-1YEARS,NOW',
  'projection.date.type'='date',
  'projection.enabled'='true',
  'storage.location.template'='s3://{バケット名}/${date}',
  'typeOfData'='file'
)

 

SELECT from_unixtime(timestamp/1000, 'Asia/Tokyo') AS JST, *
FROM waf_acl-log
WHERE
  -- パーティションを選択
  date BETWEEN '2023/11/01' AND '2023/11/1'
  AND action = 'BLOCK'
--    AND action = 'ALLOW'
--    AND terminatingruleid = 'AWS-AWSManagedRulesSQLiRuleSet'
--    AND terminatingruleid = 'AWS-AWSManagedRulesPHPRuleSet'
ORDER BY timestamp ASC
LIMIT 100;

MySQLでクエリのパフォーマンスを改善する

クエリ性能を考慮したスキーマ設計&クエリを実装するための覚え書き。

インデックスの効用

顕著に効果が出るケース


データ量=NLJ突合量が多いテーブル同士のJOINでは、内部表の結合キーに思考停止レベルでインデックスを貼る

  1. テーブル内のデータ量が多く、少量のレコードを検索する場合
  2. WHERE句の条件、結合の条件、ORDER BY句の条件として頻繁に利用するただし全表走査が目的のテーブルであれば索引は不要
  3. NULL値が多いデータから、NULL値以外の検索をするIndexはNULLを含まないのでNULL値以外の検索には効果がある

MySQLのIndexをはるコツ

  • ただし表から大部分のレコードを検索する場合、オプティマイザは効率的にフルスキャンを選択する場合はある
  • 追加・更新頻度が高い表は索引の再構築が走るため性能低下に繋がるリスクがある

インデックスが使えない検索条件

SQL実践入門──高速でわかりやすいクエリの書き方 (WEB+DB PRESS plus)


中間一致、後方一致のLIKE術語(前方一致はOK)

  • ... WHERE col_1 LIKE '%検索値'
  • ... WHERE col_1 LIKE '%検索値%'
  • ... WHERE col_1 LIKE '検索値%'

索引列に関数を使っている/演算を行っている

  • ... WHERE LENGTH(col_1) = 10
  • ... WHERE col_1 * 1.1 > 100
  • ... WHERE col_1 > 100 / 1.1

IS NULL術語を使っている

「索引データにNULLは存在しないため」…とあったが、実行計画を見る限りref~constでヒットされた。

否定形を使っている( <> / != / NOT IN)

  • ... WHERE col_1 <> 100

アプリケーション設計


  • ナノ秒早くなるから予約語は大文字に統一する
  • 雰囲気でORDERしない
  • WHEREとORDERはひとつのテーブルに集中する
  • 否定演算は等価演算にする
  • JOINの駆動表は行数の少ないテーブルを選択する

実行計画

スロークエリはEXPLAINで当該クエリの実行計画を確認してボトルネックを分析する。

まとめ

EXPLAINコマンドの各フィールドの詳細を説明したが、実際にEXPLAINコマンドを使ってクエリの実行計画を見る際には次のようなステップを踏むといいだろう。

  1. id/select_type/tableフィールドを見て、どのテーブルがどの順序でアクセスされるのかを知る。これらはクエリの構造を示すフィールドであると言える。サブクエリが含まれている場合にはEXPLAINの表示順とアクセスされる順序が異なる場合があるので気をつける必要がある。
  2. type/key/ref/rowsフィールドを見て、各テーブルから行がどのようにフェッチされるのかを知る。どのテーブルへのアクセスが最も重いか(クエリの性能の足を引っ張っているのか)を、これらのフィールドから判断することが出来る。
  3. Extraフィールドを見て、オプティマイザがどのように判断して、各々のテーブルへのアクセスにおいて何を実行しているのかを知る。Extraフィールドはオプティマイザの挙動を示すものであり、クエリの全体像を把握するのに役立つ。

同じ結果が得られるなら、ひとつのクエリにおいてテーブルからフェッチされる行数が減れば減るほど良い。なぜなら、ストレージエンジンの性能には上限があるからである。例えば、あるストレージエンジンが1秒間に100万行のフェッチが出来る性能を持っているとすると、一つのクエリが平均1000行のフェッチを行うならば1秒間に最大1000クエリ、一つのクエリが平均100行のフェッチを行うならば1秒間に最大1万クエリが可能であるという計算になる。ひとつのクエリがフェッチする行数が減れば、クエリのレスポンスだけでなく全体のスループットも向上するのである。

漢(オトコ)のコンピュータ道: MySQLのEXPLAINを徹底解説!!

select_type 注目


 SIMPLE

そのクエリはJOINも含む単独のクエリ

サブクエリ系

サブクエリの場合、外部クエリとサブクエリでは別々のidが付番される

 PRIMARY

そのクエリは外部クエリを指している tableが<derived>なら実態はDERIVEDサブクエリ

 SUBQUERY

そのクエリは相関関係のないサブクエリを指している。 サブクエリが実行されるのは最初の一回だけで、それ以降はキャッシュされた実行結果が利用されている。

 DERIVED

そのクエリはFROM句で用いられているサブクエリを指している

 ❗DEPENDENT SUBQUERY

そのクエリは相関関係のあるサブクエリ

対策

  • サブクエリ側にインデックスを張って改善する
  • 非相関クエリになるようにクエリを再設計する

table 情報


対象のテーブル名

type 注目


対象のテーブルに対してどのような方法でアクセスするかを示す。

 const👏

プライマリキーまたはユニークキーによって、単一のステートメントに一致した。

 eq_ref👏

JOINにおいてプライマリキーまたはユニークで単一のステートメントに一致した。

 ref👏

インデックスで単一のステートメントに一致した。

 range👏

インデックスによる範囲検索でステートメントに一致した。

 ❗index

フルインデックススキャンした。

対策

 ❗ALL

フルテーブルスキャンした

対策

  • インデックスを作成する

possible_keys


オプティマイザがテーブルのアクセスに利用可能なインデックスの候補として挙げたキーの一覧。

key


実際にオプティマイザによって選択されたキー。

key_len


選択されたキーの長さ。キー長は短い方が早い

ref


検索条件で、keyと比較されている値やカラムを示す。

  • 定数が指定されている場合はconst。
  • JOINの場合は、結合する相手側のテーブルで検索条件として利用されているカラム。

rows


対象tableからフェッチされる行数の見積もりを示す。

  • Extra=Using whereの状況では、フェッチした行に対してさらにWHERE句の検索条件が適用されて行の絞り込みが行われるので、結果は少なくなる可能性がある。
  • JOINの場合、最終的な結果行数の見積もりはJOINする全てのテーブルのrowsフィールドの積である。
  • レコードアクセスタイプがeq_ref(プライマリキー or ユニークキー)の場合、rowsフィールドは1になる。

Extra


オプティマイザがどのような戦略を選択したかということを示す

Using index👏

クエリがインデックスだけを用いて解決された

Using index for group-by👏

MIN()/MAX()がGROUP BY句と併用されているとき、クエリがインデックスだけを用いて解決された

Using where

インデックスだけではWHERE句の条件を全て適用することが出来ていない

対策

  • カバリングインデックスを作成する

Range checked for each record (index map: N)

JOINにおいてrangeまたはindex_mergeが利用された

Not exists

LEFT JOINにおいて、左側のテーブルからフェッチされた行にマッチする行が右側のテーブルに存在しない場合、右側のテーブルはNULLとなるが、右側のテーブルがNOT NULLとして定義されたフィールドでJOINされている場合にはマッチしない行を探せば良い・・・ということを示す。

❗Using filesort

ソート処理にインデックスが使われず、クイックソートされた。

対策

❗Using temporary

ソートのために一時テーブルが作成された。

対策

まとめ。

以上の話をまとめると次のようになる。

  • Filesortとはクイックソートのことである。
  • JOINにおいてORDER BYが指定されているとき、ソートが行われるタイミングは3種類ある。
    • Using filesortがEXPLAINに出ていないときはインデックスを用いてソートが行われている。(最も高速。)
    • Using filesortだけがEXPLAINの最初の行に出ている時は、先にソートしてからJOINが実行されている。
    • Using temporary; Using filesortがEXPLAINの最初の行に出ている時は、先にJOINしてからソートされている。(最も遅い。)
  • WHERE句による検索条件とORDER BYにおけるソート条件は、一つのテーブルに集中させる。
  • JOINにおいてソート処理する場合には、LIMIT句が適用されるのはJOINとソートが完了した後である。
  • LEFT/RIGHT JOINの場合には、LIMIT句の問題はサブクエリである程度対策が可能である。

漢(オトコ)のコンピュータ道: Using filesort

用語

  • NLJ (ネステッド・ループ・ジョイン) テーブル結合のアルゴリズム。結合元テーブル駆動表×被結合テーブル内部表の直積。
  • 駆動表 NLJにおける結合元テーブル
  • 内部表 NLJにおける被結合テーブル
  • 外部クエリ サブクエリからみたPRIMARYのクエリ
  • 相関クエリ サブクエリにおいて、メインクエリの各行に対してサブクエリが実行されるため、オーダー数が大爆発する(dependent subquery)。非相関サブクエリとは、まずサブクエリが実行され、その結果をメインクエリに渡す。実行計画が逆。
  • カバリングインデックス クエリ(WHERE~ORDER=>SELECT)が必要とする情報を網羅した複合インデックスであり、インデックススキャンのみで高速にデータを取得するために最適化設計したインデックス。達成した暁にはExplainのExtraにて、Using Indexの誉が与えられる。

調べ物

駆動表に小さなテーブルを選ぶ理由


結果的に駆動表(n)と内部表(m)の突合数はどちら(nm or mn)にせよを変わらないのでは、、

・駆動表の重要性 Nested Loopsの性能を改善するキーワードとして「駆動表に小さなテーブルを選ぶ」ということを聞いたことのある人もいると思います。これは大方針として間違いではないのですが、実はある前提条件がないと意味がないので、なぜ駆動表が小さいほうが性能的に有利なのか、それが意味を持つ条件は何なのか、その理由をここで理解しておきましょう。 実際、上で解説した Nested Loopsのしくみを前提すると、駆動表がどちらのテーブルになっても、結局のところアクセスされる行数はR(A)✕R(B)で表現されるのだから、駆動表が小さかろうが大きかろうが、結合コストに違いはないように思われます。実は、この「駆動表を小さく」という格言には、次のような暗黙の前提が隠れています。 内部表の結合キーの列にインデックスが存在すること もし内部表の結合キーの列にインデックスが存在する場合、そのインデックスをたどることによって、DBMSは駆動表の1行に対して内部表を馬鹿正直にループする必要がなくなります。いわば内部表のループをある程度スキップできるようになるのです

SQL実践入門──高速でわかりやすいクエリの書き方 (WEB+DB PRESS plus)

内部表はインデックス付き結合キーで高速にルックアップできるため、あとは駆動表が小さいほど結合の効率がよいということだ。

where句またはjoin句にANDで複数条件を与えたときインデックスは使われないのか


  • クエリに用いられるインデックスはテーブルごとに1つで、オプティマイザが最適なインデックスを選択する。
  • クエリ条件&ソートに登場するカラムを網羅的にカバーする複合インデックスが必要。=カバリングインデックス
  • 複合インデックスに対してクエリはインデックス定義順に前方一致でオーダーしなければならない。

実例で学ぶ、JOIN (NLJ) が遅くなる理屈と対処法 #MySQL - Qiita

order by句にインデックスは使われないのか


Using filesortを解消するためにソートしたいカラムにインデックスを張ったが使われない。

-- at_dayカラムにはインデックスを張っているのにUsing filesort
EXPLAIN SELECT * FROM reports ORDER BY at_day DESC;
id	select_type	table	partitions	type	possible_keys	key	key_len	ref	rows	filtered	Extra
1	SIMPLE	reports	NULL	ALL	NULL	NULL	NULL	NULL	240	100.00	Using filesort

なぜなら、'Using index'は(カバリングインデックスを使用している場合など)インデックスの情報のみを使用してデータを取得できる場合に使用できる方法だからです。

MySQLのインデックスがうまく効かない MySQL :: MySQL 8.0 リファレンスマニュアル :: 8.2.1.16 ORDER BY の最適化

  • Using index 

実際の行を読み取るための追加のシークを実行する必要がなく、インデックスツリーの情報のみを使用して、テーブルからカラム情報が取得されます。 この戦略は、クエリーで単一のインデックスの一部であるカラムのみを使用している場合に使用できます。

MySQL :: MySQL 8.0 リファレンスマニュアル :: 8.8.2 EXPLAIN 出力フォーマット

インデックスツリーの情報のみを使用してテーブルからカラムが取得するためには、SELECTでカバリングインデックスに含まれないカラムを指定してはいけない。

-- インデックスに含まれるカラムのみをselectするとUsing index
EXPLAIN SELECT at_day FROM reports ORDER BY at_day DESC;
id	select_type	table	partitions	type	possible_keys	key	key_len	ref	rows	filtered	Extra
1	SIMPLE	reports	NULL	index	NULL	idx_at_day	3	NULL	240	100.00	Using index

select_type.DEPENDENT SUBQUERYとは


explain select * from employees e
where exists ( select * from reports r where r.`employee_id` = e.id );
id	select_type	table	partitions	type	possible_keys	key	key_len	ref	rows	filtered	Extra
1	PRIMARY	e	NULL	ALL	NULL	NULL	NULL	NULL	11	100.00	Using where
2	DEPENDENT SUBQUERY	r	NULL	ref	idx_employee_id	idx_employee_id	4	kkk.e.id	30	100.00	Using where; Using index
  • DEPENDENT SUBQUERYとはいわゆる相関サブクエリに相当するもので、サブクエリにおいて外部クエリのカラムを参照しているサブクエリのことである。
  • 現時点でのMySQL(バージョン5.1)では、サブクエリはまず外部クエリの条件から評価される。そして、外部クエリの条件に合致する行が見つかると、その行がサブクエリの条件に合致するかどうかが評価されるわけである。即ち、サブクエリにおいてフェッチしなければいけない行数が平均N行、外部クエリでフェッチされる行数がM行のとき、サブクエリにおいてM×N行の評価が行われることになる。これは膨大な計算量である。
  • MySQLは内部的にINを直接処理することができないので、EXISTSに変換することでSQL的には相関のないサブクエリも相関サブクエリになってしまうのである。これがまさにMySQLのサブクエリが遅い!と言われている原因だろう。

なぜMySQLのサブクエリは遅いのか。

相関サブクエリに相当する。外部クエリでフェッチしたステートメントごとに、サブクエリを都度フェッチして評価するため計算量が膨らむため遅い。以下対策。

  • 外部クエリのフェッチを絞る
  • サブクエリにインデックスを張る

参考文献


SQL実践入門──高速でわかりやすいクエリの書き方 (WEB+DB PRESS plus)

インデックスの定石系

実行計画系

SSHトンネルで開発環境からリモートのMySQLホストにつないでセキュアにデバッグ

sshホストがフォワード先


ローカルホスト:13306にバインドして、sshリモートホストmysql.host:22にssh接続し、localhost:3306にループバックポートフォワードする


ssh -N -L 13306:localhost:3306 -p 22 -i ./id-rsa.pem user@mysql.host

踏み台sshホスト経由


  • 直にssh接続できないホスト(RDSインスタンス)の場合
  • データベースサーバーがインターネットフェイス非公開の場合

ローカルホスト:13306にバインドして、sshリモートホストbation.host:22にssh接続し、mysql.host:3306にポートフォワードしている


ssh -N -L 13306:mysql.host:3306 -p 22 -i ./id-rsa.pem user@bation.host

前提としてbation.host から mysql.hostへファイアウォールでインバウンド許可していること
フォワード先のホストがインターナルネットワークのみ許可している場合、インターネットフェイスなDNSアクセスだとグローバルIPなのでファイアウォールで弾かれる(挙動としては応答なしで延々と捕まる・固まる)。

SSHコマンド

ssh
	-N # リモートコマンドを実行しない=ターミナルを開かない=トンネル専用
	-L {ローカルホストのバインドポート}:{フォーワード先のホスト}:{フォワード先のポート}
	-p {sshホストのポート} 
	-i {ssh鍵ファイル}
	{ユーザ名}@{sshホスト}

フォーワード先のホストsshホストからみたネットワーク名


BloggerにGoogleフォーム×reCAPTCHAでお問い合わせフォームを作成する


Bloggerで連絡フォームウィジェットがエラーで送信できなかったのでGoogleフォームを使ってカスタムフォームを実装したはなし。


お問い合わせページ



連絡フォームウィジェットのエラー原因


フォーム送信先のエンドポイントhttps://www.blogger.com/contact-form.doがオリジンhttps://blog.bigbridge.work対して、Access-Control-Allow-Origin許可してくれないためCORSエラーが発生していた。

リクエスト詳細


# 概要
リクエスト URL:https://www.blogger.com/contact-form.do
リクエスト メソッド:POST
ステータス コード:200 OK
参照ポリシー:strict-origin-when-cross-origin

# リクエストヘッダ
:authority:www.blogger.com
:method:POST
:path:/contact-form.do
:scheme:https
Accept:*/*
Accept-Encoding:gzip, deflate, br, zstd
Accept-Language:ja,en-US;q=0.9,en;q=0.8
Content-Length:170
Content-Type:application/x-www-form-urlencoded;charset=UTF-8
Origin:https://blog.bigbridge.work
Referer:https://blog.bigbridge.work/
Sec-Ch-Ua:"Google Chrome";v="123", "Not:A-Brand";v="8", "Chromium";v="123"
Sec-Ch-Ua-Mobile:?0
Sec-Ch-Ua-Platform:"macOS"
Sec-Fetch-Dest:empty
Sec-Fetch-Mode:cors
Sec-Fetch-Site:cross-site
User-Agent:Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36

# ペイロード部
## フォーム
name: テスト
email: test@bigbridge.work
message: テスト
## トークン
blogID: ******************************
token: AOuZoY6iKedo_5ezKrVuDX5Zx7eK_7l4xg:1711659541683

レスポンス詳細


Access-Control-Allow-Originヘッダがない


# レスポンスヘッダ
Alt-Svc:h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
Cache-Control:no-cache, no-store, max-age=0, must-revalidate
Content-Encoding:gzip
Content-Length:70
Content-Security-Policy:script-src 'self' *.google.com *.google-analytics.com 'unsafe-inline' 'unsafe-eval' *.gstatic.com *.googlesyndication.com *.blogger.com *.googleapis.com uds.googleusercontent.com https://s.ytimg.com https://i18n-cloud.appspot.com https://www.youtube.com www-onepick-opensocial.googleusercontent.com www-bloggervideo-opensocial.googleusercontent.com www-blogger-opensocial.googleusercontent.com https://www.blogblog.com; report-uri /cspreport
Content-Type:text/javascript; charset=UTF-8
Date:Thu, 28 Mar 2024 20:59:49 GMT
Expires:Mon, 01 Jan 1990 00:00:00 GMT
P3p:CP="This is not a P3P policy! See https://www.google.com/support/accounts/bin/answer.py?hl=en&answer=151657 for more info."
Pragma:no-cache
Server:GSE
X-Content-Type-Options:nosniff
X-Frame-Options:SAMEORIGIN
X-Xss-Protection:1; mode=block

Bloggerフォーラムに送信トークンについて仕様変更を示唆するコメントがあった。Contact us Stopped Working - Blogger Community


一応リクエストのペイロードにはトークンらしきものは添加されているが、そもそも使用テーマWrite Simpleの公開時期が2016年とかなり古いため現在の要件を満たしていないのかもしれない。使用テーマは変えたくなかったのでGoogleフォームを利用してフォームを実装する。


お問い合わせフォームの要件


お問い合わせに求める要件は次の通り


  • お問い合わせを受信出来て、履歴を一元的に管理できること
  • フォームをスタリングできること
  • reCAPTCHAでボット対策できること
  • 入力内容に対してバリデーションできること
  • 送信完了後は完了画面にフォワードできること

お問い合わせを受信出来て、履歴を一元的に管理できること

Google formコンソール
お問い合わせのスプレッドシート


お問い合わせはGoogleフォームで受信し、管理する。すべての受信履歴はスプレッドシートにスタックされる。



回答タブでメール通知はONにしておく



フォームをスタリングできること


Googleフォームをサイトにインラインフレームで組み込むと、入力から完了までGoogleフォームまるだしだ。これはいかん。是が非でもサイトのスタイリングに統一したい。



Googleフォームのaction属性とフォームパーツのname属性を拝借して、自前で組んだフォームにそれぞれ充てる。



こんな感じで、GoogleフォームにPOSTが出来る。
(が、送信後Googleフォームの送信完了画面にリダイレクトするため、自前の完了画面に遷移する実装を後述)


<form action="https://docs.google.com/forms/u/0/d/e/1FAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/formResponse" method="POST">
    <div>
        <div><span>お名前</span></div>
        <div><input type="text" name="entry.2XXXXXXXX"></div>
    </div>
    <div>
        <div><span>メールアドレス</span></div>
        <div><input type="email" name="entry.3XXXXXXXX"></div>
    </div>
    <div>
        <div><span>お問い合わせ内容</span></div>
        <div><textarea name="entry.1XXXXXXXX" rows="10"></textarea></div>
    </div>
    <div>
		<input type="submit" value="送信">
    </div>
</form>

reCAPTCHAでボット対策できること

reCAPTCHAコンソール


reCAPTCHA-V2を設置して、reCAPTCHA検証イベントで送信ボタンを活性状態に切り替える。
フォームの送信タイミングでバリデーションなど追加実装を割り込ませるためにsubmitボタンをbuttonに変更して初期状態はdisabled


<script src="https://www.google.com/recaptcha/api.js"></script>
<form action="https://docs.google.com/forms/u/0/d/e/1FAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/formResponse" method="POST">
	...
	<!-- それらしく送信ボタンの前にreCAPTCHAを組み込む -->
	<div class="g-recaptcha" data-callback="gValidatedCallback" data-sitekey="6XXXXXXXXXXXXXXX_FXXXXXXXXXXXXXXXXXXXXXX" disabled></div>
	<!-- 送信ボタン 初期状態は非活性-->
    <div>
		<button id="id-contact-form-submit" class="submit" disabled>送信</button>
    </div>
</form>

reCAPTCHA検証イベントで送信ボタンを活性状態にスイッチする。disabledフラグだけではhtml改ざんが出来るのでJSで追加検証のフラグも実装する。


/************************************************
 * reCAPTCHA検証のコールバック
 * コードがあれば=判定通過で、ボタンのdisableを解除して通過フラグを立てる
 ************************************************/
let gValidated = false;
function gValidatedCallback(code) {
    if(code !== ''){
  		$('#id-contact-form-submit').removeAttr("disabled");
		gValidated = true;
	}
}

/************************************************
 * 送信ボタンのコールバック
 * - reCAPTCHA検証フラグを追加検証する
 ************************************************/
$(document).on('click', '#id-contact-form-submit', function(){  
	try{
        if( !gValidated ) throw new Error('reCAPTCHA検証を完了してください。');
	}catch(error){
    	alert(error.message);
	}
	return false;
});

送信完了後は完了画面にフォワードできること


GoogleフォームのactionにPOSTするとGoogleフォーム完了画面にリダイレクトしてしまうので、formのtarget属性にダミーの非表示インラインフレームを指定してGoogleフォームのレスポンスを葬りつつ、ロケーションを送信完了ページに移動する。


<form id="id-contact-form" action="https://docs.google.com/forms/u/0/d/e/1FAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/formResponse" method="post" target="hidden_iframe">
	...
</form>
<iframe name="hidden_iframe" style="display:none;"></iframe>

...
/************************************************
* 送信ボタンのコールバック
* - reCAPTCHA検証フラグを追加検証する
* - Googleフォームに送信する
* - ロケーションを完了画面に移動する
 ************************************************/
$(document).on('click', '#id-contact-form-submit', function(e){
	try{
		// バリデーション
        if( !gValidated ) throw new Error('reCAPTCHA検証を完了してください。');

		// フォーム送信 => レスポンスは"target="hidden_iframe"に葬られる
		$('#id-contact-form').submit();
		// ロケーションを完了画面に移動する
		window.location='/p/been-contact.html';      
	}catch(error){
    	alert(error.message);
	}
	return false;
});

入力内容に対してバリデーションできること


Googleフォームの送信結果はインラインフレームに葬るため、送信結果を受け取ることが出来ないのでフロントでフォーム入力値のバリデーションを実装する。
※なおajaxによるフォーム送信ではCORSエラーである


/************************************************
 * メールアドレスのフォーマット検証
 * @return bool true: 合格 / false: 不合格
 ************************************************/
function isValidEmail(email) {
  const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
  return emailRegex.test(email);
}

/************************************************
 * 送信ボタンのコールバック
 * - reCAPTCHA検証フラグを追加検証する
 * - フォーム入力値を検証する
 * - Googleフォームに送信する
 * - ロケーションを完了画面に移動する
 ************************************************/
$(document).on('click', '#id-contact-form-submit', function(e){
    
	try{
		// バリデーション
        if( !gValidated ) throw new Error('reCAPTCHA検証を完了してください。');

		// 必須フォームフィールド
		const nm_name	= 'entry.2XXXXXXXX';
		const nm_email	= 'entry.3XXXXXXXX';
		const nm_body	= 'entry.1XXXXXXXX';

		if( !$(`input[name="${nm_name}"]`).val() ) throw new Error('お名前を入力してください');
		if( !$(`input[name="${nm_email}"]`).val() ) throw new Error('メールアドレスを入力してください');
		if( !isValidEmail( $(`input[name="${nm_email}"]`).val() ) ) throw new Error('メールアドレスのフォーマットが不正です');
		if( !$(`textarea[name="${nm_body}"]`).val() ) throw new Error('お問い合わせ内容を入力してください');
            
		// フォーム送信
		$('#id-contact-form').submit();
		// ロケーション移動
		window.location='/p/been-contact.html';
      
	}catch(error){
    	alert(error.message);
	}
	return false;
});

最終的なお問い合わせページの実装


要件を満たした最終的なお問い合わせページの実装


<script src="https://www.google.com/recaptcha/api.js"></script>
<script>
let gValidated = false;
/************************************************
 * reCAPTCHA検証のコールバック
 * コードがあれば=判定通過で、ボタンのdisableを解除して通過フラグを立てる
 ************************************************/
function gValidatedCallback(code) {
    if(code !== ''){
  		$('#id-contact-form-submit').removeAttr("disabled");
		gValidated = true;
	}
}

/************************************************
 * メールアドレスのフォーマット検証
 * @return bool true: 合格 / false: 不合格
 ************************************************/
function isValidEmail(email) {
  const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
  return emailRegex.test(email);
}

/************************************************
 * 送信ボタンのコールバック
 * - reCAPTCHA検証フラグを追加検証する
 * - フォーム入力値を検証する
 * - Googleフォームに送信する
 * - ロケーションを完了画面に移動する
 ************************************************/
$(document).on('click', '#id-contact-form-submit', function(e){
	try{      
        if( !gValidated ) throw new Error('reCAPTCHA検証を完了してください。');

		const nm_name = 'entry.2XXXXXXXX';
		const nm_email = 'entry.3XXXXXXXX';
		const nm_body = 'entry.1XXXXXXXX';
		if( !$(`input[name="${nm_name}"]`).val() ) throw new Error('お名前を入力してください');
		if( !$(`input[name="${nm_email}"]`).val() ) throw new Error('メールアドレスを入力してください');
		if( !isValidEmail( $(`input[name="${nm_email}"]`).val() ) ) throw new Error('メールアドレスのフォーマットが不正です');
		if( !$(`textarea[name="${nm_body}"]`).val() ) throw new Error('お問い合わせ内容を入力してください');
      
		$('#id-contact-form').submit();
		window.location='/p/been-contact.html';
	}catch(error){
    	alert(error.message);
	}
	return false;
});
</script>

<style>
  
  .formgroup{}
  .formgroup:not(last-child){
    margin-bottom: 2em;
  }
  .formgroup__label{
    color: #000;
  }
  .formgroup__form > * {
    	width: 100%;
  }
  .formgroup .require{
  		font-size: 0.8em;
    	color: #f00;
  }
  .formgroup .require:before{
    	margin-left: 0.8em;
    	content: '*';
  }
  
  button {
  		text-shadow: none;
  }
    
  .submit{
    	color: #fff;
  		background-color: #000;
  }
  .submit:hover{
    	color: #000;
  		background-color: #fff;
  }
  .submit:disabled{
    	color: #000;
  		background-color: #ccc;
  }
  
  .g-recaptcha{
		margin: 1em 0;
  }

</style>

<p>フォームにお問い合わせ内容を入力して送信してください。</p>

<form id="id-contact-form" action="https://docs.google.com/forms/u/0/d/e/1FAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/formResponse" method="post" target="hidden_iframe">
	<div>
    	<div class="formgroup">
        	<div class="formgroup__label">
          		<span>お名前</span>
          	</div>
        	<div class="formgroup__form">
            	<input type="text" name="entry.2XXXXXXXX" >
          	</div>
    	</div>
    	<div class="formgroup">
        	<div class="formgroup__label">
              	<span>メールアドレス</span>
          	</div>
        	<div class="formgroup__form">
              	<input type="email" name="entry.3XXXXXXXX" >
        	</div>
    	</div>
    	<div class="formgroup">
        	<div class="formgroup__label">
              	<span>お問い合わせ内容</span>
          	</div>
        	<div class="formgroup__form">
              	<textarea name="entry.1XXXXXXXX" rows="10"></textarea>
          	</div>
    	</div>
  	</div>
	<div class="g-recaptcha" data-callback="gValidatedCallback" data-sitekey="6XXXXXXXXXXXXXXX_FXXXXXXXXXXXXXXXXXXXXXX" disabled></div>
	<button id="id-contact-form-submit" class="submit" disabled>送信</button>
</form>
<iframe name="hidden_iframe" style="display:none;"></iframe>