Есть ли способ применить условное выражение к списку значений в объекте jsonb?

#sql #postgresql #kotlin

#sql #postgresql #kotlin

Вопрос:

В настоящее время у меня есть столбец jsonb в таблице PostgreSQL, в которой хранится объект json с форматом

 {
  "subscriptions": {
    "subscription1": {
      "subscribed": boolean
    },
    "subscription2": {
      "subscribed": boolean
    }
  }
}
  

В документе может быть любое количество подписок1 / 2 / и т.д., все с разными именами. Я пытаюсь выяснить, содержит ли документ subscription1.subscribed == true и т. Д. для каждого типа подписки, который у меня есть.

У меня есть список ключей подписки, которые мне нужно запросить, поэтому моим первым побуждением было перебрать их и запросить базу данных, чтобы получить эти данные:

 subscriptionsKeys.forEach { subscription ->
    // find number of users where `subscription` == true
    val queryResult = repository.getNumberOfUsersSubscribed(subscription)

    // queryResult contains the number of users subscribed to `subscription`
}
  

Где getNumberOfUsersSubscribed() определяется как:

 @SqlQuery(
    """
    SELECT count(*) as number_of_users FROM table
    WHERE jsonb_extract_path_text(body,'subscriptions',:subscriptionKey,'subscribed') = 'true'
    """
)
fun getNumberOfUsersSubscribedToOptOutList(
    subscriptionKey: String
): Int
  

Это работает нормально, но для большого количества подписок это означает, что я буду выполнять подсчет (*) в таблице n раз, что в нашей производственной среде составляет более 60 запросов, каждый из которых работает с более чем 45 миллионами записей.

Я надеюсь, что есть какой-то способ создать запрос SQL / jsonb, который позволит мне передать список / массив ключей [‘subscription1’,’subscription2’и т. Д.], Который применит указанное выше = true условное условие к списку входных данных для каждой строки, а затем вернет результаты в следующем примереформат:

  ------------------------------------ 
| subscription_key | number_of_users |
 ------------------ ----------------- 
| subscription1    | 6               |
| subscription2    | 59              |
| etc.             | n               |
 ------------------ ----------------- 
  

Любые советы будут оценены!

Ответ №1:

Пока ваша библиотека позволяет передавать массив в text[] заполнитель, это должно работать. Это будет не особенно быстро, но должно быть быстрее, чем метод, который у вас уже работает.

 with yourtable (id, body) as (
  values (1, '{
  "subscriptions": {
    "subscription1": {
      "subscribed": true
    },
    "subscription2": {
      "subscribed": true
    }
  }
}'::jsonb)
), search_terms as (
  select * 
    from unnest(array['subscription1', 'subscription2']) as st(term)
)
select st.term, count(*) 
  from yourtable y
       cross join lateral jsonb_each(y.body->'subscriptions') as b(k, v)
       join search_terms st 
         on st.term = b.k
        and b.v->>'subscribed' = 'true'
 group by st.term
;

┌───────────────┬───────┐
│     term      │ count │
├───────────────┼───────┤
│ subscription1 │     1 │
│ subscription2 │     1 │
└───────────────┴───────┘
(2 rows)

  

Согласно вашему комментарию, на случай, если вы хотите подсчитать подписки, которые явно не отображаются subscribed как false :

 with yourtable (id, body) as (
  values (1, '{
  "subscriptions": {
    "subscription1": {
      "subscribed": true
    },
    "subscription2": {
      "subscribed": true
    },
    "subscription3": {
      "subscribed": false
    }
  }
}'::jsonb),
        (2, '{
  "subscriptions": {
    "subscription1": {
      "subscribed": true
    },
    "subscription2": {
      "subscribed": true
    },
    "subscription3": {
      "subscribed": false
    }
  }
}'::jsonb),
        (3, '{}'::jsonb)
), search_terms as (
  select * 
    from unnest(array['subscription1', 'subscription2', 'subscription3', 'subscription4']) as st(term)
)
select st.term, 
       count(*) 
         filter (where 
           coalesce(
             body->'subscriptions'
               ->st.term 
               ->>'subscribed', ''
           ) != 'false')
  from yourtable t
       cross join search_terms st
 group by st.term
 order by st.term
;

┌───────────────┬───────┐
│     term      │ count │
├───────────────┼───────┤
│ subscription1 │     3 │
│ subscription2 │     3 │
│ subscription3 │     1 │
│ subscription4 │     3 │
└───────────────┴───────┘
(4 rows)

  

Комментарии:

1. Потрясающе! Кажется, это отлично работает. Мне любопытно, знаете ли вы также, как его изменить, где subscription1 / subscription2, отсутствующие в документе, также будут применяться к счетчику? Другими словами, если subscription1.subscribed = true или если subscription1 отсутствовало полностью, оба должны добавляться к подсчету? Я пытаюсь разобраться в этом сам, но этот уровень SQL немного чужд мне. Ценю ваш ответ!

2. Я опробовал редактирование и обнаружил две проблемы: во-первых, хотя проверка на null работает, когда в таблице есть одна запись, Если вы добавляете вторую запись (или третью и т. Д.), Количество нулей никогда не увеличивается. Например, если вы скопируете values with id=2 и запустите свой запрос, subscription4 по-прежнему будет равен 1 (где должно быть 2). Во-вторых, если subscriptions узел полностью отсутствует (поэтому примерное значение для jsonb было бы просто {} , то есть не обрабатывается как true / counted . Я играю с этим, чтобы попытаться исправить это самостоятельно, но если вы хотите помочь, я тоже ценю это!

3. @Josh Этот код с левым соединением был ошибочным — извините за это. Я заменил его упрощенным запросом, который может обрабатывать условия отсутствия ключей.