Постгрес подготовил заявления с различными полями - PullRequest
2 голосов
/ 18 марта 2012

Я ищу способы абстрагировать доступ к базе данных для postgres. В моих примерах я буду использовать гипотетический твиттер-клон в nodejs, но в конечном итоге это вопрос о том, как postgres обрабатывает подготовленные операторы, поэтому язык и библиотека на самом деле не имеют значения:

Предположим, я хочу получить доступ к списку всех твитов пользователя по имени пользователя:

name: "tweets by username"
text: "SELECT (SELECT * FROM tweets WHERE tweets.user_id = users.user_id) FROM users WHERE users.username = $1"
values: [username]

Это прекрасно работает, но кажется неэффективным, как с практической точки зрения, так и с точки зрения качества кода, когда приходится создавать другую функцию для обработки твитов по электронной почте, а не по имени пользователя:

name: "tweets by email"
text: "SELECT (SELECT * FROM tweets WHERE tweets.user_id = users.user_id) FROM users WHERE users.email = $1"
values: [email]

Можно ли включить поле в качестве параметра в подготовленный оператор?

name: "tweets by user"
text: "SELECT (SELECT * FROM tweets WHERE tweets.user_id = users.user_id) FROM users WHERE users.$1 = $2"
values: [field, value]

Хотя это правда, что это может быть немного менее эффективно в случае углового доступа к твитам по user_id, я хочу совершить эту сделку, чтобы улучшить качество кода и, надеюсь, в целом повысить эффективность за счет уменьшения количества шаблонов запросов до 1 вместо 3 +.

Ответы [ 2 ]

3 голосов
/ 18 марта 2012

@ Ответ Клодоальдо верен в том смысле, что он дает возможность, которую вы желаете, и должен давать правильные результаты. К сожалению, это приводит к довольно медленному выполнению.

Я создал экспериментальную базу данных с твитами и пользователями. Население 10 000 пользователей, каждый из которых имеет 100 твитов (1 млн записей твитов). Я проиндексировал PKs u.id, t.id, FK t.user_id и поля предикатов u.username, u.email.

create table t(id serial PRIMARY KEY, data integer, user_id bignit);
create index t1 t(user_id);
create table u(id serial PRIMARY KEY, name text, email text);
create index u1 on u(name);
create index u2 on u(email);
insert into u(name,email) select i::text, i::text from generate_series(1,10000) i;
insert into t(data,user_id) select i, (i/100)::bigint from generate_series(1,1000000) i;
analyze table t;
analyze table u;

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

prepare qn as select t.* from t join u on t.user_id = u.id where u.name = $1;

explain analyze execute qn('1111');
 Nested Loop  (cost=0.00..19.81 rows=1 width=16) (actual time=0.030..0.057 rows=100 loops=1)
   ->  Index Scan using u1 on u  (cost=0.00..8.46 rows=1 width=4) (actual time=0.020..0.020 rows=1 loops=1)
         Index Cond: (name = $1)
   ->  Index Scan using t1 on t  (cost=0.00..10.10 rows=100 width=16) (actual time=0.007..0.023 rows=100 loops=1)
         Index Cond: (t.user_id = u.id)
 Total runtime: 0.093 ms

Запрос с использованием регистра case в предложении where как @Clodoaldo занимает почти 30 секунд:

prepare qen as select t.* from t join u on t.user_id = u.id
  where case $2 when 'e' then u.email = $1 when 'n' then u.name = $1 end;

explain analyze execute qen('1111','n');
 Merge Join  (cost=25.61..38402.69 rows=500000 width=16) (actual time=27.771..26345.439 rows=100 loops=1)
   Merge Cond: (t.user_id = u.id)
   ->  Index Scan using t1 on t  (cost=0.00..30457.35 rows=1000000 width=16) (actual time=0.023..17.741 rows=111200 loops=1)
   ->  Index Scan using u_pkey on u  (cost=0.00..42257.36 rows=500000 width=4) (actual time=0.325..26317.384 rows=1 loops=1)
         Filter: CASE $2 WHEN 'e'::text THEN (u.email = $1) WHEN 'n'::text THEN (u.name = $1) ELSE NULL::boolean END
 Total runtime: 26345.535 ms

Наблюдая за этим планом, я подумал, что использование подвыбора объединения с последующей фильтрацией его результатов для получения идентификатора, соответствующего параметризованному предикату, позволит планировщику использовать конкретные индексы для каждого предиката. Оказывается, я был прав:

prepare qen2 as 
select t.*
from t 
join (
 SELECT id from 
  (
  SELECT 'n' as fld, id from u where u.name = $1
  UNION ALL
  SELECT 'e' as fld, id from u where u.email = $1
  ) poly
 where poly.fld = $2
) uu
on t.user_id = uu.id;

explain analyze execute qen2('1111','n');
 Nested Loop  (cost=0.00..28.31 rows=100 width=16) (actual time=0.058..0.120 rows=100 loops=1)
   ->  Subquery Scan poly  (cost=0.00..16.96 rows=1 width=4) (actual time=0.041..0.073 rows=1 loops=1)
         Filter: (poly.fld = $2)
         ->  Append  (cost=0.00..16.94 rows=2 width=4) (actual time=0.038..0.070 rows=2 loops=1)
               ->  Subquery Scan "*SELECT* 1"  (cost=0.00..8.47 rows=1 width=4) (actual time=0.038..0.038 rows=1 loops=1)
                     ->  Index Scan using u1 on u  (cost=0.00..8.46 rows=1 width=4) (actual time=0.038..0.038 rows=1 loops=1)
                           Index Cond: (name = $1)
               ->  Subquery Scan "*SELECT* 2"  (cost=0.00..8.47 rows=1 width=4) (actual time=0.031..0.032 rows=1 loops=1)
                     ->  Index Scan using u2 on u  (cost=0.00..8.46 rows=1 width=4) (actual time=0.030..0.031 rows=1 loops=1)
                           Index Cond: (email = $1)
   ->  Index Scan using t1 on t  (cost=0.00..10.10 rows=100 width=16) (actual time=0.015..0.028 rows=100 loops=1)
         Index Cond: (t.user_id = poly.id)
 Total runtime: 0.170 ms
0 голосов
/ 18 марта 2012
SELECT t.* 
FROM tweets t 
inner join users u on t.user_id = u.user_id
WHERE case $2
    when 'username' then u.username = $1
    when 'email' then u.email = $1
    else u.user_id = $1
    end
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...