在 Postgresql 的 WHERE 中使用 JSON 数组查询

Query with JSON array in WHERE in Postgresql

我有一个包含 payment_info JSONB 列的用户模型,其中包含以下 json 示例:

{
    "customer_id": "cst_K5gCsCkKAU",
    "subscriptions": [
        {
            "status": "active",
            "external_id": "sub_3Q9Q4bP2zW"
        }
    ]
}

我是一个有 JSON 查询的新手,但针对似乎有效的 Postgres (PG) 数据库创建了以下内容,即:我搜索具有特定 external_id 的所有用户价值:

SELECT payment_info->'subscriptions' as Subscriptions
    FROM public."user"
    , jsonb_array_elements(payment_info->'subscriptions') as subs
    where (subs->>'external_id')::text = 'sub_3Q9Q4bP2zW'

如何在 SQLAlchemy 中执行相同的操作?我尝试了几种我在网上找到的想法 (SO),但它不起作用。我试过了:

  1. JSONB Comparator

    query = misc.setup_query(db_session, User).filter(
        User.payment_info.comparator.contains(
            ('subscriptions', 'external_id') == payment_subscription_id))
    

    这会导致以下错误:

    sqlalchemy.exc.ProgrammingError: (psycopg2.ProgrammingError) operator does not exist: jsonb @> boolean
    LINE 3: WHERE "user".payment_info @> false
                                  ^
    HINT:  No operator matches the given name and argument type(s). You might need to add explicit type casts.
    
  2. json_contains函数:

    from sqlalchemy import func
    query = misc.setup_query(db_session, User).filter(
        func.json_contains(User.payment_info,
                           payment_subscription_id,
                           ['subscriptions', 'external_id']))
    

    这导致:

    LINE 3: WHERE json_contains("user".payment_info, 'sub_QxyMEmU', ARRA...
               ^
    HINT:  No function matches the given name and argument types. You might need to add explicit type casts.
    
  3. 密钥路径:

    query = misc.setup_query(db_session, User).filter(
        User.payment_info['subscriptions', 'external_id'].astext == payment_subscription_id)
    

    结果为空,查询如下:

    SELECT *
    FROM "user" 
    WHERE ("user".payment_info #>> %(payment_info_1)s) = %(param_1)s
    

我做错了什么,我该如何让它发挥作用?顺便说一句:我需要在 external_id 上添加索引吗? (尚未出现)

您几乎可以使用 aliased function expression:

来实现您原来的方法
misc.setup_query(db_session, User).\
    select_from(
        User,
        func.jsonb_array_elements(User.payment_info['subscriptions']).
            alias('subs')).\
    filter(column('subs', type_=JSONB)['external_id'].astext == 'sub_3Q9Q4bP2zW')

编译为

SELECT "user".id AS user_id, "user".payment_info AS user_payment_info 
FROM "user", jsonb_array_elements("user".payment_info -> %(payment_info_1)s) AS subs 
WHERE (subs ->> %(subs_1)s) = %(param_1)s

另一方面,您可以使用 containment operator:

misc.setup_query(db_session, User).\
    filter(User.payment_info['subscriptions'].contains(
        [{'external_id': 'sub_3Q9Q4bP2zW'}]))

请注意,最外面的列表是必需的,因为它是要检查的“路径”的一部分。使用相同的逻辑,您可以省略提取数组:

misc.setup_query(db_session, User).\
    filter(User.payment_info.contains(
        {'subscriptions': [{'external_id': 'sub_3Q9Q4bP2zW'}]}))

上述 @> 使用方法可使用 GIN index 进行索引。第一个需要一个函数索引,因为它首先提取数组:

CREATE INDEX user_payment_info_subscriptions_idx ON "user"
USING GIN ((payment_info -> 'subscriptions'));

第二个需要索引整个 payment_info jsonb 列。创建 GIN 索引可以在 SQLAlchemy 模型定义中完成 Postgresql-specific index options:

class User(Base):
    ...

Index('user_payment_info_subscriptions_idx',
      User.payment_info['subscriptions'],
      postgresql_using='gin')

至于为什么各种尝试都不成功:

  1. 您不应该直接访问比较器。它提供了类型的操作符。此外,您还传递 contains() 表达式

    的结果
     ('subscriptions', 'external_id') == payment_subscription_id
    

    最有可能是错误的(取决于 payment_subscription_id 是什么)。也就是说在Python.

    中求值
  2. There is no json_contains() function 在 Postgresql 中(不同于 MySQL)。使用 @> 运算符,或 SQL/JSON 路径函数,例如 jsonb_path_exists().

  3. 你走错路了。 User.payment_info['subscriptions', 'external_id'].astext 会匹配 {"subscriptions": {"external_id": "foo"}} 之类的内容,但在您的数据中 subscriptions 引用了一个数组。