我有以下验证:
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秒钟?这里有什么问题?
答案 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)