内容
AWS Athenaへ用意されているテーブルのデータを、SQLを使って取得できるよう目指す方向け「入門編」です。
その他ボリュームページ
公式説明書
Athena エンジンバージョン2は Presto 0.217 に基づいています。
Athenaへ用意されているテーブルからデータを取得するSQLが書けるようになるには、6. Funcsiont and Operatorsのページが参考になります。
SQL基本操作
SQLは表で管理しているデータから、該当データを抽出して取得できます。
以下のような表Aがあるとします。
商品コード | 商品名 | 単価 |
---|---|---|
001 | 消しゴム | 80 |
002 | 鉛筆 | 70 |
003 | 筆箱 | 750 |
004 | ボールペン | 120 |
005 | 電卓 | 780 |
006 | 三角定規 | 200 |
SQLは以下のような形でデータを取得できます。
SELECT 列名 FROM 表名 WHERE 条件
列を取り出す(SELECT)
SELECT句はどの列を取り出すかを決めることができます。
表Aから「商品コード」の列を取り出したい場合、SELECT 商品コード
となります。
(結果はこんな感じ)
商品コード |
---|
001 |
002 |
003 |
004 |
005 |
006 |
全ての列を取り出したい場合、SELECT *
と書きます。
表を選択する(FROM)
FROM句ではどの表を対象とするかを選択することができます。
今回は表Aが対象ですので、FROM 表A
となります。
行を選択する(WHERE)
WHERE句は特定の条件に合致する行を取り出すことができます。
表Aから「単価が100未満」の行を取り出したい場合、WHERE 単価 < 100
となります。
(結果はこんな感じ)
商品コード | 商品名 | 単価 |
---|---|---|
001 | 消しゴム | 80 |
002 | 鉛筆 | 70 |
データを操作する基本の句として
- SELECT
- FROM
- WHERE
の3つがあります。
以降は、オリジナルのデータを使って解説していきます。
練習用データ
Athenaで以下のクエリを「実行」すると、テーブルを作ることができます。
このテーブルデータを使って、以降は解説しますので、Athenaのクエリ実行環境がある方は、是非作って試してみてください。
テーブル1(practice_user)
CREATE TABLE practice_user AS
SELECT
user_id,
sex,
age,
pref,
cast(created_at as timestamp) as created_at
FROM (
VALUES
(1, '女性', '32', '東京都','2021-10-06 12:20:45'),
(2, '男性', '26', '青森県','2021-10-24 14:47:20'),
(3, '女性', '49', '岐阜県','2021-11-18 02:50:33'),
(4, '女性', '48', '東京都','2021-09-03 01:11:07'),
(5, '男性', '20', '大阪府','2021-08-30 01:43:22'),
(6, '男性', '70', '京都府','2021-08-12 21:15:59'),
(7, '女性', '不明', '東京都','2021-09-03 03:38:01'),
(8, '女性', '18', '石川県','2021-08-16 18:40:18'),
(9, '男性', '22', '山口県','2021-12-21 08:55:13'),
(10, '女性', '53', '鹿児島県','2021-08-13 20:37:22')
) AS t (user_id, sex, age, pref, created_at)
テーブル2(practice_product)
CREATE TABLE practice_product AS
SELECT
item_id,
product_name,
unit_price,
product_group
FROM (
VALUES
('001', '消しゴム', 80, 0),
('002', '鉛筆', 70, 0),
('003', '筆箱', 750, 1),
('004', 'ボールペン', 120, 0),
('005', '電卓', 780, 1),
('006', '三角定規', 200, 1),
('007', 'ノート', 180, 0),
('008', 'コンパス', 600, 1),
('009', '下敷き', 320, 0),
('010', 'シャーペン', 150, 0)
) AS t (item_id, product_name, unit_price, product_group)
テーブル3(practice_sales)
CREATE TABLE practice_sales AS
SELECT
sale_id,
cast(sales_at as timestamp) as sales_at,
substr(sales_at,1,10) as sales_date,
item_id,
quantity,
user_id
FROM (
VALUES
(0,'2022-02-23 22:41:24','006',4, 8),
(1,'2021-09-20 17:30:11','001',4, 5),
(2,'2022-01-28 17:48:15','010',5, 3),
(3,'2021-09-22 23:59:06','009',3, 4),
(4,'2022-01-19 04:41:30','010',3, 2),
(5,'2021-08-21 05:27:01','007',1, 10),
(6,'2021-09-22 14:43:39','003',5, 4),
(7,'2021-11-29 03:45:11','010',4, 3),
(8,'2021-11-04 13:03:38','007',5, 1),
(9,'2022-02-15 05:38:25','001',4, 10),
(10,'2022-02-03 17:39:21','009',4, 3),
(11,'2022-01-26 06:57:37','005',1, 9),
(12,'2021-11-27 13:56:16','002',1, 7),
(13,'2021-08-26 21:59:46','004',1, 8),
(14,'2021-11-12 13:55:27','009',4, 2),
(15,'2021-12-22 05:27:00','001',3, 5),
(16,'2022-03-12 14:23:05','009',5, 4),
(17,'2021-08-20 13:17:50','006',5, 10),
(18,'2021-09-17 16:55:10','010',3, 5),
(19,'2021-09-20 16:55:53','005',3, 7),
(20,'2022-02-23 18:09:06','007',3, 8),
(21,'2021-09-13 20:33:34','004',1, 10),
(22,'2022-02-22 14:21:04','001',4, 9),
(23,'2022-03-05 02:18:10','009',5, 10),
(24,'2022-01-13 19:43:05','005',4, 4),
(25,'2021-11-29 20:24:37','010',4, 3),
(26,'2021-09-06 04:58:59','006',3, 4),
(27,'2021-12-26 02:54:35','006',2, 7),
(28,'2021-11-23 14:32:08','006',3, 4),
(29,'2021-10-26 07:44:13','009',4, 5),
(30,'2021-09-22 06:00:45','009',5, 10),
(31,'2021-10-27 10:54:27','009',4, 6),
(32,'2021-11-14 08:48:17','005',3, 5),
(33,'2022-01-31 17:15:35','009',4, 4),
(34,'2021-09-15 03:41:44','006',1, 4),
(35,'2022-01-27 05:43:28','003',5, 8),
(36,'2021-12-13 06:00:52','001',3, 8),
(37,'2022-03-28 22:45:28','004',4, 2),
(38,'2021-10-25 01:48:08','007',1, 6),
(39,'2022-02-22 17:28:21','006',1, 6),
(40,'2022-03-19 18:34:07','004',2, 4),
(41,'2022-01-06 07:27:04','010',1, 9),
(42,'2022-03-10 10:30:38','010',3, 3),
(43,'2022-03-22 04:07:53','009',3, 5),
(44,'2022-02-07 00:27:35','004',5, 6),
(45,'2021-09-30 04:26:58','004',2, 5),
(46,'2022-02-13 16:10:24','006',3, 8),
(47,'2021-12-17 08:52:51','001',1, 6),
(48,'2022-02-18 05:34:58','002',2, 1),
(49,'2021-12-23 23:58:23','009',1, 9)
) AS t (sale_id, sales_at, item_id, quantity, user_id)
論理演算子
AND
ANDは「かつ」を意味していて、aとbの条件にANDを付けて a AND b とすると、aかつbを満たすという意味になります。
【例】practice_productテーブルから、unit_priceが100以上かつproduct_groupが0のデータを出したい
SELECT
*
FROM
practice_product
WHERE
unit_price >= 100
AND
product_group = 0
unit_priceが100以上でproduct_groupが0の4商品が出てきました。
OR
ORは「または」を意味していて、aとbの条件にORを付けて a OR b とすると、aまたはbを満たすという意味になります。
【例】practice_productテーブルから、unit_priceが100以上またはproduct_groupが0のデータを出したい
SELECT
*
FROM
practice_product
WHERE
unit_price >= 100
OR
product_group = 0
unit_priceが100以上またはproduct_groupが0の全ての商品が出てきました。
今回の例で出てこない場合があるのは、unit_priceが100未満のproduct_groupが1の商品となります。(存在しないのですべての商品が出てきました)
NOT
NOTは「ではない」を意味していて、aの条件にNOTをつけて not a とすると、aではないという意味になります。
【例】practice_productテーブルから、unit_priceが100以上ではないデータを出したい
SELECT
*
FROM
practice_product
WHERE
NOT unit_price >= 100
unit_priceが100以上ではない2商品が出てきました。
比較演算子/範囲演算子
論理演算子にて、unit_priceが100以上を>=100
としましたが、この他、以下のような比較演算子があります
コマンド | 内容 |
---|---|
< | 未満 |
> | より大きい |
<= | 以下 |
>= | 以上 |
= | 等しい |
<> | 等しくない |
!= | 等しくない |
BETWEEN a AND b | aとbの間 |
こちらについては、比較のコマンドを変更して結果を試してみてください。
BETWEEN
BETWEENのみ説明します。
BETWEEN a AND b
は
a <= 対象列 <= b
と同等です。
【例】practice_productテーブルから、unit_priceが200以上600以下のデータを出したい
SELECT
*
FROM
practice_product
WHERE
unit_price BETWEEN 200 AND 600
データの整列
データを昇順または降順で並べたい場合、ORDER BY 列名 ASC or DESC
と書きます
ORDER BYはWHERE句の後、つまり一番最後に書きます。
ASCは昇順、DESCは降順です。昇順の場合、ASCは除いても構わないです。
昇順
【例】practice_productテーブルを、unit_price昇順に並べたい
SELECT
*
FROM
practice_product
ORDER BY unit_price ASC
結果
降順
【例】practice_productテーブルを、unit_price昇順に並べたい
SELECT
*
FROM
practice_product
ORDER BY unit_price DESC
昇順・降順複合
ORDER BYは複数列選択することができ、それぞれ昇順降順を選択できます。
【例】practice_productテーブルを、product_groupを降順、unit_price昇順に並べたい
SELECT
*
FROM
practice_product
ORDER BY product_group DESC, unit_price ASC
注意点
昇順降順ですが、データの型に気を付けないと、意に反する結果となる事があります。
主には文字列型(varchar)に対してORDER BY
すると起こります。
型については後述しますが、Athenaのテーブルのプラスボタンから見ることができます。
practice_userテーブルのuser_idをわざとvarcharに変換して降順にしてみます。
SELECT * FROM practice_user
ORDER BY cast(user_id as varchar) desc
結果
9が一番大きく、10は2番目に小さい値となってしまいました。
varchar型での並べ替えの場合、1文字目のUnicodeコードポイントでの並べ替えになるので、このような形になります。
別名を付ける
表や列に別名を付ける場合 AS
を使います。
次の集計でASを用いますので。説明します。
【例】practice_productテーブルのproduct_groupをgroup_number
という列名に変更して、全列を表示したい
SELECT
item_id,
product_name,
unit_price,
product_group as group_number
FROM
practice_product
注意点
別名を付ける際、分かりやすさのために日本語を付けたくなりますが、通常とは違う書き方をする必要がありますので、お勧めしません。
※ 以下のように”
で括る必要があります。
SELECT
item_id,
product_name,
unit_price,
product_group as "製品グループ"
FROM
practice_product
集計
SQLではデータを取り出す際に、平均を取ったり、最大値を取ったりと集計を行うことができます。
代表的な(よく使う)ものとしては以下があります。
コマンド | コマンド |
---|---|
MAX(列名) | 列の最大値を求める |
MIN(列名) | 列の最小値を求める |
AVG(列名) | 列の平均値を求める |
SUM(列名) | 列の合計を求める |
COUNT(列名) | 列のデータのある行数を求める |
COUNT(DISTINCT 列名) | 列のデータの重複値を除いた行数を求める |
APPROX_PERCENTILE(列名, 0.5) | 列の中央値を求める(パーセンタイル値を用いる) |
MAX
【例】practice_productテーブルの、unit_price最大値を求めたい
SELECT
MAX(unit_price) as max_unit_price
FROM
practice_product
MIN
【例】practice_productテーブルの、unit_price最小値を求めたい
SELECT
MIN(unit_price) as min_unit_price
FROM
practice_product
結果
AVG
【例】practice_productテーブルの、unit_price平均値を求めたい
SELECT
AVG(unit_price) as avg_unit_price
FROM
practice_product
SUM
【例】practice_productテーブルの、unit_price合計値を求めたい
SELECT
SUM(unit_price) as sum_unit_price
FROM
practice_product
COUNT
【例】practice_productテーブルの、product_groupの行数を求めたい
SELECT
COUNT(product_group) as count_product_group
FROM
practice_product
選択した列のデータにNULLがある場合、その行はカウントされません。
COUNT(DISTINCT 列名)
【例】practice_productテーブルの、product_groupのデータ重複を除いた行数を求めたい
SELECT
COUNT(DISTINCT product_group) as distinct_count_product_group
FROM
practice_product
結果
選択した列のユニークなデータの個数を求めるような形です。
APPROX_PERCENTILE(列名, 0.5)
【例】practice_productテーブルの、unit_price中央値を求めたい
SELECT
APPROX_PERCENTILE(unit_price, 0.5) as median_unit_price
FROM
practice_product
Athenaの場合、中央値を求める関数が無いため、パーセンタイル値の50%点で代用します。
よって、総数が偶数の場合でも真ん中に最も近い2つの値の平均を取るようなことをしませんので、200が返ってきます。
グループ化
集計を「特定のグループごと」に行いたい場合、GROUP BY
を用います。
GROUP BY
は、WHERE句などで条件を書いた後に書きます。
【例】practice_productテーブルの、product_groupごとに、unit_price平均値を求めたい
SELECT
AVG(unit_price) as avg_unit_price
FROM
practice_product
GROUP BY product_group
これだと、出てきた2つのどちらがproduct_groupの0,1なのかが分からないですね。
SELECTで列にも追加してあげるのがよく使う方法です。
SELECT
product_group,
AVG(unit_price) as avg_unit_price
FROM
practice_product
GROUP BY product_group
このようにしてSELECTで「特定のグループごと」の列も呼び出す場合、呼び出した列は必ずGROUP BYに入れる必要があります。
- 実行できない例
SELECT
product_group,
item_id,
AVG(unit_price) as avg_unit_price
FROM
practice_product
GROUP BY product_group
グループ化からの条件を付ける
GROUP BY
をした後の結果に対してWHEREのように条件を付けたい場合があります。
その場合はHAVING
を用います。
【例】practice_productテーブルの、product_groupごとに、unit_price平均値を求めて、unit_price平均値が500以上のデータを出したい
SELECT
product_group,
AVG(unit_price) as avg_unit_price
FROM
practice_product
GROUP BY product_group
HAVING AVG(unit_price) >= 500
Athenaでは HAVING AVG(unit_price)
の部分をASで変更した列名にすることはできません。
つまり、HAVING avg_unit_price >= 500
とすることはできません。
表の結合
表を選択するFROM
では、複数の表を選ぶことができます。
練習用データの表同士の関係は、以下の図のようになっています。
表と表は、特定の列を結合してくっつけることができます。
くっつける場合は、どの列と列が結びついているのかをWHERE句に書く必要があります。
【例】practice_salesとpractice_userの表を選択して結合したい
SELECT
*
FROM
practice_sales,practice_user
WHERE
practice_sales.user_id = practice_user.user_id
結果
practice_salesのデータとpractice_userのデータをくっつけることができました。
ただ、列名に同じ名前が含まれていることは好ましくありません。
practice_sales.user_idとpractice_user.user_idで結合したので、「user_id」という列が2つ存在してしまっています。
表を結合したときは、同じ列名のものが無いようにして、どの列を選択してきているのかきちんと書いてあげるのが良いです。
SELECT
practice_sales.sale_id,
practice_sales.sales_at,
practice_sales.item_id,
practice_user.user_id,
practice_user.pref
FROM
practice_sales,practice_user
WHERE
practice_sales.user_id = practice_user.user_id
列の選択では、「表名.列名」のようにしていますが、「列名」だけでSELECTすると、どちらの表にも存在する列名の場合、エラーを吐いてしまいます。
結合時は「表名.列名」とかいてSELECTして上げるのが良いと思います。
- どちらの表にも存在する列名の為、エラーを吐いてしまう
SELECT
user_id
FROM
practice_sales,practice_user
WHERE
practice_sales.user_id = practice_user.user_id
結合の種類
この他、表と表を結合する操作として以下があります。
下の表では分かりづらいかもしれません。図でこの後説明しますので、そちらもご覧ください。
コマンド | 内容 |
---|---|
(INNER) JOIN | 両方に存在するデータを結合 |
LEFT (OUTER) JOIN | 左側に書いた表をベースとして結合 |
RIGHT (OUTER) JOIN | 右側に書いた表をベースとして結合 |
FULL (OUTER) JOIN | 両方に存在するデータは結合し、存在しないデータはそのままくっつく |
CROSS JOIN | 結合に選択した列名と列名の全てのパターンを生成(クロス積) |
この後に、以下の句を書いて、結合する列名を定義します。
コマンド | 内容 |
---|---|
ON | 互いの表で結合する列名をそれぞれ記載 |
USING | 互いの表に共通する列名を1つ記載 |
書き方は
SELECT
列名
FROM 表名
INNER JOIN 表名
ON 表名.列名 = 表名.列名 (もしくは USING 列名)
のような形です
INNER JOIN
JOIN
またはINNER JOIN
と書くと、両方に存在するデータを結合することができます。
【例】practice_productとpractice_salesの先頭3行の表を使って、INNER JOINする
SELECT
*
FROM practice_product
INNER JOIN (SELECT * FROM practice_sales limit 3) as practice_sales -- practice_salesから先頭3行取得した表をpractice_salesと定義
ON practice_product.item_id = practice_sales.item_id
表の結合で貼った図の赤線同士が繋がっているデータのみ抽出できました。
ちなみにこれは、FROM句で2つの表を書いてWHERE句で条件を書いた
SELECT
user_id
FROM
practice_sales,practice_user
WHERE
practice_sales.user_id = practice_user.user_id
この例も、動作としてはINNER JOIN
と同じです。
また、例ではON
句を使っていましたが、互いに列名が同じ場合はUSING
句を使うことができます。
SELECT
*
FROM practice_product
INNER JOIN (SELECT * FROM practice_sales limit 3) as practice_sales -- practice_salesから先頭3行取得した表をpractice_salesと定義
USING (item_id)
USING
句の場合、結合後に同名の列が出てくるのを避けることができます
ON
句の場合と比べて、item_id列が1つ減っています。
LEFT JOIN
LEFT JOINは左側、つまりFROM句で選択した表をベースとして、LEFT JOINで選択した表を結合します。
【例】practice_productとpractice_salesの先頭3行の表を使って、LEFT JOINする
SELECT
*
FROM practice_product
LEFT JOIN (SELECT * FROM practice_sales limit 3) as practice_sales -- practice_salesから先頭3行取得した表をpractice_salesと定義
ON practice_product.item_id = practice_sales.item_id
左側をベースとして右側を結合するので、右側にデータが無いものはNULL(データなし)として結合されます。
RIGHT JOIN
RIGHT JOINは右側、つまり RIGHT JOINで選択した表をベースとして、FROM句で選択した表を結合します。
【例】practice_productとpractice_salesの先頭3行の表を使って、RIGHT JOINする
SELECT
*
FROM practice_product
RIGHT JOIN (SELECT * FROM practice_sales limit 3) as practice_sales -- practice_salesから先頭3行取得した表をpractice_salesと定義
ON practice_product.item_id = practice_sales.item_id
右側のitem_idは左側にもすべて存在するので、結果としてはINNER JOINと同じになりました。
右側にしかないitem_idがもしあった場合は、左側のデータとしてはその行はNULL(データなし)として結合されます。
FULL JOIN
FULL JOINは両側にデータがあるものはINNER JOIN、片側データが無いものについては、もう片側がNULL(データなし)となります。
結果を見たほうが理解が早いかと思います。
practice_product表も先頭5行のみにして、以下のような関係にします。
これらについて、FULL JOINしてみます。
【例】practice_productの先頭5行の表とpractice_salesの先頭3行の表を使って、FULL JOINする
SELECT
*
FROM (SELECT * FROM practice_product limit 5) as practice_product -- practice_productから先頭5行取得した表をpractice_productと定義
FULL JOIN (SELECT * FROM practice_sales limit 3) as practice_sales -- practice_salesから先頭3行取得した表をpractice_salesと定義
ON practice_product.item_id = practice_sales.item_id
結果
両側にデータがあるものは互いに結合され、片方にしかないものは、もう片方がNULL(データなし)として結合されます。
CROSS JOIN
CROSS JOINは結合する表同士の全ての組み合わせを返します。
ON
やUSING
句は使いません。
【例】practice_productの先頭5行の表とpractice_salesの先頭3行の表を使って、CROSS JOINする
SELECT
*
FROM (SELECT * FROM practice_product limit 5) as practice_product -- practice_productから先頭5行取得した表をpractice_productと定義
CROSS JOIN (SELECT * FROM practice_sales limit 3) as practice_sales -- practice_salesから先頭3行取得した表をpractice_salesと定義
色のついた枠を見ると分かりやすいかもしれませんが、右側の表のitem_id列1つ1つに対して左側の表のitem_idの全てのパターンが結合されています。
左側5行と右側3行のCROSS JOINなので、5 x 3で15行のデータが抽出されます。