SQLメモ

テストもかねて「SQLパズル」を解いていたときのメモを残しておきます。

実行環境はPostgreSQL13

二人組を作る

CREATE TABLE Nums(num INTEGER NOT NULL);
INSERT INTO Nums VALUES(2);
INSERT INTO Nums VALUES(3);
INSERT INTO Nums VALUES(5);
INSERT INTO Nums VALUES(5);
INSERT INTO Nums VALUES(8);
INSERT INTO Nums VALUES(9);

上のようなテーブルに対して2行ずつをペアにして表示したい

  • 自己結合+集約
SELECT N1.num AS num1, MIN(N2.num) AS num2
  FROM (SELECT num
              ,ROW_NUMBER() OVER(ORDER BY num) AS rn
          FROM Nums) AS N1
       INNER JOIN 
       (SELECT num
              ,ROW_NUMBER() OVER(ORDER BY num) AS rn
          FROM Nums) AS N2
       ON N1.rn % 2 = 1
       AND N1.rn < N2.rn
 GROUP BY N1.num;
  • Window関数でグループ番号を振る
SELECT MIN(num) AS num1, MAX(num) AS num2
  FROM (SELECT num
              ,(ROW_NUMBER() OVER(ORDER BY nums) + 1) / 2  AS pair_num
          FROM Nums) AS TMP
 GROUP BY pair_num
 ORDER BY num1;

3列以上に拡張したい場合はこうする

SELECT MAX(CASE col WHEN 1 THEN num END) AS num1
      ,MAX(CASE col WHEN 2 THEN num END) AS num2
      ,MAX(CASE col WHEN 3 THEN num END) AS num3
  FROM (SELECT num
              ,(ROW_NUMBER() OVER(ORDER BY nums) - 1) / 3 + 1  AS row
              ,(ROW_NUMBER() OVER(ORDER BY nums) - 1) % 3 + 1  AS col
          FROM Nums) AS TMP
 GROUP BY row
 ORDER BY num1;
  • Window関数で次の行を見る
SELECT num AS num1, next_num AS num2
  FROM (SELECT num
              ,MAX(num) OVER(ORDER BY num ROWS BETWEEN 1 FOLLOWING AND 1 FOLLOWING) AS next_num
              ,ROW_NUMBER() OVER(ORDER BY nums) AS rn
          FROM Nums) AS TMP
 WHERE rn % 2 = 1;

二人組を作る(ぼっちもいるパターン)

CREATE TABLE SPAN(num INTEGER UNIQUE NOT NULL, start_or_end CHAR(2) NOT NULL);
INSERT INTO SPAN VALUES(2, 's');
INSERT INTO SPAN VALUES(3, 'e');
INSERT INTO SPAN VALUES(6, 's');
INSERT INTO SPAN VALUES(9, 'e');
INSERT INTO SPAN VALUES(11, 'se');
INSERT INTO SPAN VALUES(15, 's');
INSERT INTO SPAN VALUES(19, 'e');
INSERT INTO SPAN VALUES(20, 'se');
INSERT INTO SPAN VALUES(21, 'se');
INSERT INTO SPAN VALUES(22, 's');
INSERT INTO SPAN VALUES(25, 'e');
num | start_or_end
------------------
  2 | 's'
  3 | 'e'
  6 | 's'
  9 | 'e'
 11 | 'se'
 15 | 's'
 19 | 'e'
 20 | 'se'    
 21 | 'se'
 22 | 's'
 25 | 'e'

こんなテーブルがあったとして このテーブルから下の表をだしたい

start | end 
------+-----
    2 |   3
    6 |   9
   11 |  11
   15 |  19
   20 |  20
   21 |  21
   22 |  25 

始めかつ終わりなやつが1行であらわされているのがやりづらい numが重複を許さないため(11,'s')(11,'e')みたいに2行もちができない

  • 自己結合+集約
SELECT S1.num AS start, MIN(S2.num) AS end
  FROM Span AS S1
       INNER JOIN 
       Span AS S2
       ON CASE S1.start_or_end WHEN 'se'
                               THEN (S1.num = S2.num )
                               WHEN 's'
                               THEN (S2.start_or_end = 'e' AND S1.num < S2.num )
                               ELSE false
          END
 GROUP BY S1.num
 ORDER BY S1.num;
  • Window関数を使う
SELECT MIN(num) AS start, MAX(num) AS end
  FROM (SELECT num
              ,CEIL((SUM((CASE WHEN start_or_end ='se' THEN 2 ELSE 1 END)) OVER(ORDER BY num) + 1) / 2 ) AS pair_num
          FROM Span) AS TMP
 GROUP BY pair_num
 ORDER BY MIN(num);

最大と最小を除いた平均

SELECT AVG(CASE acct_balance WHEN max THEN NULL
                             WHEN min THEN NULL
                             ELSE acct_balance END)
  FROM (SELECT customer_id
             ,acct_balance
             ,MIN(acct_balance) OVER() AS min
             ,MAX(acct_balance) OVER() AS max
         FROM Customers) AS TMP;