通过大表上的操作加速组

Posted

技术标签:

【中文标题】通过大表上的操作加速组【英文标题】:Speeding up a group by operation on large table 【发布时间】:2021-12-24 17:40:44 【问题描述】:

我有两个大表,tokens(100.000 条条目)和buy_orders(1.000.000 条条目)需要有效地加入和分组。

如下所示,代币由合约地址(一个 20 字节的十六进制字符串)和一个 id(一个 256 字节的整数)唯一标识:

TABLE tokens (
  contract TEXT NOT NULL
  token_id NUMERIC(78, 0) NOT NULL
  top_bid NUMERIC(78, 0)

  PRIMARY KEY (contract, token_id)
)

用户可以对各种代币进行出价。投标具有有效期(通过时间范围表示)和价格(256 字节整数)。出价只能是以下两种类型之一:

类型 1:单一合约,token_id 范围(例如contract + start_token_id + end_token_id) 类型 2:多个合约,多个 token_id(例如[(contract1 + token_id1), (contract2 + token_id2), ...]

下表是保留出价的表格。它是高度非规范化的,以适应出价可能具有的 2 种可能类型。

TABLE buy_orders (
  id INT NOT NULL PRIMARY KEY
  contract TEXT
  start_token_id NUMERIC(78, 0)
  end_token_id NUMERIC(78, 0)
  token_list_id INT REFERENCES token_lists(id)
  price NUMERIC(78, 0) NOT NULL,
  valid_between TSTZRANGE NOT NULL,
  cancelled BOOLEAN NOT NULL,
  executed BOOLEAN NOT NULL

  INDEX ON (contract, start_token_id, end_token_id DESC)
  INDEX ON (token_list_id)
  INDEX ON (price)
  INDEX ON (cancelled, executed)
  INDEX ON (valid_between) USING gist
)

以下是保存属于每个列表的标记的相应表:

TABLE token_lists (
  id INT PRIMARY KEY
)

TABLE token_lists_tokens (
  token_list_id INT NOT NULL REFERENCES token_lists(id)
  contract TEXT NOT NULL
  token_id NUMERIC(78, 0) NOT NULL

  FOREIGN KEY (contract, token_id) REFERENCES tokens(address, id)
  INDEX ON (contract, token_id)
)

正如您在tokens 表中所见,它会跟踪最高出价,以便尽可能高效地检索令牌数据(我们将有一个分页 API 用于检索地址的所有令牌,包括其当前最高出价)。随着新出价的出现、被取消/填写或过期,我需要一种有效的方法来更新出价所针对的代币的最高出价。对于类型 2 的出价,这不是问题,因为它们大多数时候会引用少量的代币,但它会为类型 1 的出价带来问题,因为在这种情况下,我可能需要重新计算 100.000 的最高出价有效的代币(例如,类型 2 出价的范围可能是[1, 100.000])。这是我现在正在使用的查询(我限制了结果,否则它需要很长时间):

SELECT t.contract, t.token_id, max(b.price) FROM tokens t
JOIN buy_orders b ON t.contract = b.contract AND b.start_token_id <= t.token_id AND t.token_id <= b.end_token_id
WHERE t.contract = 'foo' AND NOT b.cancelled AND NOT b.filled AND b.valid_between @> now() 
GROUP BY t.contract, t.token_id
LIMIT 1000

这是它的执行计划:

 Limit  (cost=5016.77..506906.79 rows=1000 width=81) (actual time=378.231..19260.361 rows=1000 loops=1)
   ->  GroupAggregate  (cost=5016.77..37281894.72 rows=74273 width=81) (actual time=123.729..19005.567 rows=1000 loops=1)
         Group Key: t.contract, t.token_id
         ->  Nested Loop  (cost=5016.77..35589267.24 rows=225584633 width=54) (actual time=83.885..18953.853 rows=412253 loops=1)
               Join Filter: ((b.start_token_id <= t.token_id) AND (t.token_id <= b.end_token_id))
               Rows Removed by Join Filter: 140977658
               ->  Index Only Scan using tokens_pk on tokens t  (cost=0.55..8186.80 rows=99100 width=49) (actual time=0.030..5.394 rows=11450 loops=1)
                     Index Cond: (contract = 'foo'::text)
                     Heap Fetches: 0
               ->  Materialize  (cost=5016.21..51551.91 rows=20487 width=60) (actual time=0.001..0.432 rows=12348 loops=11450)
                     ->  Bitmap Heap Scan on buy_orders b  (cost=5016.21..51449.47 rows=20487 width=60) (actual time=15.245..116.099 rows=12349 loops=1)
                           Recheck Cond: (contract = 'foo'::text)
                           Filter: ((NOT cancelled) AND (NOT filled) AND (valid_between @> now()))
                           Rows Removed by Filter: 87771
                           Heap Blocks: exact=33525
                           ->  Bitmap Index Scan on buy_orders_contract_start_token_id_end_token_id_index  (cost=0.00..5011.09 rows=108072 width=0) (actual time=10.835..10.835 rows=100120 loops=1)
                                 Index Cond: (contract = 'foo'::text)
 Planning Time: 0.816 ms
 JIT:
   Functions: 15
   Options: Inlining true, Optimization true, Expressions true, Deforming true
   Timing: Generation 3.922 ms, Inlining 106.877 ms, Optimization 99.947 ms, Emission 47.445 ms, Total 258.190 ms
 Execution Time: 19264.851 ms

如果可能的话,我正在寻找一种提高此特定查询效率的方法,或其他建议以达到相同的结果。

我正在使用 Postgres 13。

【问题讨论】:

【参考方案1】:

部分的多列索引可能会有所帮助。比如;

CREATE INDEX ON buy_orders (contract, valid_between) -- Multiple fields
  INCLUDE (price) -- non-key column for index only scan
  WHERE -- represents partial index
    NOT cancelled AND
    NOT filled;

这将允许buy_orders 上的索引扫描删除更多行,这样您就不会得到

Rows Removed by Join Filter: 140977658

这就是让您的查询变得昂贵的原因。

【讨论】:

以上是关于通过大表上的操作加速组的主要内容,如果未能解决你的问题,请参考以下文章

有啥方法可以在同一个大表上使用 3x UNION All 来加速复杂查询?

大表上的慢 MySQL SELECT

Oracle - 未使用大表上的索引

大表上的第一次查询调用速度非常慢

mysql 表很少,一个大表上的子查询执行缓慢

在 Django 中的大表上的内存效率(常量)和速度优化迭代