web-dev-qa-db-ja.com

すべてが配列されているPostgreSQL

INを使用する場合の1つだけでなく、配列内のすべての要素を一致させる必要がある句を実現するための最も簡単で最速の方法は何ですか?結局のところ、それは mongodbの$ all のように動作するはずです。

Context_usersがconversation_idとuser_idの間の結合テーブルであるグループ会話について考えると、私は次のようなことを念頭に置いています。

WHERE (conversations_users.user_id ALL IN (1,2))

[〜#〜]更新[〜#〜]16.07.12

スキーマとケースに関する詳細情報の追加:

  1. 結合テーブルはかなり単純です。

                  Table "public.conversations_users"
         Column      |  Type   | Modifiers | Storage | Description 
    -----------------+---------+-----------+---------+-------------
     conversation_id | integer |           | plain   | 
     user_id         | integer |           | plain   | 
    
  2. 会話には多くのユーザーがいて、ユーザーは多くの会話に属しています。会話内のすべてのユーザーを見つけるために、私はこの結合テーブルを使用しています。

  3. 結局、私はRuby on Rails scopeで、参加者に応じて会話を見つけることができます。例:

    scope :between, ->(*users) {
      joins(:users).where('conversations_users.user_id all in (?)', users.map(&:id))
    }
    

[〜#〜]更新[〜#〜]23.07.12

私の質問は、人々の完全な一致を見つけることについてです。したがって:

(1,2,3)を照会すると、(1,2)間の会話は一致しません

25
pex

結合テーブルがグッドプラクティスに従い、一意の複合キーが定義されている、つまり行の重複を防ぐための制約があると仮定すると、次の簡単なクエリのようになります。

select conversation_id from conversations_users where user_id in (1, 2)
group by conversation_id having count(*) = 2

最後の数字の2は、user_idのリストの長さであることに注意することが重要です。 user_idリストの長さが変更された場合は、明らかに変更する必要があります。結合テーブルに重複が含まれていないと想定できない場合は、パフォーマンスをいくらか犠牲にして、「count(*)」を「count(distinctuser_id)」に変更してください。

このクエリは、指定されたすべてのユーザーを含むすべての会話を検索しますたとえ会話には追加のユーザーも含まれます。

exactly指定されたユーザーのセットとの会話のみが必要な場合、1つのアプローチは、以下のようにwhere句でネストされたサブクエリを使用することです。最初と最後の行は元のクエリと同じであり、真ん中の2行だけが新しいことに注意してください。

select conversation_id from conversations_users where user_id in (1, 2)
   and conversation_id not in
   (select conversation_id from conversations_users where user_id not in (1,2))
group by conversation_id having count(*) = 2

同様に、データベースでサポートされている場合は、差セット演算子を使用できます。これは、Oracle構文の例です。 (PostgresまたはDB2の場合、キーワード「マイナス」を「例外」に変更します。)

select conversation_id from conversations_users where user_id in (1, 2)
  group by conversation_id having count(*) = 2
minus
  select conversation_id from conversations_users where user_id not in (1,2)

優れたクエリオプティマイザshouldは、最後の2つのバリエーションを同じように扱いますが、特定のデータベースで確認してください。たとえば、Oracle 11GR2クエリプランは、マイナス演算子を適用する前に2セットのカンバセーションIDを並べ替えますが、最後のクエリの並べ替え手順はスキップします。したがって、行数、コア、キャッシュ、インデックスなどの複数の要因に応じて、どちらのクエリプランも高速になる可能性があります。

29
Alex Blakemore

私はそれらのユーザーを配列にまとめています。また、これを読みやすくするためにCTE(WITH句にあるもの)を使用しています。

=> select * from conversations_users ;
 conversation_id | user_id
-----------------+---------
               1 |       1
               1 |       2
               2 |       1
               2 |       3
               3 |       1
               3 |       2
(6 rows)       

=> WITH users_on_conversation AS (
  SELECT conversation_id, array_agg(user_id) as users
  FROM conversations_users
  WHERE user_id in (1, 2) --filter here for performance                                                                                      
  GROUP BY conversation_id
)
SELECT * FROM users_on_conversation
WHERE users @> array[1, 2];
 conversation_id | users
-----------------+-------
               1 | {1,2}
               3 | {1,2}
(2 rows) 

[〜#〜]編集[〜#〜](一部のリソース)

7
hgmnz

INcount()を使用した@Alexの回答はおそらく最も簡単な解決策ですが、このPL/pgSQL関数の方が高速であると思います。

CREATE OR REPLACE FUNCTION f_conversations_among_users(_user_arr int[])
  RETURNS SETOF conversations AS
$BODY$
DECLARE
    _sql text := '
    SELECT c.*
    FROM   conversations c';
    i int;
BEGIN

FOREACH i IN ARRAY _user_arr LOOP
    _sql  := _sql  || '
    JOIN   conversations_users x' || i || ' USING (conversation_id)';
END LOOP;

_sql  := _sql  || '
    WHERE  TRUE';

FOREACH i IN ARRAY _user_arr LOOP
    _sql  := _sql  || '
    AND    x' || i || '.user_id = ' || i;
END LOOP;

/* uncomment for conversations with exact list of users and no more
_sql  := _sql  || '
    AND    NOT EXISTS (
        SELECT 1
        FROM   conversations_users u
        WHERE  u.conversation_id = c.conversation_id
        AND    u.user_id <> ALL (_user_arr)
        )
*/

-- RAISE NOTICE '%', _sql;
RETURN QUERY EXECUTE _sql;

END;
$BODY$ LANGUAGE plpgsql VOLATILE;

コール:

SELECT * FROM f_conversations_among_users('{1,2}')

関数は動的にビルドし、次の形式のクエリを実行します。

SELECT c.*
FROM   conversations c
JOIN   conversations_users x1 USING (conversation_id)
JOIN   conversations_users x2 USING (conversation_id)
...
WHERE  TRUE
AND    x1.user_id = 1
AND    x2.user_id = 2
...

このフォームは、 リレーショナル除算のクエリの広範なテスト で最高のパフォーマンスを発揮しました。

アプリでクエリを作成することもできますが、1つの配列パラメーターを使用することを前提としています。また、これはとにかくおそらく最速です。

どちらのクエリでも、高速であるためには、次のようなindexが必要です。

CREATE INDEX conversations_users_user_id_idx ON conversations_users (user_id);

(user_id, conversation_id)の複数列の主(または一意)キーも同様ですが、(conversation_id, user_id)の1つ(非常によくあるように!)は劣ります。上記のリンクで短い理論的根拠を見つけるか、 dba.SEのこの関連する質問の下での包括的な評価

また、conversations.conversation_idに主キーがあると仮定します。

@Alexのクエリとこの関数でEXPLAIN ANALYZEを使用してパフォーマンステストを実行し、結果を報告できますか?

どちらのソリューションも、配列内の少なくともユーザーが参加する会話を見つけることに注意してください-追加のユーザーとの会話も含まれます。
これらを除外する場合は、関数の追加句のコメントを解除します(または他のクエリに追加します)。

関数の機能についてさらに説明が必要な場合は教えてください。

3

これにより、ActiveRecordオブジェクトが保持されます。

以下の例では、配列内のすべてのコードに関連付けられているタイムシートを知りたいと思います。

_codes = [8,9]

Timesheet.joins(:codes).select('count(*) as count, timesheets.*').
           where('codes.id': codes).
           group('timesheets.id').
           having('count(*) = ?', codes.length)
_

使用する完全なActiveRecordオブジェクトが必要です。真のスコープにしたい場合は、上記の例を使用して、.pluck(:id)で結果を渡すことができます。

2
Don Pflaster

可能なすべての値を使用してマッピングテーブルを作成し、これを使用します

select 
    t1.col from conversations_users as t1 
    inner join mapping_table as map on t1.user_id=map.user_id
group by 
    t1.col  
having  
    count(distinct conversations_users.user_id)=
    (select count(distinct user_id) from mapping)
1
Madhivanan
select id from conversations where not exists(
    select * from conversations_users cu 
    where cu.conversation_id=conversations.id 
    and cu.user_id not in(1,2,3)        
)

これは簡単にRailsスコープにすることができます。

1
maniek

一時テーブルをいじり始めたくないのではないかと思います。

正確にユーザーのセットとの会話が必要なのか、スーパーセットとの会話が必要なのかについての質問は不明確でした。以下はスーパーセット用です。

with users as (select user_id from users where user_id in (<list>)
              ),
     conv  as (select conversation_id, user_id
               from conversations_users
               where user_id in (<list>)
              )
select distinct conversation_id
from users u left outer join
     conv c
     on u.user_id = c.user_id
where c.conversation_id is not null

このクエリが適切に機能するためには、usersとconversations_usersの両方にuser_idのインデックスがあることを前提としています。

正確なセットについて。 。 。

with users as (select user_id from users where user_id in (<list>)
              ),
     conv  as (select conversation_id, user_id
               from conversations_users
               where user_id in (<list>)
              )
select distinct conversation_id
from users u full outer join
     conv c
     on u.user_id = c.user_id
where c.conversation_id is not null and u.user_id is not null
1
Gordon Linoff

@Alex Blakemoreの回答に基づくと、同等のRails 4スコープのConversationクラスは次のようになります。

_# Conversations exactly with users array
scope :by_users, -> (users) { 
                           self.by_any_of_users(users)
                             .group("conversations.id")
                             .having("COUNT(*) = ?", users.length) -
                           joins(:conversations_users)
                             .where("conversations_users.user_id NOT IN (?)", users)
}
# generates an IN clause
scope :by_any_of_users, -> (users) { joins(:conversations_users).where(conversations_users: { user_id: users }).distinct }
_

Rails _-_(マイナス)を実行する代わりに最適化できることに注意してください。.where("NOT IN")を実行できますが、それを読むのは非常に複雑です。

1
Bruno Peres

アレックスブレイクモアの回答に基づく

select conversation_id
from conversations_users cu
where user_id in (1, 2)
group by conversation_id 
having count(distinct user_id) = 2

User_1とuser_2を含む会話のconversation_idを見つけるという、同じ目標を持つ代替クエリを見つけました(追加のユーザーを無視します)

select *
from conversations_users cu1
where 2 = (
    select count(distinct user_id)
    from conversations_users cu2
    where user_id in (1, 2) and cu1.conversation_id = cu2.conversation_id
)

Postgresがexplainクエリステートメントを介して実行する分析によると、速度は遅くなります。少なくとも、conversations_usersの各行について、サブクエリが相関しているために実行される条件が多いため、これは正しいと思います。サブクエリ。このクエリの良い点は、グループ化していないため、conversations_usersテーブルの追加フィールドを選択できることです。状況によっては(私のように)便利かもしれません。

0
Victor