web-dev-qa-db-ja.com

SELECT n ONの方法はありますか(DISTINCT ONのようですが、それぞれ複数あります)

次のようなテーブルus_customersがあります(数十万行あります)。

+----------+----------+
|    id    | us_state |
+----------+----------+
| 12345678 | MA       |
| 23456781 | AL       |
| 34567812 | GA       |
| 45678123 | FL       |
| 56781234 | AZ       |
| 67812345 | MA       |
| 78123456 | CO       |
| 81234567 | FL       |
+----------+----------+

...そして、各us_stateからnの顧客のサンプルを選択したいと思います。

PostgreSQL 9.3でそれをきれいに行う方法はありますか?

us_stateから1人の顧客を簡単に取得できます。

SELECT DISTINCT ON (us_state) id
FROM us_customers
ORDER BY us_state;

しかし、たとえば、各州から3人の顧客が必要な場合、同じクエリを複数回実行せずにこれを実行できる方法はありますか?

4
Shaun Scovil

ROW_NUMBER()ウィンドウ関数を使用してus_stateによってidに番号を付けて順序付けでき、nの最初の値のみを保持します。

SELECT * 
FROM (
  SELECT *
    , ROW_NUMBER() OVER(PARTITION BY us_state ORDER BY id) as n
  FROM data
) as ord
WHERE n <= 2
ORDER BY us_state
;

または、サブクエリを使用してCROSS JOINを実行できます。

SELECT l.*
FROM (
  SELECT DISTINCT us_state FROM data
) as s
CROSS JOIN LATERAL (
  SELECT * 
  FROM data d
  WHERE d.us_state = s.us_state
  ORDER BY id
  LIMIT 2
) as l
ORDER BY l.us_state
;
  • サンプルSQL Fiddle here
  • 状態ごとに1〜3行の小さなサンプルを使用しました。したがって、2つの値のみに制限します
  • 私はidsで注文しましたが、それを変更して、最適な方法で注文できます。

私の小さなサンプルでの出力:

       id | us_state | n
      123 |       AL | 1 
      456 |       AL | 2 
 56781234 |       AZ | 1 
 78123456 |       CO | 1 
 45678123 |       FL | 1 
 81234567 |       FL | 2 
 34567812 |       GA | 1 
      123 |       MA | 1 
      456 |       MA | 2 

NはROW_NUMBERの結果であり、2番目のクエリには存在しないことに注意してください。大きなテーブルでは、パーティション(us-state)および順序(ここではid)列のインデックスが役立ちます。

使用したサンプル:

CREATE TABLE data
    ("id" int, "us_state" varchar(2))
;

INSERT INTO data
    ("id", "us_state")
VALUES
    (12345678, 'MA'),
    (123, 'MA'),
    (456, 'MA'),
    (23456781, 'AL'),
    (123, 'AL'),
    (456, 'AL'),
    (34567812, 'GA'),
    (45678123, 'FL'),
    (56781234, 'AZ'),
    (67812345, 'MA'),
    (78123456, 'CO'),
    (81234567, 'FL')
;
6

us_statesテーブル

us_stateの可能な値の完全なセットを含む別のテーブルがない場合は、すぐに作成することをお勧めします。

CREATE TABLE us_states (
  us_state varchar(2) PRIMARY KEY
  -- ... more columns?
);

1つは、us_customers.us_stateにFK制約を追加して、正当な値を適用することです。しかし、もっと重要な点として、かなり高速なクエリが可能です:

SELECT u.us_state, d.id  -- or more columns?
FROM   us_states u
LEFT   JOIN LATERAL (
   SELECT id             -- or more columns?
   FROM   us_customers
   WHERE  us_state = u.us_state
   LIMIT  3              -- three customers from each state
   ) d ON true
ORDER  BY u.us_state;

毎回DISTINCTを使用して「数十万行」から50個の異なる値を抽出することは、残りのクエリよりもコストがかかる可能性があり、時間の大きな無駄になります。

しないでくださいサブクエリにORDER BYを追加してください。選択する行を定義していないため、任意の選択で十分です。

LEFT JOINに一致する行が見つからない場合でも、us_customersに少なくとも1回はeveryの状態を含めます。

us_statesテーブルなし

us_statesテーブルがない場合、まだかなり高速です方法があります @ Julienによって提案された標準的な手法 よりも。 CTEでルーズインデックススキャンをエミュレートします。 (あなたは以下に概説するインデックスが必要です。):

WITH RECURSIVE us_states AS (
  (SELECT us_state FROM data ORDER BY us_state LIMIT 1)  -- parentheses required

   UNION ALL
   SELECT (SELECT us_state FROM data WHERE us_state > u.us_state
           ORDER BY us_state LIMIT 1)                    -- correlated subquery
   FROM   us_states u
   WHERE  u.us_state IS NOT NULL
   )
SELECT us_state FROM us_states
WHERE  us_state IS NOT NULL;

このクエリは、不足しているus_statesテーブルのドロップイン置換であるか、または不足しているテーブルの作成に使用できます。

WITH RECURSIVE us_states AS (
  (SELECT us_state FROM us_customers ORDER BY us_state LIMIT 1)

   UNION ALL
   SELECT (SELECT us_state FROM us_customers WHERE us_state > c.us_state
           ORDER BY us_state LIMIT 1)
   FROM   us_states c
   WHERE  c.us_state IS NOT NULL
   )    
SELECT u.us_state, d.id          -- or more columns?
FROM   us_states u
CROSS  JOIN LATERAL (
   SELECT id                     -- or more columns?
   FROM   us_customers
   WHERE  us_state = u.us_state  -- eliminates NULL value from CTE
   LIMIT  3                      -- three customers from each state
   ) d
ORDER  BY u.us_state;

今回はCROSS JOINを使用します。これは、CTEが既存の値を検出しただけであり、WHERE us_state IS NOT NULLをこの方法で追加する必要がないためです。

または、可能であれば保証us_stateごとに少なくとも3行あり、これは非常に高速です

WITH RECURSIVE cte AS (
  (SELECT us_state, id FROM us_customers3 ORDER BY us_state LIMIT 3)

   UNION ALL
   SELECT u.us_state, id
   FROM  (SELECT us_state FROM cte LIMIT 1) c
   ,      LATERAL (
      SELECT us_state, id
      FROM   us_customers3
      WHERE  us_state > c.us_state
      ORDER  BY us_state
      LIMIT  3
      ) u
   )
TABLE cte
ORDER BY us_state;

状態が3行未満の場合、最後のクエリは失敗します。

インデックス

いずれにせよ、このような複数列のインデックスを作成してください!

CREATE INDEX data_covering_idx ON data (us_state, id);

SQLフィドル


関連、詳細な説明

2