唯一性区分大小写的错误导致查询速度慢

时间:2014-12-18 07:09:32

标签: ruby-on-rails postgresql ruby-on-rails-4 activerecord query-optimization

我有以下验证:

validates :username, uniqueness: { case_sensitive: false }

这导致以下查询运行缓慢:

5,510 ms
SELECT ? AS one FROM "users" WHERE (LOWER("users"."username") = LOWER(?) AND "users"."id" != ?) LIMIT ?
Explain plan
1 Query plan    Limit (cost=0.03..4.03 rows=1 width=0)
2 Query plan    -> Index Scan using idx_users_lower_username on users (cost=0.03..4.03 rows=1 width=0)
3 Query plan    Index Cond: ?
4 Query plan    Filter: ?

使用structure.sql在我的CREATE INDEX idx_users_lower_username ON users USING btree (lower((username)::text));中创建了索引。有关详情,请参阅我的问题How to create index on LOWER("users"."username") in Rails (using postgres)

这是使用我设置的索引,仍然需要5秒钟?这里有什么问题?

2 个答案:

答案 0 :(得分:1)

这里有几个不同的,相互关联的事情。具体如何执行更改取决于您如何管理对数据库结构的更改。最常见的方法是使用Rails迁移,但是您的链接问题表明您没有这样做。所以我将主要用SQL来讲,你可以根据你的方法进行调整。

使用sargable WHERE子句

你的WHERE子句不是sargable。这意味着它的编写方式会阻止dbms使用索引。要创建一个PostgreSQL可以在这里使用的索引。 。 。

create index on "users" (lower("username") varchar_pattern_ops);

现在对小写用户名的查询可以使用该索引。

explain analyze
select * 
from users
where lower(username) = lower('9LCDgRHk7kIXehk6LESDqHBJCt9wmA');

看起来好像PostgreSQL必须小写表中的每个用户名,但它的查询规划器足够聪明,可以看到表达式lower(username)本身已被索引。 PostgreSQL使用索引扫描。

"Index Scan using users_lower_idx on users  (cost=0.43..8.45 rows=1 width=35) (actual time=0.034..0.035 rows=1 loops=1)"
"  Index Cond: (lower((username)::text) = 'b0sa9malg7yt1shssajrynqhiddm5d'::text)"
"Total runtime: 0.058 ms"

该表有一百万行随机数据;查询返回非常非常快。对于“id”的附加条件,它几乎同样快,但是LIMIT子句减慢了很多。 “减慢很多”并不意味着它很慢;它仍然在不到0.1毫秒内返回。

此外,varchar_pattern_ops允许使用LIKE运算符的查询使用索引。

explain analyze
select *
from users
where lower(username) like 'b%'


"Bitmap Heap Scan on users  (cost=1075.12..9875.78 rows=30303 width=35) (actual time=10.217..91.030 rows=31785 loops=1)"
"  Filter: (lower((username)::text) ~~ 'b%'::text)"
"  ->  Bitmap Index Scan on users_lower_idx  (cost=0.00..1067.54 rows=31111 width=0) (actual time=8.648..8.648 rows=31785 loops=1)"
"        Index Cond: ((lower((username)::text) ~>=~ 'b'::text) AND (lower((username)::text) ~<~ 'c'::text))"
"Total runtime: 93.541 ms"

仅需94毫秒即可从百万行中选择并返回30k行。

即使有可用的索引,对非常小的表的查询也可能使用顺序扫描。如果我是你,我不会担心。

在数据库中强制实施唯一性

如果您期望任何流量突发,您应该在数据库中强制执行唯一性。无论对流量有什么期望(猜测),我都会所有时间。

RailsGuides Active Record Validations包含了关于“唯一性”帮助者的这个有点误导或混淆的段落。

  

此帮助程序验证属性的值是否唯一   在保存对象之前。它不会创造唯一性   数据库中的约束,所以可能会发生两种不同的情况   数据库连接创建两个具有相同值的记录   您打算独特的列。为避免这种情况,您必须创建一个   数据库中两列的唯一索引。请参阅MySQL手册   有关多列索引的更多详细信息。

它清楚地说,事实上,它并不保证唯一性。误导性部分是关于在“两列”上创建唯一索引。如果您希望“用户名”是唯一的,则需要在“用户名”列上声明唯一约束。

alter table "users"
add constraint constraint_name unique (username);

<强>区分大小写

在SQL数据库中,区分大小写由排序规则决定。排序规则是SQL标准的一部分。

在PostgreSQL中,您可以在数据库级别,列级别,索引级别和查询级别设置排序规则。值来自使用initdb创建新数据库集群时操作系统公开的语言环境。

在Linux系统上,您可能没有不区分大小写的排序规则。这就是为什么我们必须比针对SQL Server和Oracle的人更多地跳过这个环节。

答案 1 :(得分:0)

尝试使用explain analyze在psql中运行查询,因此确保postgres运行正常,因为显然索引和查询是正确的。

如果在psql中速度很快,那么你的rails代码就会出现问题。

这个针对3k记录表的查询给出了这个结果(在我的本地开发机器中):

app=# explain analyze SELECT id AS one FROM "users" WHERE (LOWER(email) = LOWER('marcus@marcus.marcus') AND "users"."id" != 2000);
                                                       QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------
 Bitmap Heap Scan on users  (cost=4.43..58.06 rows=19 width=4) (actual time=0.101..0.101 rows=0 loops=1)
   Recheck Cond: (lower((email)::text) = 'marcus@marcus.marcus'::text)
   Filter: (id <> 2000)
   ->  Bitmap Index Scan on users_lower_idx  (cost=0.00..4.43 rows=19 width=0) (actual time=0.097..0.097 rows=0 loops=1)
         Index Cond: (lower((email)::text) = 'marcus@marcus.marcus'::text)
 Total runtime: 0.144 ms
(6 rows)