Как я могу запросить количество пересечений идентификаторов из двух разных таблиц соединения - PullRequest
0 голосов
/ 01 июля 2019

У меня есть несколько моделей. Пользователь, Трек, Плейлист, UserTrack и PlaylistTrack. Пользователь может иметь много дорожек, как и плейлист. Я хочу запросить плейлисты с большинством треков, которые соответствуют пользовательским трекам пользователя.

Вот как я это делаю сейчас:

user_tracks = # get all track_ids for a user in the users_tracks join table

playlists =
  from(p in Playlist,
    join: pt in PlaylistTrack,
    where: pt.playlist_id == p.id,
    having:
      fragment(
        "cardinality(array(
              select unnest(array_agg(?)::varchar[])
              intersect
              select unnest(?::varchar[])
            )) > 0",
        pt.track_id,
        ^user_tracks
      ),
    group_by: p.id,
    order_by:
      fragment(
        "cardinality(array(
              select unnest(array_agg(?)::varchar[])
              intersect
              select unnest(?::varchar[])
            )) DESC",
        pt.track_id,
        ^user_tracks
      ),
    limit: ^limit,
    select: p
  )

Это работает, но довольно медленно, есть ли лучший способ написать этот запрос?

Вкратце: «Если у нас есть таблица users, со многими ассоциациями в объединяемой таблице user_tracks, то эта ссылка на tracks. Упорядочить элементы в таблице playlists по количеству ассоциаций, которые они имеют с таблицей tracks общего с users assocs в таблице tracks "

1 Ответ

0 голосов
/ 08 июля 2019

Я бы начал с создания оптимального необработанного SQL-запроса, а затем посмотрел, можно ли / как его перевести в Ecto.

Учитывая эти данные:

CREATE TABLE playlist_tracks (playlist_id integer, track_id integer);
CREATE TABLE playlists (id integer, title character varying);
CREATE TABLE user_tracks (user_id integer, track_id integer);

INSERT INTO user_tracks VALUES (1, 2), (1, 1), (1, 3), (1, 4), (1, 5), (1, 6), (1, 7), (1, 8), (1, 9), (1, 10);
INSERT INTO playlist_tracks VALUES (1, 1), (1, 2), (1, 3), (2, 2), (2, 3), (2, 4), (2, 5), (3, 3), (3, 4), (3, 5), (3, 6), (3, 7), (4, 9), (4, 10), (4, 11), (5, 10), (5, 11), (5, 12);
INSERT INTO playlists VALUES (1, 'Classic Rock''s Greatest Hits'),
                             (2, 'Kings & Queens of Blues Rock'),
                             (3, 'Mojito Lounge'),
                             (4, 'Dark Electro Lounging'),
                             (5, 'Atmospheric Chilled Electronica');

Полученный запросвыглядит следующим образом:

  SELECT p.id,
         p.title
    FROM playlists p
    JOIN playlist_tracks pt on p.id = pt.playlist_id
    JOIN user_tracks ut on pt.track_id = ut.track_id
   WHERE ut.user_id = 1
GROUP BY p.id,
         p.title
ORDER BY count(1) DESC;

Обратите внимание, что вам не нужно присоединяться к таблице users.

Выполнение explain analyse ... по запросу возвращает следующее:

                                                                  QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------------------------------
 Sort  (cost=150.61..151.11 rows=200 width=44) (actual time=0.131..0.131 rows=5 loops=1)
   Sort Key: (count(1)) DESC
   Sort Method: quicksort  Memory: 25kB
   ->  HashAggregate  (cost=140.97..142.97 rows=200 width=44) (actual time=0.119..0.121 rows=5 loops=1)
         Group Key: p.id, p.title
         ->  Hash Join  (cost=82.25..135.05 rows=789 width=36) (actual time=0.104..0.108 rows=15 loops=1)
               Hash Cond: (p.id = pt.playlist_id)
               ->  Seq Scan on playlists p  (cost=0.00..22.70 rows=1270 width=36) (actual time=0.011..0.012 rows=5 loops=1)
               ->  Hash  (cost=80.70..80.70 rows=124 width=4) (actual time=0.073..0.073 rows=15 loops=1)
                     Buckets: 1024  Batches: 1  Memory Usage: 9kB
                     ->  Hash Join  (cost=38.39..80.70 rows=124 width=4) (actual time=0.042..0.066 rows=15 loops=1)
                           Hash Cond: (pt.track_id = ut.track_id)
                           ->  Seq Scan on playlist_tracks pt  (cost=0.00..32.60 rows=2260 width=8) (actual time=0.005..0.008 rows=18 loops=1)
                           ->  Hash  (cost=38.25..38.25 rows=11 width=4) (actual time=0.020..0.020 rows=10 loops=1)
                                 Buckets: 1024  Batches: 1  Memory Usage: 9kB
                                 ->  Seq Scan on user_tracks ut  (cost=0.00..38.25 rows=11 width=4) (actual time=0.007..0.011 rows=10 loops=1)
                                       Filter: (user_id = 1)
 Planning Time: 0.215 ms
 Execution Time: 0.658 ms
(19 rows)

Наличие следующих индексов значительно улучшит производительность запросов:

create index ut_user_id on user_tracks (user_id);
create index ut_track_id on user_tracks (track_id);
create index pt_track_id on playlist_tracks (track_id);
create index pt_playlist_id on playlist_tracks (playlist_id);
create index p_id on playlists (id);

Улучшенный план:

                                                                 QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------------------------
 Sort  (cost=3.26..3.27 rows=1 width=44) (actual time=0.103..0.103 rows=5 loops=1)
   Sort Key: (count(1)) DESC
   Sort Method: quicksort  Memory: 25kB
   ->  GroupAggregate  (cost=3.23..3.25 rows=1 width=44) (actual time=0.093..0.098 rows=5 loops=1)
         Group Key: p.id, p.title
         ->  Sort  (cost=3.23..3.24 rows=1 width=36) (actual time=0.087..0.089 rows=15 loops=1)
               Sort Key: p.id, p.title
               Sort Method: quicksort  Memory: 26kB
               ->  Nested Loop  (cost=1.27..3.22 rows=1 width=36) (actual time=0.043..0.075 rows=15 loops=1)
                     ->  Hash Join  (cost=1.14..2.39 rows=1 width=4) (actual time=0.037..0.045 rows=15 loops=1)
                           Hash Cond: (pt.track_id = ut.track_id)
                           ->  Seq Scan on playlist_tracks pt  (cost=0.00..1.18 rows=18 width=8) (actual time=0.007..0.010 rows=18 loops=1)
                           ->  Hash  (cost=1.12..1.12 rows=1 width=4) (actual time=0.016..0.016 rows=10 loops=1)
                                 Buckets: 1024  Batches: 1  Memory Usage: 9kB
                                 ->  Seq Scan on user_tracks ut  (cost=0.00..1.12 rows=1 width=4) (actual time=0.006..0.009 rows=10 loops=1)
                                       Filter: (user_id = 1)
                     ->  Index Scan using p_id on playlists p  (cost=0.13..0.82 rows=1 width=36) (actual time=0.001..0.001 rows=1 loops=15)
                           Index Cond: (id = pt.playlist_id)
 Planning Time: 0.505 ms
 Execution Time: 0.163 ms
(20 rows)

Возвращает следующий результат:

 id |              title
----+---------------------------------
  3 | Mojito Lounge
  2 | Kings & Queens of Blues Rock
  1 | Classic Rock's Greatest Hits
  4 | Dark Electro Lounging
  5 | Atmospheric Chilled Electronica
(5 rows)

Похоже ли это на то, что вы ожидаете?

Если да, вот как это можно перевести на Ecto:

import Ecto.Query

user_id = 1

query =
  from p in "playlists",
    select: {p.id, p.title},
    join: pt in "playlist_tracks", on: pt.playlist_id == p.id,
    join: ut in "user_tracks", on: ut.track_id == pt.track_id,
    where: ut.user_id == ^user_id,
    group_by: [p.id, p.title],
    order_by: count(1)

MyRepo.all(query)

Вы должны быть в состоянии принятьэтот пример:

  • переключение ссылок на имена таблиц, таких как playlists и user_tracks для схем, например, Playlist и User.Track соответственно (при условии, что так называются ваши модули схемы),

  • изменение select: {p.id, p.title} на select: struct(p, [p.id, p.title]), что даст вам Playlist структуры вместо tuplЕсли это желательно ( не проверено, хотя).


Обычно, если запрос не должен быть очень гибким (например, зависеть от многих параметров, определенных ввремя выполнения), которое достигается путем динамического построения его с использованием Ecto, должно быть прекрасно поддерживать запрос, написанный на необработанном SQL.

...