【问题标题】:Slow query ordering by a column in a joined table按联接表中的列进行慢速查询排序
【发布时间】:2012-04-10 21:34:57
【问题描述】:

在查询中引入 ORDER BY 子句会增加总时间,因为数据库必须做额外的工作才能对结果集进行排序:

  • 将生成的元组复制到一些临时内存中
  • 对它们进行排序(希望在内存中,否则使用磁盘)
  • 将结果流式传输到客户端

我想念的是为什么仅仅从连接表中添加一列会产生如此不同的性能。

查询1

EXPLAIN ANALYZE
SELECT p.*
FROM product_product p
JOIN django_site d ON (p.site_id = d.id)
WHERE (p.active = true  AND p.site_id = 1 )
ORDER BY d.domain, p.ordering, p.name

查询计划

Sort  (cost=3909.83..3952.21 rows=16954 width=1086) (actual time=1120.618..1143.922 rows=16946 loops=1)
   Sort Key: django_site.domain, product_product.ordering, product_product.name
   Sort Method:  quicksort  Memory: 25517kB
   ->  Nested Loop  (cost=0.00..2718.86 rows=16954 width=1086) (actual time=0.053..87.396 rows=16946 loops=1)
         ->  Seq Scan on django_site  (cost=0.00..1.01 rows=1 width=24) (actual time=0.010..0.012 rows=1 loops=1)
               Filter: (id = 1)
         ->  Seq Scan on product_product  (cost=0.00..2548.31 rows=16954 width=1066) (actual time=0.036..44.138 rows=16946 loops=1)
               Filter: (product_product.active AND (product_product.site_id = 1))
 Total runtime: 1182.515 ms

查询 2

同上,但不按django_site.domain排序

查询计划

 Sort  (cost=3909.83..3952.21 rows=16954 width=1066) (actual time=257.094..278.905 rows=16946 loops=1)
   Sort Key: product_product.ordering, product_product.name
   Sort Method:  quicksort  Memory: 25161kB
   ->  Nested Loop  (cost=0.00..2718.86 rows=16954 width=1066) (actual time=0.075..86.120 rows=16946 loops=1)
         ->  Seq Scan on django_site  (cost=0.00..1.01 rows=1 width=4) (actual time=0.015..0.017 rows=1 loops=1)
               Filter: (id = 1)
         ->  Seq Scan on product_product  (cost=0.00..2548.31 rows=16954 width=1066) (actual time=0.052..44.024 rows=16946 loops=1)
               Filter: (product_product.active AND (product_product.site_id = 1))
 Total runtime: 305.392 ms

This question 可能是相关的。

编辑:添加了更多细节

           Table "public.product_product"
 Column       |          Type          |  
 -------------+------------------------+---------
 id                | integer                | not null default nextval('product_product_id_seq'::regclass)
 site_id           | integer                | not null
 name              | character varying(255) | not null
 slug              | character varying(255) | not null
 sku               | character varying(255) | 
 ordering          | integer                | not null
 [snip some columns ]

 Indexes:
    "product_product_pkey" PRIMARY KEY, btree (id)
    "product_product_site_id_key" UNIQUE, btree (site_id, sku)
    "product_product_site_id_key1" UNIQUE, btree (site_id, slug)
    "product_product_site_id" btree (site_id)
    "product_product_slug" btree (slug)
    "product_product_slug_like" btree (slug varchar_pattern_ops)


                  Table "public.django_site"
 Column |          Type          | 
--------+------------------------+----------
 id     | integer                | not null default nextval('django_site_id_seq'::regclass)
 domain | character varying(100) | not null
 name   | character varying(50)  | not null
Indexes:
    "django_site_pkey" PRIMARY KEY, btree (id)

Postgres 版本是 8.4

一些表格统计数据:

# select count(*) from django_site;
 count 
-------
     1

# select count(*) from product_product;
 count 
-------
 17540

# select active, count(*) from product_product group by active;
 active | count 
--------+-------
 f      |   591
 t      | 16949

# select site_id, count(*) from product_product group by site_id;
 site_id | count 
---------+-------
       1 | 17540

【问题讨论】:

  • Site_id 应该是两个表中的关键字段(的一部分)。是吗?

标签: sql performance postgresql sql-order-by collation


【解决方案1】:

测试用例

PostgreSQL 9.1。资源有限的测试数据库,但对于这个小案例来说已经足够了。排序规则的语言环境将是相关的:

SHOW LC_COLLATE;

 de_AT.UTF-8

步骤 1) 重建原始测试环境

-- DROP TABLE x;
CREATE SCHEMA x;  -- test schema

-- DROP TABLE x.django_site;
CREATE TABLE x.django_site (
id serial primary key
,domain character varying(100) not null
,int_col int not null
);
INSERT INTO x.django_site values (1,'www.testsite.com/foodir/', 3);

-- DROP TABLE x.product;
CREATE TABLE x.product (
 id serial primary key
,site_id integer not null
,name character varying(255) not null
,slug character varying(255) not null
,sku character varying(255) 
,ordering integer not null
,active boolean not null
);

INSERT INTO x.product (site_id, name, slug, sku, ordering, active)
SELECT 1
    ,repeat(chr((random() * 255)::int + 32), (random()*255)::int)
    ,repeat(chr((random() * 255)::int + 32), (random()*255)::int)
    ,repeat(chr((random() * 255)::int + 32), (random()*255)::int)
    ,i -- ordering in sequence
    ,NOT (random()* 0.5174346569119122)::int::bool
FROM generate_series(1, 17540) AS x(i);
-- SELECT ((591::float8 / 17540)* 0.5) / (1 - (591::float8 / 17540))
-- = 0.5174346569119122

CREATE INDEX product_site_id on x.product(site_id);

第 2 步)分析

    ANALYZE x.product;
    ANALYZE x.django_site;

第 3 步)通过 random() 重新排序

-- DROP TABLE x.p;
CREATE TABLE x.p AS
SELECT *
FROM   x.product
ORDER  BY random();

ANALYZE x.p;

结果

EXPLAIN ANALYZE
    SELECT p.*
    FROM   x.p
    JOIN   x.django_site d ON (p.site_id = d.id)
    WHERE  p.active
    AND    p.site_id = 1
--    ORDER  BY d.domain, p.ordering, p.name
--    ORDER  BY p.ordering, p.name
--    ORDER  BY d.id, p.ordering, p.name
--    ORDER  BY d.int_col, p.ordering, p.name
--    ORDER  BY p.name COLLATE "C"
--    ORDER  BY d.domain COLLATE "C", p.ordering, p.name -- dvd's final solution

1) 预分析(-> 位图索引扫描)
2) 后分析 (-> seq 扫描)
3) 通过 random() 重新排序,ANALYZE

ORDER  BY d.domain, p.ordering, p.name

1) 总运行时间:1253.543 毫秒
2) 总运行时间:1250.351 毫秒
3) 总运行时间:1283.111 毫秒

ORDER  BY p.ordering, p.name

1) 总运行时间:177.266 毫秒
2) 总运行时间:174.556 毫秒
3) 总运行时间:177.797 毫秒

ORDER  BY d.id, p.ordering, p.name

1) 总运行时间:176.628 毫秒
2) 总运行时间:176.811 毫秒
3) 总运行时间:178.150 毫秒
计划者显然考虑到 d.id 是功能依赖的。

ORDER BY d.int_col, p.ordering, p.name -- 其他表中的整数列

1) 总运行时间:242.218 毫秒 -- !!
2) 总运行时间:245.234 毫秒
3) 总运行时间:254.581 毫秒
规划器显然忽略了 d.int_col (NOT NULL) 与功能相关。但是按整数列排序很便宜。

ORDER BY p.name -- varchar(255) 在同一张表中

1) 总运行时间:2259.171 毫秒 -- !!
2) 总运行时间:2257.650 毫秒
3) 总运行时间:2258.282 毫秒
按(长)varchartext 列排序很昂贵...

按 p.name 排序 整理“C”

1) 总运行时间:327.516 毫秒 -- !!
2) 总运行时间:325.103 毫秒
3) 总运行时间:327.206 毫秒
...但如果没有语言环境,则不会那么昂贵。

在不影响语言环境的情况下,按varchar 列进行排序的速度并不快,但几乎一样快。语言环境"C" 实际上是“无语言环境,仅按字节值排序”。我quote the manual

如果您希望系统表现得好像没有语言环境支持,请使用 特殊的语言环境名称 C,或等效的 POSIX。


综上所述,@dvd 选择了:

ORDER  BY d.domain COLLATE "C", p.ordering, p.name

... 3) 总运行时间:275.854 毫秒
应该可以。

【讨论】:

  • 你又打败了我。我刚刚完成了表格创建和人口的输入。事实上,优化器应该看到 d.domain 在功能上依赖于 p.site_id = d.id。我什至添加了一个 FK 约束,但这并没有帮助。另外:OP中的rowsize非常大,大约1K。
  • 是的,优化器有一个盲点。由于p.site_id 有一个= 条件,而其他涉及的列d.id, d.domain 不为空,因此按d.domain 排序在逻辑上无效。有一个更聪明的计划者的潜力。你认为行大小有额外的影响吗?
  • 看起来 25M 的工作内存可以容纳所有 17K * 1K 的记录。如果 identical .domain 字符串,最后的 ORDER BY 必须遍历整个长度。优化器可能知道这一点(功能依赖 site_id -> 域导致域的等价类域比较。字符编码/整理似乎使事情变得更糟)。当我找到时间时,我会调查源头。也许通知汤姆莱恩?
  • 确实是一个非常有趣的案例。还有一个条件:d.id 是唯一的。 (如果不是,d.domain 可能有多个值。)我将离开几天。快乐的黑客攻击!
  • 它不仅是唯一的,而且是固定在一个常数上的。但是最近似乎有很多情况,最终的排序(+ LIMIT ...)是把小龙虾扔进漂亮的机器里。徒步愉快!
【解决方案2】:

EXPLAIN ANALYZE 的输出与排序操作相同,因此排序会有所不同。

在这两个查询中,您都返回product_product 的所有行,但在第一种情况下,您按django_site 的列排序,因此必须另外检索django_site.domain,这需要额外费用。但无法解释其中的巨大差异。

很有可能product_product 中行的物理顺序 已经根据ordering 列,这使得 case 2 的排序非常便宜,而 case in case 的排序1 贵。


在“添加更多细节”之后:
它也相当昂贵所以按character varying(100) 排序比按integer 列排序。除了整数要小得多之外,还有排序规则支持会减慢您的速度。要验证,请尝试使用COLLATE "C" 订购。阅读有关collation support in the manual 的更多信息。 如果您正在运行 PostgreSQL 9.1。我现在看到,你有 PostgreSQL 8.4。

显然,查询输出中的所有行都具有相同的 django_site.domain 值,因为您在 p.site_id = 1 上进行过滤。如果查询计划器更聪明,它可能会跳过第一列以开始排序。

您运行的是 PostgreSQL 8.4。 9.1 的查询计划器变得更加智能。升级可能会改变这种情况,但我不能肯定。


要验证我关于物理排序的理论,您可以尝试制作一个大表的副本,其中的行以随机顺序插入,然后再次运行查询。像这样:

CREATE TABLE p AS
SELECT *
FROM   public.product_product
ORDER  BY random();

然后:

EXPLAIN ANALYZE
SELECT p.*
FROM   p
JOIN   django_site d ON (p.site_id = d.id)
WHERE  p.active
AND    p.site_id = 1
ORDER  BY d.domain, p.ordering, p.name;

有什么不同吗? --> 显然这并不能解释它......


好的,为了测试varchar(100) 是否有所作为,我重新创建了您的场景。请参阅separate answer with a detailed test case and benchmark。这个答案已经超载了。

总结一下:
事实证明,我的其他解释是合适的。速度变慢的主要原因显然是根据locale (LC_COLLATE)varchar(100) 列排序。

我添加了一些解释和指向test case 的链接。结果不言自明。

【讨论】:

  • 两个输出不相等,估计成本是,但不是实际时间。显然这两个查询计划是相同的。
  • @dvd:嗯,计划的操作是一样的。仅在一种情况下,这种类型的成本要高得多——原因我们现在正在猜测。
  • Brandstetter:我在这里,我使用 order by random() 创建了表(我已经粘贴了你的代码),结果是相同的: >1sec with d.domain in order 子句,
  • Brandsetter:与 Postgres 9.1 的行为相同
  • @dvd:查看我的测试结果。现在得跑了,数字应该说明一切。
【解决方案3】:

据我所知,您需要一些索引

  1. 在 product_product(active, site_id) 上创建索引 product_product_idx01; 这可能会加快您的查询速度。
  2. 你为什么要按域排序,你的查询没有意义

【讨论】:

  • 索引在这种情况下没有帮助,查询花费了他的时间排序而不是过滤,并且 order by 的第一列没有被索引覆盖。域列是由 django(站点应用程序)自动添加的,我会为此开一张票,但在修补代码之前我想知道我是否可以调整数据库。
  • 这两个 seqscan 是可疑的。如果 site_id 是密钥/FK 的(第一部分),则不会发生这种情况。请添加表定义,包括索引/键约束。顺便说一句,stie_id 到底有多独特?
  • 你应该添加这个索引:) 如果我弄错了你可以随时删除它。记录量很小,查询这么贵
  • @Pavieg 抱歉不清楚,我已经测试了您的建议,然后删除了索引,因为它没有任何优势
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多