🦔

sqlfmtによって何を解決したいのか

2023/05/13に公開2

これは何

データ基盤の開発にsqlfmtを導入することについて考えてみたものです。
(チームにsqlfmtを導入するために書いてるものになります)

https://github.com/tconbeer/sqlfmt

sqlfmtによってどのような課題を解決したいのか

大きくはこれに集約されるかなと思います。
ではスタイル周りにある開発者生産性を阻害する要因とはどのようなものかというと:

  1. 読みづらいSQLによるバグの発見の遅れ
  2. ロジック周りには関係のない箇所のレビューをする必要性
  3. SQLスタイルのスタンスの違いによる衝突

というものがあります。
これをsqlfmtならどう解決できるかを紹介します。

sqlfmtならどう解決できるか

読みづらいSQLによるバグの発見の遅れ

これはそもそもformatterを導入していないことにより生じるものを想定しています。
やたらと長い一行、スペースの無い濃密な一行、揃わないインデント、無意味な改行...などにより、不用意に認知負荷が増大してしまったクエリは、そのクエリを理解するために脳のリソースを使ってしまいバグの発見の遅れに繋がってしまいます。
sqlfmtは意味のあるスペースと改行によって不必要な認知負荷を下げる手助けをしてくれます。
(「スペースと改行が消費するコンピューティングリソースは安いが、それが無いことで消費される人間の脳のリソースは高い」という言葉が何らかしらのdocsにあって気に入ってます)
sqlfmtのトップページでunformatted queryをformatしてくれる例を引用すると以下のようなbefore/afterになります。
どこからどこの括弧が何を表しているのかの見通しがよくなっていたり、予約語のインデントが揃っている様子が見て取れます。

before

with source as (select * from {{ source('my_application', 'users') }}),
  renamed as ( select
      --ids
      id, NULLIF(xid,'') as xid,

      --dates
      created_on, updated_on,

      NULLIF(email,'') as email,
      
      -- names
      NULLIF(full_name,'') as full_name,
      NULLIF(trim(case when regexp_count(nullif(full_name,''), ' ') = 0 then nullif(full_name,'')
        when regexp_count(nullif(full_name,''), ' ') = 1 then split_part(nullif(full_name,''), ' ', 1)
        else regexp_substr(nullif(full_name,''), '.* .* ') -- this user has more than two names!
        end
      ), 'TEST_USER') as first_name,
      nullif(split_part(nullif(full_name,''), ' ', greatest(2, regexp_count(nullif(full_name,''), ' ')+1)),'') as last_name

    FROM
    source

    where nvl(is_deleted, false) is false and id <> 123456 -- a very long comment about why we would exclude this user from this table that we will wrap

  )
select * from renamed

after

with
    source as (select * from {{ source("my_application", "users") }}),
    renamed as (
        select
            -- ids
            id,
            nullif(xid, '') as xid,

            -- dates
            created_on,
            updated_on,

            nullif(email, '') as email,

            -- names
            nullif(full_name, '') as full_name,
            nullif(
                trim(
                    case
                        when regexp_count(nullif(full_name, ''), ' ') = 0
                        then nullif(full_name, '')
                        when regexp_count(nullif(full_name, ''), ' ') = 1
                        then split_part(nullif(full_name, ''), ' ', 1)
                        else regexp_substr(nullif(full_name, ''), '.* .* ')  -- this user has more than two names!
                    end
                ),
                'TEST_USER'
            ) as first_name,
            nullif(
                split_part(
                    nullif(full_name, ''),
                    ' ',
                    greatest(2, regexp_count(nullif(full_name, ''), ' ') + 1)
                ),
                ''
            ) as last_name

        from source

        where nvl(is_deleted, false) is false and id <> 123456  -- a very long comment about why we would exclude this user from this table that we will wrap

    )
select *
from renamed

ロジック周りには関係のない箇所のレビューをする必要性

上の例で示したbeforeの状態でレビューを依頼された場合、どうしても「インデント揃えましょう」とか「予約語の大文字小文字は統一しましょう」などレビューする必要があります。(そのまま放置するとバグの発見の遅れに繋がってしまうので...)
このようなレビューは必要なものではありますが、レビュアーにとってもレビュイーにとっても気持ちの良いものではありません。お互いに本当はもっと深いところで議論をしたいはずです。
formatterはこのような不必要なモヤモヤを取り払ってくれます。
エディターと組み合わせて保存時にsqlfmtを実行するようにすれば、待ち時間もなく、CIを入れるまでもなくほぼノーコストでこれが実現されます。

SQLスタイルのスタンスの違いによる衝突

これはsqlfluffではなくsqlfmtを使ってformatすることの大きい理由の一つです。
人それぞれがそれぞれのSQL開発のバックボーンを持っているので、それぞれのスタイル流儀を持っていると思います。これはSQLに対する習熟度が高い人たちのチームだと尚更だと思います。(御多分に洩れず私自身もこのようなブログを書くほどです)
パッと思いつくだけでも

  • 予約語大文字、小文字
  • インデントはスペース2つ,4つ
  • select句のカンマは行頭、行末

などなどあります。
これらのコンセンサスをとって、sqlfluffのルールに入れていくのはとても大変です。sqlfluffではルールを細かく設定できるのでいろんなルールに対応できますが、それを決め切るのは骨の折れる作業です。
一方sqlfmtには「一行の文字数の上限」しか設定できる項目がありません。これは議論するポイントが1つだけということを表しています。
そしてPythonでblackを使ってた方にとっては馴染み深い思想なのかなと思います。
一旦このスタイルに慣れてしまえば、sqlfmtを使うどのチームでもスムーズにSQLを読むことができ、コラボレーションを阻む要因が一つ撲滅できます。

The sqlfmt Styleには「なぜ小文字にするべきか」などが書いてありますが、「小文字に揃える!カンマは行末に揃える!」ということよりも、「スタイルのコンセンサスを喧喧諤諤と揃え続けなくてよくなる」ことの方が重要だと思うので、そこに留意して読んでみてください。
個人的にとても共感しているdbtのstyle guideとも齟齬がなくて私自身はとても気に入っています。

sqlfluffはいらなくなるか?

sqlfmtはsqlfluffに比べて速いし、dbtで使う際に認証情報も渡さなくていいので、sqlfmt最高、sqlfmtだけでいいじゃん!となるかというと、私はNoだと思います。
sqlfmtはあくまでformatterなので、スタイルに沿ったSQLにはしてくれますが、コード規約に従ったSQLにはしてくれません。
例えば(dbtのstyle guideを参考にしながら)

  • CTEsはjoinや変換を避けたimportする部分と、logicを担当する二つの部分に分けましょう
  • joinする際テーブルに別名をつけるのはやめましょう(特にcとかTみたいな一文字をつけるのは可読性が下がるのでやめましょう)

などを適用したい場合はsqlfmtだけだと担保できないので、sqlfluffも用途を絞って効果的に使っていくのがいいと思います。

Integrations

私はVS Codeとともに使っています。とても簡単に導入できました。
チームで使うためにはGithub Actionを用いたCIを準備しました、これもdocsに従って比較的簡単に準備できました。

サマリ

考えること減るからsqlfmt導入したい

Discussion

ぺい(pei0804)ぺい(pei0804)

面白い記事ありがとうございます。
私の考えもちょっと書いてみました。

私は、sqlfluffとsqlfmt、両方を利用したことがありますが、現在では、sqlfluffをやめて、sqlfmt一本になりました。

理由は、tenajimaさんが書かれてることと同様で、sqlfmtは考えることが少ないことと、fmtだけで必要十分だったためです。
また高速に動作するため、時間の節約にもなりました。(2分30秒から3秒くらいになった)

sqlfluffは、地味にdbt macroとの相性が悪く、lintを通すためのテクが要求されたり、自動で修正されないルールとかを通すための編集が、必要になることがあります。
それぞれの問題は軽微でしたが、複数人の開発となると無視できないコストでした。

sqlfmtは、当記事にも書かれてる通り、設定する余地がほとんどなく、簡単に使えます。しかも、高速です。
最初は、sqlfluffに比べて調整できないことにモヤモヤした時もありましたが、一度sqlfmtでやってみたら、これで十分じゃんってなったのもあり、sqlfmtだけで運用を今日まで続けています。

tenajimatenajima

コメントありがとうございます!

sqlfluffは、地味にdbt macroとの相性が悪く、lintを通すためのテクが要求されたり、自動で修正されないルールとかを通すための編集が、必要になることがあります。
それぞれの問題は軽微でしたが、複数人の開発となると無視できないコストでした。

両方使ったことある意見とても貴重です、ありがとうございます。
生産性を阻害する要因の撲滅という意味ではsqlfluffが新たな阻害要因となってしまうのは本末転倒だなと感じました。sqlfluffを導入することが目的とならないよう考える必要あるなと考えさせられました。