Cómo realizar operaciones de actualización en columnas de tipo JSONB en Postgres 9.4

132

Mirando a través de la documentación del tipo de datos Postgres 9.4 JSONB, no es inmediatamente obvio para mí cómo hacer actualizaciones en las columnas JSONB.

Documentación para tipos y funciones JSONB:

http://www.postgresql.org/docs/9.4/static/functions-json.html http://www.postgresql.org/docs/9.4/static/datatype-json.html

Como ejemplos, tengo esta estructura de tabla básica:

CREATE TABLE test(id serial, data jsonb);

Insertar es fácil, como en:

INSERT INTO test(data) values ('{"name": "my-name", "tags": ["tag1", "tag2"]}');

Ahora, ¿cómo actualizaría la columna 'datos'? Esta es una sintaxis no válida:

UPDATE test SET data->'name' = 'my-other-name' WHERE id = 1;

¿Está esto documentado en algún lugar obvio que me perdí? Gracias.

jvous
fuente

Respuestas:

32

Idealmente, no utiliza documentos JSON para datos estructurados y regulares que desea manipular dentro de una base de datos relacional. Utilice un diseño relacional normalizado en su lugar.

JSON está destinado principalmente a almacenar documentos completos que no necesitan ser manipulados dentro del RDBMS. Relacionado:

Actualizar una fila en Postgres siempre escribe una nueva versión de toda la fila. Ese es el principio básico del modelo MVCC de Postgres . Desde una perspectiva de rendimiento, apenas importa si cambia una sola pieza de datos dentro de un objeto JSON o todo: debe escribirse una nueva versión de la fila.

Por lo tanto, el consejo en el manual :

Los datos JSON están sujetos a las mismas consideraciones de control de concurrencia que cualquier otro tipo de datos cuando se almacenan en una tabla. Aunque es factible almacenar documentos grandes, tenga en cuenta que cualquier actualización adquiere un bloqueo de nivel de fila en toda la fila. Considere limitar los documentos JSON a un tamaño manejable para disminuir la contención de bloqueo entre las transacciones de actualización. Idealmente, los documentos JSON deberían representar cada uno un dato atómico que las reglas de negocio dictan, no se puede subdividir razonablemente en datums más pequeños que se puedan modificar de forma independiente.

La esencia de esto: para modificar cualquier cosa dentro de un objeto JSON, debe asignar un objeto modificado a la columna. Postgres proporciona medios limitados para construir y manipular jsondatos además de sus capacidades de almacenamiento. El arsenal de herramientas ha crecido sustancialmente con cada nueva versión desde la versión 9.2. Pero el principio permanece: siempre debe asignar un objeto modificado completo a la columna y Postgres siempre escribe una nueva versión de fila para cualquier actualización.

Algunas técnicas para trabajar con las herramientas de Postgres 9.3 o posterior:

Esta respuesta ha atraído tantos votos negativos como todas mis otras respuestas en SO juntas . A la gente no parece gustarle la idea: un diseño normalizado es superior para los datos no dinámicos. Esta excelente publicación de blog de Craig Ringer explica con más detalle:

Erwin Brandstetter
fuente
66
Esta respuesta solo se refiere al tipo JSON e ignora JSONB.
fiatjaf
77
@fiatjaf: esta respuesta es totalmente aplicable a los tipos de datos jsony jsonbsimilares. Ambos almacenan datos JSON, jsonblo hacen en una forma binaria normalizada que tiene algunas ventajas (y pocas desventajas). stackoverflow.com/a/10560761/939860 Ninguno de los dos tipos de datos es bueno para manipular mucho dentro de la base de datos. No hay tipo de documento. Bueno, está bien para documentos JSON pequeños, apenas estructurados. Pero los documentos anidados grandes serían una locura de esa manera.
Erwin Brandstetter
77
"Instrucciones sobre cómo trabajar con las herramientas de Postgres 9.3" realmente es lo primero en su respuesta, ya que responde a la pregunta formulada ... a veces tiene sentido actualizar json para cambios de mantenimiento / esquema, etc. y las razones para no actualizar json don realmente no se aplica
Michael Wasser
22
Esta respuesta no es útil, lo siento. @jvous, ¿no quieres aceptar la respuesta de Jimothy, ya que realmente responde a tu pregunta?
Bastian Voigt
10
Responda la pregunta primero antes de agregar su propio comentario / opinión / discusión.
Ppp
331

Si puede actualizar a Postgresql 9.5, el jsonb_setcomando está disponible, como han mencionado otros.

En cada una de las siguientes declaraciones SQL, he omitido la wherecláusula de brevedad; obviamente, querrás volver a agregar eso.

Nombre de actualización:

UPDATE test SET data = jsonb_set(data, '{name}', '"my-other-name"');

Reemplace las etiquetas (en lugar de agregar o quitar etiquetas):

UPDATE test SET data = jsonb_set(data, '{tags}', '["tag3", "tag4"]');

Sustitución de la segunda etiqueta (indexada a 0):

UPDATE test SET data = jsonb_set(data, '{tags,1}', '"tag5"');

Agregue una etiqueta ( esto funcionará siempre que haya menos de 999 etiquetas; cambiar el argumento 999 a 1000 o superior genera un error . Este ya no parece ser el caso en Postgres 9.5.3; se puede usar un índice mucho mayor) :

UPDATE test SET data = jsonb_set(data, '{tags,999999999}', '"tag6"', true);

Eliminar la última etiqueta:

UPDATE test SET data = data #- '{tags,-1}'

Actualización compleja (elimine la última etiqueta, inserte una nueva etiqueta y cambie el nombre):

UPDATE test SET data = jsonb_set(
    jsonb_set(data #- '{tags,-1}', '{tags,999999999}', '"tag3"', true), 
    '{name}', '"my-other-name"');

Es importante tener en cuenta que en cada uno de estos ejemplos, en realidad no está actualizando un solo campo de los datos JSON. En cambio, está creando una versión temporal y modificada de los datos, y está asignando esa versión modificada a la columna. En la práctica, el resultado debería ser el mismo, pero teniendo esto en cuenta debería hacer actualizaciones complejas, como el último ejemplo, más comprensibles.

En el ejemplo complejo, hay tres transformaciones y tres versiones temporales: Primero, se elimina la última etiqueta. Luego, esa versión se transforma al agregar una nueva etiqueta. A continuación, la segunda versión se transforma cambiando el namecampo. El valor en la datacolumna se reemplaza con la versión final.

Jimothy
fuente
42
obtienes puntos de bonificación por mostrar cómo actualizar una columna en una tabla como solicitó el OP
chadrik
1
@chadrik: agregué un ejemplo más complejo. No hace exactamente lo que solicitó, pero debería darle una idea. Tenga en cuenta que la entrada a la jsonb_setllamada externa es la salida de la llamada interna, y que la entrada a esa llamada interna es el resultado de data #- '{tags,-1}'. Es decir, los datos originales con la última etiqueta eliminada.
Jimothy
1
@PranaySoni: Para ese propósito, probablemente usaría un procedimiento almacenado o, si la sobrecarga no es una preocupación, recuperar esos datos, manipularlos en el lenguaje de la aplicación y luego escribirlos de nuevo. Esto suena pesado, pero tenga en cuenta que, en todos los ejemplos que di, todavía no está actualizando un solo campo en el JSON (B): está sobrescribiendo la columna completa de cualquier manera. Por lo tanto, un proceso almacenado no es realmente diferente.
Jimothy
1
@ Alex: Sí, un poco hack. Si lo digo {tags,0}, eso significaría "el primer elemento de la matriz tags", lo que me permite dar un nuevo valor a ese elemento. Al usar un número grande en lugar de 0, en lugar de reemplazar un elemento existente en la matriz, agrega un nuevo elemento a la matriz. Sin embargo, si la matriz en realidad tuviera más de 999,999,999 elementos, esto reemplazaría el último elemento en lugar de agregar uno nuevo.
Jimothy
1
¿Qué pasa si el campo contiene nulo? Parece que no funciona. Por ejemplo, el campo info jsonb es nulo: "UPDATE organizer SET info = jsonb_set (info, '{country}', '" FRA "') donde info - >> 'country' :: text IS NULL;" Obtengo ACTUALIZAR 105 registro pero sin cambios en db
stackdave
24

Esto viene en 9.5 en forma de jsonb_set por Andrew Dunstan basado en una extensión existente jsonbx que funciona con 9.4

philofinfinitejest
fuente
Otro problema en esta línea es el uso de jsonb_build_object(), porque x->key, no devuelve el par clave-objeto, para llenar lo que necesita jsonb_set(target, path, jsonb_build_object('key',x->key)).
Peter Krauss
18

Para aquellos que se encuentran con este problema y quieren una solución muy rápida (y están atascados en 9.4.5 o anterior), esto es lo que hice:

Creación de tabla de prueba.

CREATE TABLE test(id serial, data jsonb);
INSERT INTO test(data) values ('{"name": "my-name", "tags": ["tag1", "tag2"]}');

Actualización de la declaración para cambiar el nombre de la propiedad jsonb

UPDATE test 
SET data = replace(data::TEXT,'"name":','"my-other-name":')::jsonb 
WHERE id = 1;

En última instancia, la respuesta aceptada es correcta porque no puede modificar una pieza individual de un objeto jsonb (en 9.4.5 o anterior); sin embargo, puede convertir el objeto jsonb en una cadena (:: TEXT) y luego manipular la cadena y volver al objeto jsonb (:: jsonb).

Hay dos advertencias importantes.

  1. esto reemplazará todas las propiedades llamadas "nombre" en el json (en el caso de que tenga varias propiedades con el mismo nombre)
  2. esto no es tan eficiente como lo sería jsonb_set si usa 9.5

Dicho esto, me encontré con una situación en la que tenía que actualizar el esquema para el contenido de los objetos jsonb y esta era la forma más sencilla de lograr exactamente lo que pedía el póster original.

Chad Capra
fuente
1
Buen señor, he estado buscando cómo hacer una actualización de jsonb durante aproximadamente dos horas para poder reemplazar todos \u0000los caracteres nulos, el ejemplo muestra la imagen completa. ¡Gracias por esto!
Joshua Robinson
3
¡se ve bien! por cierto, el segundo argumento para reemplazar en su ejemplo incluye los dos puntos y el tercero no. Parece que su llamada debería serreplace(data::TEXT, '"name":', '"my-other-name":')::jsonb
davidicus
Gracias @davidicus! Perdón por la actualización muy retrasada, ¡pero agradezco que compartas con otros!
Chad Capra
12

Esta pregunta se hizo en el contexto de postgres 9.4, sin embargo, los nuevos espectadores que lleguen a esta pregunta deben tener en cuenta que en postgres 9.5, las operaciones de creación / actualización / eliminación de subdocumentos en campos JSONB son compatibles de forma nativa con la base de datos, sin necesidad de extensión funciones

Ver: JSONB modificando operadores y funciones

bguiz
fuente
7

actualizar el atributo 'nombre':

UPDATE test SET data=data||'{"name":"my-other-name"}' WHERE id = 1;

y si desea eliminar, por ejemplo, los atributos 'nombre' y 'etiquetas':

UPDATE test SET data=data-'{"name","tags"}'::text[] WHERE id = 1;
Arturo
fuente
5

Escribí una pequeña función para mí que funciona de forma recursiva en Postgres 9.4. Tuve el mismo problema (bueno, resolvieron algo de este dolor de cabeza en Postgres 9.5). De todos modos, aquí está la función (espero que funcione bien para usted):

CREATE OR REPLACE FUNCTION jsonb_update(val1 JSONB,val2 JSONB)
RETURNS JSONB AS $$
DECLARE
    result JSONB;
    v RECORD;
BEGIN
    IF jsonb_typeof(val2) = 'null'
    THEN 
        RETURN val1;
    END IF;

    result = val1;

    FOR v IN SELECT key, value FROM jsonb_each(val2) LOOP

        IF jsonb_typeof(val2->v.key) = 'object'
            THEN
                result = result || jsonb_build_object(v.key, jsonb_update(val1->v.key, val2->v.key));
            ELSE
                result = result || jsonb_build_object(v.key, v.value);
        END IF;
    END LOOP;

    RETURN result;
END;
$$ LANGUAGE plpgsql;

Aquí está el uso de muestra:

select jsonb_update('{"a":{"b":{"c":{"d":5,"dd":6},"cc":1}},"aaa":5}'::jsonb, '{"a":{"b":{"c":{"d":15}}},"aa":9}'::jsonb);
                            jsonb_update                             
---------------------------------------------------------------------
 {"a": {"b": {"c": {"d": 15, "dd": 6}, "cc": 1}}, "aa": 9, "aaa": 5}
(1 row)

Como puede ver, analice en profundidad y actualice / agregue valores donde sea necesario.

J. Raczkiewicz
fuente
Esto no funciona en 9.4, porque jsonb_build_objectse introdujo en 9.5
Greg
@ Greg Tienes razón, acabo de comprobar y ahora estoy ejecutando PostgreSQL 9.5, es por eso que funciona. Gracias por señalarlo: mi solución no funcionará en 9.4.
J. Raczkiewicz
4

Quizás: ACTUALIZAR prueba SET data = '"my-other-name"' :: json WHERE id = 1;

Funcionó con mi caso, donde los datos son de tipo json

Gianluigi Sartori
fuente
1
También funcionó para mí, en postgresql 9.4.5. Todo el registro se reescribe para que no se pueda actualizar un solo cajero automático de campo.
kometen