关于postgresql:使用非空约束优化Postgres JSONB查询

Optimizing Postgres JSONB query with not null constraint

我有一个包含170万条记录的Postgres 9.4.4数据库,其中以下信息存储在名为accounts的表中名为data的JSONB列中:

1
2
3
4
5
6
7
8
DATA: {
 "lastUpdatedTime":"2016-12-26T12:09:43.901Z",
 "UID":"2c5bb7fd-1a00-4988-8d92-ffaa52ebc20d",
 "data": {
   "country":"UK",
   "verified_at":"2017-01-01T23:49:10.217Z"
  }
}

由于这是遗留信息,因此无法更改数据格式。

我需要获取国家/地区UK的所有帐户,verified_at值不为空且lastUpdatedTime值大于某个给定值。

到目前为止,我有以下查询:

1
2
3
4
5
6
SELECT * FROM"accounts"
WHERE (DATA @> '{"data": {"country":"UK" } }')
AND (data->'data' ? 'verified_at')
AND ((data->'data' ->> 'verified_at') IS NOT NULL)
AND (DATA ->>'lastUpdatedTime' > '2016-02-28T05:49:08.511846')
ORDER BY DATA ->>'lastUpdatedTime' LIMIT 100 OFFSET 0;

以下索引:

1
2
"accounts_idxgin" gin (DATA)
"accounts_idxgin_on_data" gin ((DATA -> 'data'::text))

我设法将查询时间缩短到大约1000到4000毫秒

以下是查询分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 Bitmap Heap Scan ON accounts  (cost=41.31..6934.50 ROWS=9 width=1719)
                               (actual TIME=7.273..1067.657 ROWS=23190 loops=1)
   Recheck Cond: ((DATA -> 'data'::text) ? 'verified_at'::text)
   FILTER: ((((DATA -> 'data'::text) ->> 'verified_at'::text) IS NOT NULL)
           AND ((DATA ->> 'lastUpdatedTime'::text) > '2016-02-01 05:49:08.511846'::text)
           AND (((DATA -> 'data'::text) ->> 'country'::text) = 'UK'::text))
   ROWS Removed BY FILTER: 4
   Heap Blocks: exact=16039
   ->  Bitmap INDEX Scan ON accounts_idxgin_on_data  (cost=0.00..41.30 ROWS=1773 width=0)
       (actual TIME=4.618..4.618 ROWS=23194 loops=1)
         INDEX Cond: ((DATA -> 'data'::text) ? 'verified_at'::text)
 Planning TIME: 0.448 ms
 Execution TIME: 1069.344 ms
(9 ROWS)

我有以下问题

  • 有什么我可以做的,以进一步加快这个查询?
  • 使用JSONB加速field is not null查询的正确方法是什么?我最终使用带有(data->'data' ? 'verified_at')的存在运算符来过滤掉大量不匹配的记录,因为我的大部分数据都没有verified_at作为顶级键。这提高了查询的速度,但我想知道是否有一种优化此类查询的通用方法。
  • 为了使用(data->'data' ? 'verified_at')的存在运算符,我需要在((data -> 'data'::text))上添加另一个索引。我已经在gin (data)上有一个索引,但是存在运算符没有使用它。这是为什么?我认为存在和包含运算符将使用此索引。

  • 3:不是。这个案例在文档中明确提到。
    如果列data上有索引,则仅在查询表时使用它,如data @> '...'data ? '...'。当表达式(data -> 'data')上有索引时,这些查询可以利用它:(data -> 'data') @> '...'(data -> 'data') ? '...'

    2:通常的jsonb索引在(jsonb_col -> '') is [not] null查询期间根本没有帮助。不幸的是,您也不能使用jsonb_col @> '{"":null}',因为JSON对象可能完全没有密钥。也不可能反向使用索引(对于is not null)。但可能有一招......

    1:不多。可能会有一些改进,但不要指望有巨大的性能优势。所以他们去了:

    您可以使用jsonb_path_ops运算符类而不是(默认值)jsonb_ops。这应该意味着性能稍有改进,但它们不能使用存在运算符(?)。但无论如何我们都不需要它。

    你有一个单一的,索引不友好的boolean类型表达式,这会减慢你的速度。值得庆幸的是,如果您只对true值感兴趣,可以在此处使用部分索引。

    所以,你的索引应该是这样的:

    1
    2
    3
    CREATE INDEX accounts_idxgin_on_data
      ON accounts USING gin ((DATA -> 'data') jsonb_path_ops)
      WHERE (DATA -> 'data' ->> 'verified_at') IS NOT NULL;

    使用此索引,您可以使用以下查询:

    1
    2
    3
    4
    5
    6
    SELECT   *
    FROM     accounts
    WHERE    (DATA -> 'data') @> '{"country":"UK"}'
    AND      (DATA -> 'data' ->> 'verified_at') IS NOT NULL
    AND      (DATA ->> 'lastUpdatedTime') > '2016-02-28T05:49:08.511Z'
    ORDER BY DATA ->>'lastUpdatedTime';

    注意:对于正确的timestamp比较,您应该使用(data ->> 'lastUpdatedTime')::timestamptz > '2016-02-28T05:49:08.511Z'

    http://rextester.com/QWUW41874


    在玩了一下之后,我设法通过创建以下部分索引将查询时间从大约1000ms减少到350ms:

    1
    2
    3
    4
    5
     CREATE INDEX index_accounts_partial_on_verified_at
     ON accounts ((data->'data'->'verified_at'))
     WHERE (data->'data'->>'verified_at') IS NOT NULL
     AND (data->'data' ? 'verified_at')
     AND (data->'data'->>'country' = 'UK');

    我能够对此索引中的某些值进行硬编码,例如country=UK,因为我只需要考虑此查询的UK帐户。我还能够删除((data->'data'))上的索引,即258MB,并将其替换为仅为1360 kB的部分索引!

    对于任何感兴趣的人,我从这里找到了构建部分JSONB索引的详细信息


    使用路径访问运算符可以更快地访问较低级别的对象:

    1
    2
    3
    4
    5
    SELECT * FROM"accounts"
    WHERE DATA #>> '{data, country}' = 'UK'
      AND DATA #>> '{data, verified_at}' IS NOT NULL
      AND DATA ->> 'lastUpdatedTime' > '2016-02-28T05:49:08.511846'
    ORDER BY DATA ->> 'lastUpdatedTime' LIMIT 100 OFFSET 0;

    索引仅适用于顶级键。因此,对于列data的索引,支持data @> [[key]]之类的查询。但是,对于data -> 'data' ? 'verified_at'上的查询,您需要data->'data'上的索引。

    还有两点:

    • 我不认为有必要测试verified_at的存在。如果它不在那里它只是出现为NULL,所以它被相同的测试捕获。
    • 如果JSON值正确且一致地格式化,则比较timestamp值的字符串表示可能有效。转换为timestamp以确保安全。