クエリを劇的に改善できる2つの高度なSQLテクニック

'2つの高度なSQLテクニックでクエリを劇的に改善する'

Common Table Expression(CTE)とウィンドウ関数について学ぶ

Photo by Karina Szczurek on Unsplash

SQLは、すべてのデータプロフェッショナルにとって重要な基礎です。データアナリスト、データサイエンティスト、またはデータエンジニアであっても、クリーンで効率的なSQLクエリの書き方についての確かな理解が必要です。

なぜなら、厳密なデータ分析や洗練された機械学習モデルの背後には、基になるデータ自体があり、このデータはどこかから取得される必要があるからです。

もし、SQLについての私の初心者向けブログ投稿を読んでいるなら、SQLはStructured Query Languageの略であり、リレーショナルデータベースからデータを取得するために使用される言語であることを既に学んでいるはずです。

そのブログ投稿では、SELECTFROMWHEREなどの基本的なSQLコマンドについて説明しました。これらのコマンドは、SQLを使用する際に遭遇する基本的なクエリのほとんどをカバーするはずです。

しかし、もし簡単なコマンドだけでは十分でない場合はどうなるでしょうか? 欲しいデータがより堅牢なクエリのアプローチを必要とする場合はどうでしょうか?

その場合は、Common Table Expression(CTE)とウィンドウ関数という2つの新しいSQLテクニックを学ぶべきです。

これらのテクニックを学ぶために、Google Cloudから取得したタクシーの乗車時間データセット(NYCオープンデータライセンス)を使用して、オンラインのSQLエディタであるDB Fiddle(SQLite v3.39に設定)を利用します。

データの準備

データセットの準備方法に興味がない場合は、このセクションをスキップし、以下のコードをDB Fiddleに貼り付けてスキーマを生成してください。

CREATE TABLE taxi (  id varchar,  vendor_id integer,  pickup_datetime datetime,  dropoff_datetime datetime,  trip_seconds integer,  distance float);INSERT INTO taxi VALUES('id2875421', 2, '2016-03-14 17:24:55', '2016-03-14 17:32:30', 455, 0.93), ('id2377394', 1, '2016-06-12 00:43:35', '2016-06-12 00:54:38', 663, 1.12), ('id3858529', 2, '2016-01-19 11:35:24', '2016-01-19 12:10:48', 2124, 3.97), ('id3504673', 2, '2016-04-06 19:32:31', '2016-04-06 19:39:40', 429, 0.92), ('id2181028', 2, '2016-03-26 13:30:55', '2016-03-26 13:38:10', 435, 0.74), ('id0801584', 2, '2016-01-30 22:01:40', '2016-01-30 22:09:03', 443, 0.68), ('id1813257', 1, '2016-06-17 22:34:59', '2016-06-17 22:40:40', 341, 0.82), ('id1324603', 2, '2016-05-21 07:54:58', '2016-05-21 08:20:49', 1551, 3.55), ('id1301050', 1, '2016-05-27 23:12:23', '2016-05-27 23:16:38', 255, 0.82), ('id0012891', 2, '2016-03-10 21:45:01', '2016-03-10 22:05:26', 1225, 3.19), ('id1436371', 2, '2016-05-10 22:08:41', '2016-05-10 22:29:55', 1274, 2.37), ('id1299289', 2, '2016-05-15 11:16:11', '2016-05-15 11:34:59', 1128, 2.35), ('id1187965', 2, '2016-02-19 09:52:46', '2016-02-19 10:11:20', 1114, 1.16), ('id0799785', 2, '2016-06-01 20:58:29', '2016-06-01 21:02:49', 260, 0.62), ('id2900608', 2, '2016-05-27 00:43:36', '2016-05-27 01:07:10', 1414, 3.97), ('id3319787', 1, '2016-05-16 15:29:02', '2016-05-16 15:32:33', 211, 0.41), ('id3379579', 2, '2016-04-11 17:29:50', '2016-04-11 18:08:26', 2316, 2.13), ('id1154431', 1, '2016-04-14 08:48:26', '2016-04-14 09:00:37', 731, 1.58), ('id3552682', 1, '2016-06-27 09:55:13', '2016-06-27 10:17:10', 1317, 2.86), ('id3390316', 2, '2016-06-05 13:47:23', '2016-06-05 13:51:34', 251, 0.81), ('id2070428', 1, '2016-02-28 02:23:02', '2016-02-28 02:31:08', 486, 1.56), ('id0809232', 2, '2016-04-01 12:12:25', '2016-04-01 12:23:17', 652, 1.07), ('id2352683', 1, '2016-04-09 03:34:27', '2016-04-09 03:41:30', 423, 1.29), ('id1603037', 1, '2016-06-25 10:36:26', '2016-06-25 10:55:49', 1163, 3.03), ('id3321406', 2, '2016-06-03 08:15:05', '2016-06-03 08:56:30', 2485, 12.82), ('id0129640', 2, '2016-02-14 13:27:56', '2016-02-14 13:49:19', 1283, 2.84), ('id3587298', 1, '2016-02-27 21:56:01', '2016-02-27 22:14:51', 1130, 3.77), ('id2104175', 1, '2016-06-20 23:07:16', '2016-06-20 23:18:50', 694, 2.33), ('id3973319', 2, '2016-06-13 21:57:27', '2016-06-13 22:12:19', 892, 1.57

SELECT * from taxi を実行した後、次のような結果のテーブルが表示されます。

Image by Author.

このテーブルが実際にどのようにしてできたのか知りたい人のために説明します。私はトレーニングデータを最初の30行にフィルタリングし、上記で表示されている列のみを保持しました。距離フィールドについては、ピックアップおよびドロップオフの座標(緯度と経度)間の直線距離を計算しました。

直線距離は球面上の2点間の最短距離であり、したがって実際のタクシーの移動距離よりも低くなります。ただし、今日行っている作業の目的には、これを無視しても問題ありません。

直線距離を計算するための式はこちらにあります。それでは、SQLに戻りましょう。

共通テーブル式(CTE)

共通テーブル式(CTE)は、クエリ内で返される一時テーブルです。クエリ内のクエリと考えることができます。これにより、クエリをより読みやすいチャンクに分割するだけでなく、定義されたCTEを基に新しいクエリを記述することができます。

これをデモンストレーションするために、1日の時間ごとに分割され、2016年1月から3月までの間に行われたトリップのみをフィルタリングしたいとします。

 SELECT CAST(STRFTIME('%H', pickup_datetime) AS INT) AS hour_of_day, trip_seconds, distance FROM taxi WHERE pickup_datetime > '2016-01-01' AND pickup_datetime < '2016-04-01' ORDER BY hour_of_day;  
Image by Author.

簡単ですね。さらに一歩進めましょう。

今度は、これらの時間ごとにトリップの数と平均速度を計算したいとします。これは、まず上記の一時テーブルのような一時テーブルを取得するためにCTEを利用し、次に時間ごとにグループ化してトリップ数と平均速度を計算する後続のクエリを利用できるところです。

CTEを定義する方法は、WITHおよびASステートメントを使用することです。

 WITH relevantrides AS (SELECT CAST(STRFTIME('%H', pickup_datetime) AS INT) AS hour_of_day, trip_seconds, distance FROM taxi WHERE pickup_datetime > '2016-01-01' AND pickup_datetime < '2016-04-01' ORDER BY hour_of_day) SELECT hour_of_day, COUNT(1) as num_trips, ROUND(3600 * SUM(distance) / SUM(trip_seconds), 2) as avg_speed FROM relevantrides GROUP BY hour_of_day ORDER BY hour_of_day;  
Image by Author.

CTEの代わりに、一時テーブルをFROMステートメントで囲むこともできます(以下のコードを参照)。これにより、同じ結果が得られます。ただし、コードの読みやすさの観点からはお勧めできません。さらに、1つ以上の一時テーブルを作成したい場合を想像してみてください。

 SELECT hour_of_day, COUNT(1) as num_trips, ROUND(3600 * SUM(distance) / SUM(trip_seconds), 2) as avg_speed FROM ( SELECT CAST(STRFTIME('%H', pickup_datetime) AS INT) AS hour_of_day, trip_seconds, distance FROM taxi WHERE pickup_datetime > '2016-01-01' AND pickup_datetime < '2016-04-01' ORDER BY hour_of_day) GROUP BY hour_of_day ORDER BY hour_of_day;  

ボーナス:この演習から抽出できる興味深い洞察は、タクシーはピーク時には通常よりもゆっくり移動する傾向がある(平均速度が低い)ことであり、これはおそらく通勤時間帯の混雑した交通状況によるものです。

ウィンドウ関数

ウィンドウ関数は、行のグループに対して集計演算を行いますが、元のテーブルの各行に対して結果を生成します。

ウィンドウ関数の動作を完全に理解するためには、まずGROUP BYを使用した集計の簡単な復習が役立ちます。

例えば、タクシーデータセットを使用して、月ごとにサマリー統計のリストを計算したいとします。

SELECT CAST(STRFTIME('%m', pickup_datetime) AS INT) AS month,       COUNT(1) AS trip_count,       ROUND(SUM(distance), 3) AS total_distance,       ROUND(AVG(distance), 3) AS avg_distance,       MIN(distance) AS min_distance,       MAX(distance) AS max_distanceFROM taxiGROUP BY month;
Image by Author.

上記の例では、データセット内の各個々の月について、個別の月ごとにカウント、合計、平均、最小、最大の距離が計算されています。元のタクシーテーブルは30行あったものが、6つの行にまとめられ、各個々の月ごとに1つの行が得られていることに注目してください。

では、実際にはどのような処理が行われているのでしょうか?まず、SQLは元のテーブルの30行を月ごとにグループ化しました。そして、これらの個別のグループの値に基づいて関連する計算を適用しました。

例えば、1月を取り上げてみましょう。データセットには1月に行われた2つのトリップがあり、それぞれの距離は3.97と0.68です。SQLはこれら2つの値に基づいてカウント、合計、平均、最小、最大を計算しました。このプロセスは他の月でも繰り返され、最終的に上記のような出力が得られます。

それでは、ウィンドウ関数の動作を探っていくにつれて、この考え方を頭に入れておいてください。ウィンドウ関数には、集計関数、ランキング関数、ナビゲーション関数の3つの大きなカテゴリがあります。それぞれの例を見ていきましょう。

集計関数

前の例で既に集計関数を使用した例を見てきました。集計関数には、カウント、合計、平均、最小、最大などの関数が含まれます。

しかし、ウィンドウ関数がGROUP BYと異なるのは、最終的な出力の行数です。具体的には、月ごとに集計した後、出力テーブルは6行(各異なる月ごとに1行)になります。

一方、ウィンドウ関数では、集計フィールドによるテーブルの要約は行わず、単に新しい列に結果を出力します。出力テーブルの行数は変わりません。つまり、出力テーブルの行数は常に元のテーブルと同じです。

ウィンドウ関数を実行するための構文はOVER(PARTITION BY ...)です。これは前の例でのGROUP BY文と考えることができます。

実際の動作を見てみましょう。

WITH aggregate AS(SELECT id,       pickup_datetime,       CAST(STRFTIME('%m', pickup_datetime) AS INT) AS month,        distanceFROM taxi)SELECT *,       COUNT(1) OVER(PARTITION BY month) AS trip_count,       ROUND(SUM(distance) OVER(PARTITION BY month), 3) AS total_month_distance,       ROUND(AVG(distance) OVER(PARTITION BY month), 3) AS avg_month_distance,       MIN(distance) OVER(PARTITION BY month) AS min_month_distance,       MAX(distance) OVER(PARTITION BY month) AS max_month_distanceFROM aggregate;
Image by Author.

ここでは、前回と同じ出力を得たいのですが、テーブルをまとめず、各行に個別の列として出力したいと思います。

集計後の値が変わらないことに注意してください。代わりに、テーブル内で値が繰り返し表示されるだけです。例えば、最初の2行(1月)は、トリップ数、合計月間距離、平均月間距離、最小月間距離、最大月間距離の値が前と同じです。他の月も同様です。

ウィンドウ関数がどのように役立つか疑問に思っている場合、それは各行の値を集計された値と比較するのに役立ちます。このインスタンスでは、各行の移動距離を月間平均、最小値、最大値などと簡単に比較することができます。

ランキング関数

もう一つのウィンドウ関数のタイプは、ランキング関数です。名前の通り、これは集計フィールドに基づいて複数の行をランク付けします。

WITH ranking AS(SELECT id,       pickup_datetime,       CAST(STRFTIME('%m', pickup_datetime) AS INT) AS month,        distanceFROM taxi)SELECT *,       RANK() OVER(ORDER BY distance DESC) AS overall_rank,       RANK() OVER(PARTITION BY month ORDER BY distance DESC) AS month_rankFROM rankingORDER BY pickup_datetime;
Image by Author.

上記の例では、2つのランキング列があります。1つは全体のランク(1から30)であり、もう1つは月間のランクで、どちらも降順です。

ランキングを指定するには、OVER ステートメント内で ORDER BY を使用する必要があります。

最初の行の結果を解釈する方法は、データセット全体で3番目に長い移動距離であり、1月の最も長い移動距離であるということです。

最後に、ナビゲーション関数があります。

ナビゲーション関数は、現在の行とは異なる行の値に基づいて値を割り当てます。一般的なナビゲーション関数には、FIRST_VALUELAST_VALUELEADLAG などがあります。

SELECT id,       pickup_datetime,       distance,       LAG(distance) OVER(ORDER BY pickup_datetime) AS prev_distance,       LEAD(distance) OVER(ORDER BY pickup_datetime) AS next_distanceFROM taxiORDER BY pickup_datetime;
Lag returns the value of the preceding row. Image by Author.
Lead returns the value of the subsequent row. Image by Author.

上記の例では、LAG 関数を使用して前の行の値を返し、LEAD 関数を使用して次の行の値を返しています。最初の行の lag 列は null で、最後の行の lead 列も null であることに注意してください。

SELECT id,       pickup_datetime,       distance,       LAG(distance, 2) OVER(ORDER BY pickup_datetime) AS prev_distance,       LEAD(distance, 2) OVER(ORDER BY pickup_datetime) AS next_distanceFROM taxiORDER BY pickup_datetime;
The first two rows are null when lag offset is set to 2. Image by Author.
The last two rows are null when lead offset is set to 2. Image by Author.

同様のノートで、LEAD および LAG 関数のオフセットも設定することができます。オフセットが2に設定された場合、lag 列の最初の2行が null であり、lead 列の最後の2行も null であることがわかります。

このブログ投稿がCommon Table Expression(CTE)とWindow Functionsの概念を紹介するのに役立ったことを願っています。

要約すると、CTEは一時的なテーブルまたはクエリ内のクエリです。これらはクエリをより読みやすいチャンクに分割するために使用され、定義されたCTEに対して新しいクエリを書くことができます。一方、Window Functionsは行のグループに対して集計を実行し、元のテーブルの各行の結果を返します。

これらのテクニックを向上させたい場合は、仕事で、面接問題を解決するとき、またはランダムなデータセットで遊ぶときにSQLクエリに実装することを強くお勧めします。練習すれば完璧になりますよね?

以下のリンクを使用してVoAGIメンバーシップに登録することで、私や他の素晴らしいライターをサポートしてください。幸せな学びを!

VoAGIメンバーとして、メンバーシップ料金の一部は読んだライターに支払われ、すべてのストーリーに完全アクセスできます...

chongjason.medium.com

次に何を読むか分からないですか?以下はいくつかのおすすめです。

データアナリストが知っておく必要のある最も重要なSQLコマンド10選

データベースからデータをクエリすることは複雑である必要はありません

towardsdatascience.com

正規表現の例を使ってわかりやすく説明します

文字列を扱う際に、どのデータアナリストにも必要な、最も過小評価されているスキルの一つ

towardsdatascience.com

データサイエンスプロジェクトを成功させるか失敗させるかの共通の問題

データの問題を見つけるための役立つガイド、それらがどれほど有害であるか、そして適切に対処する方法

towardsdatascience.com

We will continue to update VoAGI; if you have any questions or suggestions, please contact us!

Share:

Was this article helpful?

93 out of 132 found this helpful

Discover more