-- JSON stores data as text (slower, larger)CREATETABLEjson_example(idSERIALPRIMARYKEY,dataJSONNOTNULL);-- JSONB stores data in binary format (faster, smaller)CREATETABLEjsonb_example(idSERIALPRIMARYKEY,dataJSONBNOTNULL);-- Insert sample dataINSERTINTOjson_example(data)VALUES('{"name": "John", "age": 30, "city": "New York"}');INSERTINTOjsonb_example(data)VALUES('{"name": "John", "age": 30, "city": "New York"}');-- Compare storage sizeSELECTpg_column_size(data)asjson_size,pg_column_size(data)asjsonb_sizeFROMjson_example,jsonb_example;
-- Create GIN index for full JSONB contentCREATEINDEXidx_user_profiles_ginONuser_profilesUSINGGIN(profile_data);-- Create GIN index for specific JSONB pathsCREATEINDEXidx_user_profiles_personal_ginONuser_profilesUSINGGIN((profile_data->'personal'));CREATEINDEXidx_user_profiles_preferences_ginONuser_profilesUSINGGIN((profile_data->'preferences'));-- Create GIN index for JSONB keysCREATEINDEXidx_user_profiles_keys_ginONuser_profilesUSINGGIN((profile_data?'personal'));-- Create GIN index for JSONB array elementsCREATEINDEXidx_user_profiles_tags_ginONuser_profilesUSINGGIN((profile_data->'tags'));
-- Create B-tree index for specific JSONB valuesCREATEINDEXidx_user_profiles_email_btreeONuser_profiles((profile_data->>'personal'->>'email'));CREATEINDEXidx_user_profiles_city_btreeONuser_profiles((profile_data->'address'->>'city'));CREATEINDEXidx_user_profiles_created_date_btreeONuser_profiles(((profile_data->>'created_date')::date));
-- Create composite index for complex queriesCREATEINDEXidx_user_profiles_city_emailONuser_profiles((profile_data->'address'->>'city'),(profile_data->>'personal'->>'email'));-- Create partial index for specific conditionsCREATEINDEXidx_user_profiles_active_usersONuser_profiles((profile_data->>'status'))WHERE(profile_data->>'status')='active';
-- Extract values using -> and ->>SELECTuser_id,profile_data->'personal'->>'first_name'asfirst_name,profile_data->'personal'->>'last_name'aslast_name,profile_data->>'personal'->>'email'asemailFROMuser_profiles;-- Check if key existsSELECTuser_id,profile_data?'social'ashas_social,profile_data->'personal'?'phone'ashas_phoneFROMuser_profiles;-- Check if any of multiple keys existSELECTuser_id,profile_data?|ARRAY['social','preferences']ashas_any,profile_data?&ARRAY['personal','address']ashas_allFROMuser_profiles;
-- Create validation function for user profile schemaCREATEORREPLACEFUNCTIONvalidate_user_profile(profile_dataJSONB)RETURNSBOOLEANAS$$BEGIN-- Check required fieldsIFNOT(profile_data?'personal'ANDprofile_data->'personal'?'first_name'ANDprofile_data->'personal'?'last_name'ANDprofile_data->'personal'?'email')THENRETURNFALSE;ENDIF;-- Validate email formatIFNOT(profile_data->>'personal'->>'email'~*'^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$')THENRETURNFALSE;ENDIF;-- Validate phone format if presentIFprofile_data->'personal'?'phone'THENIFNOT(profile_data->>'personal'->>'phone'~'^\+?[1-9]\d{1,14}$')THENRETURNFALSE;ENDIF;ENDIF;-- Validate date of birth if presentIFprofile_data->'personal'?'date_of_birth'THENBEGINPERFORM(profile_data->>'personal'->>'date_of_birth')::date;EXCEPTIONWHENOTHERSTHENRETURNFALSE;END;ENDIF;RETURNTRUE;END;$$LANGUAGEplpgsql;-- Create trigger to validate JSONB dataCREATEORREPLACEFUNCTIONvalidate_user_profile_trigger()RETURNSTRIGGERAS$$BEGINIFNOTvalidate_user_profile(NEW.profile_data)THENRAISEEXCEPTION'Invalid user profile data';ENDIF;RETURNNEW;END;$$LANGUAGEplpgsql;CREATETRIGGERvalidate_user_profile_triggerBEFOREINSERTORUPDATEONuser_profilesFOREACHROWEXECUTEFUNCTIONvalidate_user_profile_trigger();
-- Optimize JSONB queries with proper indexing-- Good: Use GIN index for containment queriesSELECTuser_id,profile_dataFROMuser_profilesWHEREprofile_data@>'{"preferences": {"theme": "dark"}}';-- Good: Use B-tree index for equality queriesSELECTuser_id,profile_dataFROMuser_profilesWHEREprofile_data->>'personal'->>'email'='john.doe@example.com';-- Good: Combine JSONB operations efficientlySELECTuser_id,profile_data->>'personal'->>'first_name'asfirst_name,profile_data->>'address'->>'city'ascityFROMuser_profilesWHEREprofile_data->'address'->>'state'='NY'ANDprofile_data->'preferences'->>'theme'='dark';-- Avoid: Inefficient JSONB operations-- Bad: Multiple JSONB operations in WHERE clauseSELECTuser_id,profile_dataFROMuser_profilesWHEREprofile_data->>'personal'->>'first_name'LIKE'J%'ANDprofile_data->>'personal'->>'last_name'LIKE'D%';
-- Create migration functionCREATEORREPLACEFUNCTIONmigrate_json_to_jsonb(source_tableTEXT,source_columnTEXT,target_tableTEXT,target_columnTEXT)RETURNSVOIDAS$$DECLAREbatch_sizeINTEGER:=1000;offset_valINTEGER:=0;total_rowsINTEGER;BEGIN-- Get total row countEXECUTEformat('SELECT COUNT(*) FROM %I',source_table)INTOtotal_rows;-- Migrate in batchesWHILEoffset_val<total_rowsLOOPEXECUTEformat(' INSERT INTO %I (%I) SELECT %I::jsonb FROM %I ORDER BY ctid LIMIT %s OFFSET %s ',target_table,target_column,source_column,source_table,batch_size,offset_val);offset_val:=offset_val+batch_size;RAISENOTICE'Migrated % rows of %',offset_val,total_rows;ENDLOOP;END;$$LANGUAGEplpgsql;-- Example migrationCREATETABLEuser_profiles_jsonbASSELECTuser_id,profile_data::jsonbasprofile_data,created_at,updated_atFROMuser_profiles_json;