SQLの整形ツール

SQLを見やすく整形するためのPerlスクリプトを作ってみた

SQLのチューニングをする場合,そのSQLはプログラムの実行ログから採取する場合が多い.

その場合,チューニングするためには,まずSQLを見やすく整形しないと始まらないのだが,
この作業は結構疲れてしまうし,往々にしてチューニング対象のSQLは長いので困る.

楽をするためにフリーの整形ツールをいくつか試してみたが,どうも気に入らない.まぁ好みの問題が大きいんでしょうけど...

それで仕方がないので,自分で整形ツールを作ることにした.

本格的にやると大変なので,少々使えるレベルものを,Perlでちょろっと書いてみた.

#!/usr/bin/perl

use strict;

my $in = '';
while (<STDIN>) {
  $in .= $_;
}
print sqlformat($in);

sub sqlformat {
  my($in) = @_;

  # 前の行につなげる単語
  my(@line_continue) = qw(
DISTINCT OF
);

  # 新しい行を開始する単語
  my(@line_init) = qw(
SELECT FROM LEFT RIGHT INNER FULL CROSS WHERE
GROUP HAVING ORDER UNION SET VALUES
INSERT DELETE UPDATE
OFFSET LIMIT
ON
CASE WHEN ELSE END
FOR
);

  # インデントをクリアする単語
  my(@indent_init) = qw(
SELECT FROM LEFT RIGHT INNER FULL CROSS WHERE
GROUP HAVING ORDER UNION SET VALUES
INSERT UPDATE DELETE
OFFSET LIMIT
FOR
);

  # 現在行を終了する単語
  my(@line_terminate) = qw(
SELECT DISTINCT INSERT UPDATE DELETE BY FROM
WHERE
AND OR
);

  # 次の行のインデントを増やす単語
  my(@indent_plus) = qw(
SELECT FROM LEFT RIGHT INNER FULL CROSS WHERE
GROUP HAVING ORDER UNION SET VALUES
INSERT UPDATE DELETE
OFFSET LIMIT
ON
CASE
);

  # 次の行のインデントを減らす単語
  my(@indent_minus) = qw(
END
);

  # 大文字にする単語
  my(@capitalize) = qw(
SELECT FROM LEFT RIGHT INNER FULL CROSS WHERE
GROUP HAVING ORDER UNION SET VALUES
INSERT UPDATE DELETE
OFFSET LIMIT
ON BY
CASE WHEN ELSE END AND OR
DISTINCT FOR OF IN EXISTS
AS JOIN THEN ASC DESC
);

  # 改行コードの統一
  $in =~ s/\r\n/\n/g;

  # 空白の統一
  $in =~ s/\t/ /g;
  $in =~ s/ +/ /g;

  # カンマの後に空白を入れて見やすくする
  $in =~ s/,([^ ])/, $1/g;

  # キーワードとカッコがつながっている場合は分離する
  foreach (@capitalize) {
    $in =~ s/\)($_)/\) $1/gi;
    $in =~ s/\(($_)/\( $1/gi;
    $in =~ s/($_)\(/$1 \(/gi;
    $in =~ s/($_)\)/$1 \)/gi;
  }

  my ($out) = '';
  my ($nest_level) = 0;
  my ($indent) = 0;
  my ($newline) = 1;

  foreach (split(/\n/, $in)) {
    for my $word (split) {
      if (scalar(grep {uc($_) eq uc($word)} @line_continue) > 0) {
        if (substr ($out, length($out) - 1) eq "\n") {
          $out = substr ($out, 0, length($out) - 1);
        }

      } elsif (scalar(grep {uc($_) eq uc($word)} @line_init) > 0) {
        $out .= "\n" if (! $newline);
        $newline = 1;
      }

      if (scalar(grep {uc($_) eq uc($word)} @indent_init) > 0) {
        $indent = 0;
      }

      if ($newline) {
        $out .= indent_string($nest_level + $indent);
        $newline = 0;

      } else {
        $out .= " ";
      }

      if (scalar(grep {uc($_) eq uc($word)} @capitalize) > 0) {
        $out .= uc($word);

      } else {
        $out .= $word;
      }

      while ($word =~ /\(/g) {
        $nest_level++;
      }
      while ($word =~ /\)/g) {
        $nest_level--;
      }

      if ($word =~ /,$/ || scalar(grep {uc($_) eq uc($word)} @line_terminate) > 0) {
        $out .= "\n";
        $newline = 1;
      }

      if (scalar(grep {uc($_) eq uc($word)} @indent_plus) > 0) {
        $indent++;

      } elsif (scalar(grep {uc($_) eq uc($word)} @indent_minus) > 0) {
        $indent--;
      }
    }
  }
  return $out;
}

sub indent_string {
  my($i) = @_;

  my $ind = '';
  for (1..$i) {
    $ind .= "\t";
  }
  return $ind;
}

俺的には,SQLの整形とは大まかには
-どこで改行して
-どれだけ字下げするか

の問題だと思っていて,以下の考え方で作っている.
-字下げの量 = ネストレベル + インデント
-ネストレベルは,「(」があると深くなり,「)」があると浅くなる.(数式括弧,サブクエリーへの対応)
-インデントは,特定のキーワードの影響を受ける.(例えば「SELECT」があると次の行からの列リストは字下げするとか)

まぁ,スクリプトの見たまんまです.

対応できていない点はあるのですが,今のところあまり深入りする気は無い.

Comment