15
9

概要

業務中に出会ったSQLやSQLのTipsを、復習がてらまとめていく記事です。
知ってる知ってる!とか、こんなのあったんだー、など共感や発見があれば嬉しい限りです。

・動作確認環境( select version(); 実行結果)
PostgreSQL 14.5 (Debian 14.5-2.pgdg110+2) on x86_64-pc-linux-gnu, compiled by gcc (Debian 10.2.1-6) 10.2.1 20210110, 64-bit

NULLIF

説明

・AとBが同じであれば、NULLを返します
・AとBが異なる場合、Aを返します

NULLIF(A, B)

実行例

・NULLIFの挙動を簡単に確認する例です

-- 実行結果(AとBが異なるのでAを返す): 1
select nullif(1,3);
-- 実行結果(AとBが同じなのでNULLを返す): NULL
select nullif(3,3);
-- 実行結果(AとBが異なるのでAを返す): NULL
select nullif(null,3);
-- 実行結果(AとBが異なるのでAを返す): 3
select nullif(3,null);
-- 実行結果(AとBが同じなのでNULLを返す): NULL
select nullif(null,null);

利用シーン

実際の案件で見た利用シーンとしては、「0で割るとエラーになってしまう状況を回避したい」といった時に利用されていました
以下に理由と、実際に回避する例を記載します

・1. 以下はSQL実行時に「ERROR: division by zero」エラーになります

select 1 / 0;

・2. ただし、NULLで割ったり、0で割らなければエラーにはなりません

select 0 / null; -- 結果はNULL
select 1 / null; -- 結果はNULL
select null / 1; -- 結果はNULL
select null / 100; -- 結果はNULL

・3. 2の性質「0で割らなければエラーにならない」を利用して、IFNULLを使い0除算行わないようにしつつ客単価を取得する例を示します

with uriage_2023 as (
    -- 2023/04分の売上合計(sum_sales_amount)と、当月購入者数合計(number_of_purchasers)
    select '2023/04' AS target_month, 150000 AS sum_sales_amount, 15 AS number_of_purchasers
    union all
    -- 2023/05分の売上合計(sum_sales_amount)と、当月購入者数合計(number_of_purchasers)
    select '2023/05' AS target_month, 0 AS sum_sales_amount, 0 AS number_of_purchasers
    union all
    -- 2023/06分の売上合計(sum_sales_amount)と、当月購入者数合計(number_of_purchasers)
    select '2023/06' AS target_month, 300000 AS sum_sales_amount, 30 AS number_of_purchasers
)
select
       target_month AS "集計対象月",
       sum_sales_amount || '円' AS "当月売上合計金額",
       number_of_purchasers || '人' AS "当月購入者数合計",
       COALESCE(
            (sum_sales_amount / nullif(number_of_purchasers, 0)), 0
       ) || '円' AS "客単価"
from uriage_2023
;

・実行結果

集計対象月 当月売上合計金額 当月購入者数合計 客単価
2023/04 150000円 15人 10000円
2023/05 0円 0人 0円
2023/06 300000円 30人 10000円

※to_charを使って、金額をカンマ区切りにして表示することも可能です
※COALESCEが不明な場合は、後述していますので是非参考にしてください

※with句では以下のような集計結果を取得しています

target_month sum_sales_amount number_of_purchasers
2023/04 150000 15
2023/05 0 0
2023/06 300000 30

COALESCE

説明

・COALESCE(A, B, C...) 与えられた引数の中で最初に見つけたNULLではない値を返します

実行例

・COALESCEの挙動を簡単に確認する例です

-- 実行結果(最初に見つかるNULLではない引数が返される): 0
select COALESCE(null, 0);
-- 実行結果(最初に見つかるNULLではない引数が返される): 999
select COALESCE(999, 0);
-- 実行結果(最初に見つかるNULLではない引数が返される): 999
select COALESCE(null, 999, 0);

TRANSLATEとREPLACE

説明

・TRANSLATE(置換対象, 'ABC', 'DEF')
  AをDに、BをEに、CをFに1文字ごとに置換します

・REPLACE(置換対象, 'ABC', 'DEF')
  ABCをDEFに置換します

実行例

・TRANSLATEの挙動を簡単に確認する例です
・TRANSLATEは1文字ずつ置換する点に留意しましょう

with prefectures AS (
    select '秋田県' AS "prefecture"
    UNION ALL
    select '福井県' AS "prefecture"
    UNION ALL
    select '東京都' AS "prefecture"
)
select
       prefecture,
       -- 「秋」を「春」に置換する
       replace(prefecture, '秋', '春') AS "REPLACE置換例1",
       -- 「秋福」という文字列を探して、「春福」に置換する(※該当箇所無しのため置換は行われない)
       replace(prefecture, '秋福', '春服') AS "REPLACE置換例2",
       -- 「秋」は「春」に、「福」は「服」に。「東」は「西」に1文字ごとに置換される
       translate(prefecture, '秋福東', '春服西') AS "TRANSLATEの置換例1",
       -- 「東」に対応する置換後文字が無い場合は、「東」は置換後に空文字となる
       translate(prefecture, '秋福東', '春服') AS "TRANSLATEの置換例2"
from prefectures;

CASEで集計

前提

・テーブル名はhtk_usersとし、以下データがある前提でCASEを利用して集計する例です

id account_number name sex postcode prefecture city street room_number
1 00001 Sin 1 0100834 秋田県 秋田市旭川南町 1-1-1 XXX101号室
2 00002 Alice 2 0100834 秋田県 秋田市旭川南町 1-1-2 XXX102号室
3 00003 Dave 1 0100834 秋田県 秋田市旭川南町 1-1-3 XXX103号室
4 00004 Calol 2 8120885 福岡県 福岡市博多区相生町 1-2-1 XXX201号室
5 00005 Yuji 1 8120885 福岡県 福岡市博多区相生町 1-2-2 XXX202号室
6 00006 Serena 2 8120885 福岡県 福岡市博多区相生町 1-2-3 XXX203号室
7 00007 Bob 1 9188187 福岡県 福岡市博多区相生町 1-2-4 XXX204号室
8 00008 Candy 2 9188187 福井県 福井市花守町 1-3-2 XXX302号室
9 00009 Mike 1 9188187 福井県 福井市花守町 1-3-3 XXX303号室
10 00010 Elice 2 1080072 東京都 港区白金1 XXXXX XXX999号室

集計例

・いわゆる縦でもっているたくさんのデータ(htk_usersは10件しかないですが...笑)を、集計して横に並べてみます
・以下の集計例は、都道府県別にユーザー数を算出する例です
※補足ですが、countにnullを渡すと0が返るので埼玉県のユーザー件数は0になっています

select
    count(*) AS "全ユーザー件数",
    count(case when prefecture = '秋田県' then 1 end) as "秋田県のユーザー件数",
    count(case when prefecture = '福岡県' then 1 end) as "福岡県のユーザー件数",
    count(case when prefecture = '福井県' then 1 end) as "福井県のユーザー件数",
    count(case when prefecture = '東京都' then 1 end) as "東京都のユーザー件数",
    count(case when prefecture = '埼玉県' then 1 end) as "埼玉県のユーザー件数"
from htk_users;

・実行結果

全ユーザー件数 秋田県のユーザー件数 福岡県のユーザー件数 福井県のユーザー件数 東京都のユーザー件数 埼玉県のユーザー件数
10 3 4 2 1 0

GROUP BYで分子を、ウィンドウ関数で分母を出して比率を求める

前提

・テーブル名はhtk_usersとし、以下データがある前提で集計例を出します

id account_number name sex postcode prefecture city street room_number
1 00001 Sin 1 0100834 秋田県 秋田市旭川南町 1-1-1 XXX101号室
2 00002 Alice 2 0100834 秋田県 秋田市旭川南町 1-1-2 XXX102号室
3 00003 Dave 1 0100834 秋田県 秋田市旭川南町 1-1-3 XXX103号室
4 00004 Calol 2 8120885 福岡県 福岡市博多区相生町 1-2-1 XXX201号室
5 00005 Yuji 1 8120885 福岡県 福岡市博多区相生町 1-2-2 XXX202号室
6 00006 Serena 2 8120885 福岡県 福岡市博多区相生町 1-2-3 XXX203号室
7 00007 Bob 1 9188187 福岡県 福岡市博多区相生町 1-2-4 XXX204号室
8 00008 Candy 2 9188187 福井県 福井市花守町 1-3-2 XXX302号室
9 00009 Mike 1 9188187 福井県 福井市花守町 1-3-3 XXX303号室
10 00010 Elice 2 1080072 東京都 港区白金1 XXXXX XXX999号室
11 00011 Silva 1 1080075 東京都 港区港南二丁目999番1号 999番1号 XXXビル 8F

集計例

・GROUP BY prefectureを行い、都道府県別のユーザー件数を出しつつ、全てのユーザー数の内各都道府県別にユーザー件数の比率を出してみます

select
    prefecture,
    -- A: group by prefecture による都道府県別のユーザー件数
    count(prefecture) AS "都道府県別ユーザー件数",
    -- B: 「group by prefecture による都道府県別のユーザー件数」の合算値をウィンドウ関数(OVER句)を利用して全数を各行で取得
    sum(count(id)) over() AS "全ユーザー件数",
    -- A÷B*100で、比率を出す
    round(count(prefecture) / (sum(count(prefecture)) over()) * 100, 2) || '%' AS "都道府県別の比率"
from htk_users
group by prefecture
order by prefecture
;

・実行結果

prefecture 都道府県別ユーザー件数 全ユーザー件数 都道府県別の比率
東京都 2 11 18.18%
福井県 2 11 18.18%
福岡県 4 11 36.36%
秋田県 3 11 27.27%

UPDATE時にNULLなら上書き、NULLじゃなかったら元々の文字列に改行を加えて追記

remarksの値がNULLであれば上書きを行い、NULLじゃなければ元々の文字列に改行を加えて追記する例になります
以下申込データ(テーブル名「entries」)があると想定します

id remarks
7436 担当者Aにより案内済
7614 NULL

元々remarksに値がある場合は、上書きしたくない状況だと仮定します
「remarksがNULLであればsetで指定した値で上書きを行い、NULLではない場合は、元々の値に改行を加えてから追記したい」ような状況だとすると、下記のように実現できます
※chr(10)は改行で、||は文字列の結合です

update entries
set remarks = (
    case
        when remarks is null then 'XXXのため直接データ修正いたしました。'
        else remarks || chr(10) || 'XXXのため直接データ修正いたしました。'
    end
)
where id in (7614, 7436);

・UPDATE実行後

id remarks
7436 担当者Aにより案内済
XXXのため直接データ修正いたしました。
7614 XXXのため直接データ修正いたしました。

バルクインサート

説明

・普通のインサートでは、えいさ、ほいさ、えいさ、ほいさとデータを作成しますが、バルクインサートは一括でズドン!とデータを作成するイメージです
・作成するデータ量が多ければ、バルクインサートの方が早いし効率が良いイメージを持ってもらえれば大丈夫です

書式

insert into SAMPLE_TABLE (column1, column2) 
values 
-- 追加分1行目
('column1の値', 'column2の値'),
-- 追加分2行目
('column1の値', 'column2の値'),
-- 追加分3行目
('column1の値', 'column2の値') 
...
;

実行例

insert into htk_users (id, account_number, name, sex, postcode, prefecture, city, street, room_number)
values
(1, '00001', 'Sin', 1, '0100834', '秋田県', '秋田市旭川南町', '1-1-1', 'XXX101号室'),
(2, '00002', 'Alice', 2, '0100834', '秋田県', '秋田市旭川南町', '1-1-2', 'XXX102号室'),
(3, '00003', 'Dave', 1, '0100834', '秋田県', '秋田市旭川南町', '1-1-3', 'XXX103号室'),
(4, '00004', 'Calol', 2, '8120885', '福岡県', '福岡市博多区相生町', '1-2-1', 'XXX201号室'),
(5, '00005', 'Yuji', 1, '8120885', '福岡県', '福岡市博多区相生町', '1-2-2', 'XXX202号室'),
(6, '00006', 'Serena', 2, '8120885', '福岡県', '福岡市博多区相生町', '1-2-3', 'XXX203号室'),
(7, '00007', 'Bob', 1, '9188187', '福岡県', '福岡市博多区相生町', '1-2-4', 'XXX204号室'),
(8, '00008', 'Candy', 2, '9188187', '福井県', '福井市花守町', '1-3-2', 'XXX302号室'),
(9, '00009', 'Mike', 1, '9188187', '福井県', '福井市花守町', '1-3-3', 'XXX303号室'),
(10, '00010', 'Elice', 2, '1080072', '東京都', '港区白金1', 'XXXXX', 'XXX999号室'),
(11, '00011', 'Silva', 1, '1080075', '東京都', '港区港南二丁目999番1号', '999番1号', 'XXXビル 8F')
;

truncateでデータを全消ししつつシーケンスリセット

説明と実行例

・restart identityをつけると、データを削除しつつシーケンスをリセットします
※データを削除する構文ですので、取り扱いにご注意ください

-- restart identity がなければシーケンスリセットは行われません
truncate table SAMPLE_TABLE restart identity;

※SAMPLE_TABLEの部分は、削除したいテーブル名を入れます

正規表現を使った抽出

書式

column ~ '正規表現':正規表現に一致するものを探す(大文字小文字の区別あり)
column !~ '正規表現':正規表現に一致しないものを探す(大文字小文字の区別あり)
※大文字小文字を区別したくない場合は、~の後に*をつけます

実行例

・以下は、先頭数値1桁の後にハイフンがくるような、変な電話番号のデータがないか確認した際の例です

select count(*)
from SAMPLE_TABLE
where tel ~ '^[0-9]{1}-';

string_agg

説明

・縦持ちのデータ(行データ)を、第二引数で指定した文字列区切りの文字列にまとめます

実行例

・GROUP BY無しの例です

with tmp_venues as (
    select '会場A' as name
    union all
    select '会場B' as name
    union all
    select '会場C' as name
)
select string_agg(tmp_venues.name, ',')
from tmp_venues
;

・実行結果

string_agg
会場A,会場B,会場C

・GROUP BY有りの例です

with tmp_venues as (
    select '会場A' as name, 1 as is_using
    union all
    select '会場B' as name, 1 as is_using
    union all
    select '会場C' as name, 1 as is_using
)
select string_agg(tmp_venues.name, ',')
from tmp_venues
group by tmp_venues.is_using
;

・実行結果

string_agg
会場A,会場B,会場C

・同じ会場名があり、distinctで重複削除する例です

with tmp_venues as (
    select '会場A' as name, 1 as is_using
    union all
    select '会場B' as name, 1 as is_using
    union all
    select '会場C' as name, 1 as is_using
    union all
    select '会場C' as name, 1 as is_using
)
select string_agg(distinct tmp_venues.name, ',')
from tmp_venues
group by tmp_venues.is_using
;

・実行結果

string_agg
会場A,会場B,会場C

string_agg(distinct tmp_venues.name, ',' ORDER BY tmp_venues.name DESC)のようにstring_agg内の並び順も制御可能です

array_agg

説明

・縦持ちのデータ(行データ)を、配列にまとめます

実行例

・3行の会場データを、array_aggにより1つの配列として取得する例です

with tmp_venues as (
    select '会場A' as name
    union all
    select '会場B' as name
    union all
    select '会場C' as name
)
select array_agg(name)
from tmp_venues
;

・実行結果

array_agg
{会場A,会場B,会場C}

※array_agg内で以下のようにORDER BYも利用できます

with tmp_venues as (
    select '会場A' as name
    union all
    select '会場B' as name
    union all
    select '会場C' as name
)
select array_agg(name order by name desc)
from tmp_venues
;

・実行結果

array_agg
{会場C,会場B,会場A}

unnest

説明

・配列を、縦持ちのデータ(行データ)に展開します

実行例

SELECT unnest(ARRAY['会場A','会場B','会場C']::VARCHAR(255)[]);

・実行結果

unnest
会場A
会場B
会場C

配列 @> 配列

postgresqlの配列型を触っているとこやつの便利さに助けられる場面がありました。
パフォーマンスチューニングの際も有用だったので、覚えておいて損はないものです。(と自分は感じてます笑)
語ると長くなるので、使い方だけさくっと記載します。

説明

・書式: 左辺の配列 @> 右辺の配列
・左辺の配列の要素に存在するものが、右辺の要素にあれば、trueを返します
・右辺が空配列の場合、trueを返します
・左辺に存在しない要素が右辺にあるとfalseを返します

実行例

・挙動をシンプルな例で伝えたいのでSELECT句で@>を利用していますが、HAVING句やWHERE句でも利用できます
'{1,2,3}'::int[]は配列型のリテラル表現であり、ARRAY[1,2,3]::int[]と同じです

-- =========================================
-- 左辺の配列 @> 右辺の配列
-- =>左辺の配列の要素に存在するものが、右辺の要素にあれば、true
-- =>右辺が空配列の場合、true
-- =>左辺に存在しない要素が右辺にあるとfalse
-- =========================================
-- true
select '{1,2,3}'::int[] @> '{3}'::int[];
-- 左辺の要素にない要素が右辺にあるので、false
select '{1,2,3}'::int[] @> '{4}'::int[];
-- 左辺の要素にない要素が右辺にあるので、false
select '{1,2,3}'::int[] @> '{2,4}'::int[];
-- true
select '{1,2,3}'::int[] @> '{2,3}'::int[];
-- true
select '{1,2,3}'::int[] @> '{}'::int[];

-- true
SELECT ARRAY['会場A','会場B','会場C'] @> ARRAY['会場B'];
-- 左辺の要素にない要素が右辺にあるので、false
SELECT ARRAY['会場A','会場B','会場C'] @> ARRAY['会場B','会場D'];
-- true
SELECT ARRAY['会場A','会場B','会場C'] @> ARRAY['会場A','会場B'];
-- true (右辺が空配列)
SELECT ARRAY['会場A','会場B','会場C']::text[] @> ARRAY[]::text[];

LPAD

説明

・書式: lpad(値, 全体で何桁にするか, 桁が足りない部分を左埋めする際に使う値)
・第一引数で渡した値を、第二引数で指定した桁数となるように、第三引数の値で左埋めします

実行例

以下は、'90001234'が10桁になるように、'0'で左埋めする例です

select lpad('90001234', 10, '0');

・実行結果

lpad
0090001234

ROUND

説明

・ROUND(X::NUMERIC)
  Xを四捨五入して、整数にします

・ROUND(X::NUMERIC, Z)
  Xを四捨五入して、小数点以下Z桁の数値にします

※小数点以下Z桁になるように0で桁を埋めません、またDOUBLE PRECISION型では動作が異なるので頭に留めておきましょう。NUMERICで使う分には普通に四捨五入してくれるので気にしなくて大丈夫です。

実行例

・ROUND(X::NUMERIC) ※Xはプラスの値

with round_results as (
    select null AS "ORIGINAL VALUE", round(null::NUMERIC) AS "round(X::NUMERIC)"
    UNION ALL
    select 1.4, round(1.4::numeric)
    UNION ALL
    select 1.5, round(1.5::numeric)
    UNION ALL
    select 1.6, round(1.6::numeric)
    UNION ALL
    select 2.4, round(2.4::numeric)
    UNION ALL
    select 2.5, round(2.5::numeric)
    UNION ALL
    select 2.6, round(2.6::numeric)
    UNION ALL
    select 3.4, round(3.4::numeric)
    UNION ALL
    select 3.5, round(3.5::numeric)
    UNION ALL
    select 3.6, round(3.6::numeric)
    UNION ALL
    select 4.4, round(4.4::numeric)
    UNION ALL
    select 4.5, round(4.5::numeric)
    UNION ALL
    select 4.6, round(4.6::numeric)
)
select * from round_results;

・実行結果

ORIGINAL VALUE round(X::NUMERIC)
NULL NULL
1.4 1
1.5 2
1.6 2
2.4 2
2.5 3
2.6 3
3.4 3
3.5 4
3.6 4
4.4 4
4.5 5
4.6 5

・ROUND(X::NUMERIC) ※Xはマイナスの値

with round_results as (
    select null AS "ORIGINAL VALUE", round(null::NUMERIC) AS "round(X::NUMERIC)"
    UNION ALL
    select -1.4, round(-1.4::numeric)
    UNION ALL
    select -1.5, round(-1.5::numeric)
    UNION ALL
    select -1.6, round(-1.6::numeric)
    UNION ALL
    select -2.4, round(-2.4::numeric)
    UNION ALL
    select -2.5, round(-2.5::numeric)
    UNION ALL
    select -2.6, round(-2.6::numeric)
    UNION ALL
    select -3.4, round(-3.4::numeric)
    UNION ALL
    select -3.5, round(-3.5::numeric)
    UNION ALL
    select -3.6, round(-3.6::numeric)
    UNION ALL
    select -4.4, round(-4.4::numeric)
    UNION ALL
    select -4.5, round(-4.5::numeric)
    UNION ALL
    select -4.6, round(-4.6::numeric)
)
select * from round_results;

・実行結果

ORIGINAL VALUE round(X::NUMERIC)
NULL NULL
-1.4 -1
-1.5 -2
-1.6 -2
-2.4 -2
-2.5 -3
-2.6 -3
-3.4 -3
-3.5 -4
-3.6 -4
-4.4 -4
-4.5 -5
-4.6 -5

・ROUND(X::NUMERIC, 2) ※Xはプラスの値

with round_results as (
    select null AS "ORIGINAL VALUE", round(null::NUMERIC, 2) AS "round(X::NUMERIC, 2)"
    UNION ALL
    select 1.444, round(1.444::numeric, 2)
    UNION ALL
    select 1.555, round(1.555::numeric, 2)
    UNION ALL
    select 1.666, round(1.666::numeric, 2)
    UNION ALL
    select 2.444, round(2.444::numeric, 2)
    UNION ALL
    select 2.555, round(2.555::numeric, 2)
    UNION ALL
    select 2.666, round(2.666::numeric, 2)
    UNION ALL
    select 3.444, round(3.444::numeric, 2)
    UNION ALL
    select 3.555, round(3.555::numeric, 2)
    UNION ALL
    select 3.666, round(3.666::numeric, 2)
    UNION ALL
    select 4.444, round(4.444::numeric, 2)
    UNION ALL
    select 4.555, round(4.555::numeric, 2)
    UNION ALL
    select 4.666, round(4.666::numeric, 2)
)
select * from round_results;

・実行結果

ORIGINAL VALUE round(X::NUMERIC, 2)
NULL NULL
1.444 1.44
1.555 1.56
1.666 1.67
2.444 2.44
2.555 2.56
2.666 2.67
3.444 3.44
3.555 3.56
3.666 3.67
4.444 4.44
4.555 4.56
4.666 4.67

・ROUND(X::NUMERIC, 2) ※Xはマイナスの値

with round_results as (
    select null AS "ORIGINAL VALUE", round(null::NUMERIC, 2) AS "round(X::NUMERIC, 2)"
    UNION ALL
    select -1.444, round(-1.444::numeric, 2)
    UNION ALL
    select -1.555, round(-1.555::numeric, 2)
    UNION ALL
    select -1.666, round(-1.666::numeric, 2)
    UNION ALL
    select -2.444, round(-2.444::numeric, 2)
    UNION ALL
    select -2.555, round(-2.555::numeric, 2)
    UNION ALL
    select -2.666, round(-2.666::numeric, 2)
    UNION ALL
    select -3.444, round(-3.444::numeric, 2)
    UNION ALL
    select -3.555, round(-3.555::numeric, 2)
    UNION ALL
    select -3.666, round(-3.666::numeric, 2)
    UNION ALL
    select -4.444, round(-4.444::numeric, 2)
    UNION ALL
    select -4.555, round(-4.555::numeric, 2)
    UNION ALL
    select -4.666, round(-4.666::numeric, 2)
)
select * from round_results;

・実行結果

ORIGINAL VALUE round(X::NUMERIC, 2)
NULL NULL
-1.444 -1.44
-1.555 -1.56
-1.666 -1.67
-2.444 -2.44
-2.555 -2.56
-2.666 -2.67
-3.444 -3.44
-3.555 -3.56
-3.666 -3.67
-4.444 -4.44
-4.555 -4.56
-4.666 -4.67

最後に

いろんなSQLがありますよねー、、日々勉強になります;

15
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
9