Call to_json on multiple columns using Postgres - json

Say I have the following table schema in Postgres:
CREATE TABLE users (id text, email text, phone_number text);
And I, for whatever reason, want to select the email and the phone number as JSON:
SELECT to_json(users.email, users.phone_number) AS user FROM users WHERE id=usr_123;
I get an error that looks like this:
function to_json(text, text) does not exist
No function matches the given name and argument types. You might need to add explicit type casts.
But this works just fine:
SELECT to_json(users.*) AS user FROM users WHERE id=usr_123;
How can I select just a few columns (not all of them) using a to_json call in Postgres?

The fine manual says that to_json's signature is:
to_json(anyelement)
so you're supposed say to_json(one_single_value). When you say:
to_json(users.email, users.phone_number)
you're trying to call to_json with two values and there is no such to_json function. When you say:
to_json(user.*)
you're actually calling to_json with a single ROW argument so it works just fine.
You can use a derived table or CTE as klin suggests or you can build the ROWs by hand:
select to_json(row(users.email, users.phone_number)) ...
The problem with this is that that ROW won't have any column names so your JSON will use useless keys like "f1" and "f2". To get around that you need to cast the ROW to something that does have names. One way to get some names is to create a custom type:
create type email_and_phone_number as (
email text,
phone_number text
)
and then cast your ROW to that type:
select to_json(row(users.email, users.phone_number)::email_and_phone_number) ...
You could also use a temporary table instead of a custom type:
create temporary table email_and_phone_number (
email text,
phone_number text
)
and then use the same cast as with a custom type.
If you're building this specific JSON format a lot then a custom type would make sense. If this is a one-off then a temporary table would make sense, the temporary table will automatically disappear when the session ends so there's nothing to clean up. Of course, a derived table or CTE might also make sense depending on the query and what tools you're using to interface with your database.

Use a subquery, e.g.:
select to_json(sub)
from (
select email, phone_number
from users
where id = 'usr_123'
) sub;
or with query:
with cte as (
select email, phone_number
from users
where id = 'usr_123')
select to_json(cte)
from cte;

If you just want to remove one specific field from the result there is an elegant solution:
SELECT to_json(users)::jsonb - 'id' AS user FROM users WHERE id=usr_123;

Related

Snowflake latteral flatten data types

I have a table containing an id column and a json column(variant data type). I want to flatten the data, make the value column a variant, assign each value in the value column a data type if a condition is met, then eventually pivot the data and have each column be the correct data type.
Example code that doesn't work:
with cte as (
select
1 as id,
parse_json('{
"field1":"TRUE",
"field2":"some string",
"field3":"1.035",
"field4":"097334"
}') as my_output
)
select
id,
key,
to_variant(
case
when value in ('true', 'false') then value::boolean
when value like ('1.0') then value::decimal
else value::string
end) as value
from cte, lateral flatten(my_output)
Ultimately, I'd like to pivot the data and have a wide table with columns id, field1, field2, etc. where field1 is boolean, field2 is string, field3 is a decimal etc.
This is just a simple example, instead of 4 fields, I'm dealing with hundreds.
Is this possible?
For the pivot, I'm using dbt_utils.get_column_values to get the column names dynamically. I'd really prefer a solution that doesn't involve listing out the column names, especially since there are hundreds.
Since you'd have to define each column in your PIVOT statement, anyway, it'd probably be much easier to simply select each attribute directly and cast to the correct data type, rather than using a lateral flatten.
select
my_output.field1::boolean,
my_output.field2::string,
my_output.field3::decimal(5,3),
my_output.field4::string
from cte;
Alternatively, if you want this to be dynamically created, you could create a stored procedure that dynamically uses your json to create a view over your table that has this select in it.
Solution ended up being
select
id,
key,
ifnull(try_parse_json(value), value) as value_mod,
typeof(value_mod)
from cte, lateral flatten(my_output)
Leading zeros are removed so things like zip codes have to be accounted for.

How to extract a value from JSON that repeats multiple times?

I have the following table:
I need to create a select that returns me something like this:
I have tried this code:
SELECT Code, json_extract_path(Registers::json,'sales', 'name')
FROM tbl_registers
The previous code returns me a NULL in json_extract_path, I have tried the operator ::json->'sales'->>'name', but doesn't work too.
You need to unnest the array, and the aggregate the names back. This can be done using json_array_elements with a scalar sub-query:
select code,
(select string_agg(e ->> 'name', ',')
from json_array_elements(t.products) as x(e)) as products
from tbl_registers t;
I would also strongly recommend to change your column's type to jsonb
step-by-step demo:db<>fiddle
SELECT
code,
string_agg( -- 3
elems ->> 'name', -- 2
','
) as products
FROM tbl_registers,
json_array_elements(products::json) as elems -- 1
GROUP BY code
If you have type text (strictly not recommended, please use appropriate data type json or jsonb), then you need to cast it into type json (I guess you have type text because you do the cast already in your example code). Afterwards you need to extract the array elements into one row per element
Fetch the name value
Reaggregate by grouping and use string_agg() to create the string list

Possible to use IF in a query?

I'm using Grafana to plot data from a MySQL datasource. Is it possible to, in a panel's query editor, use an IF ... THEN ... type statement. I would like to create a variable that I could put in the IF. I want the variable to be a condition, not necessarily to be used directly in the query.
For example:
//IN THE DATA SOURCE:
CREATE TABLE Example (Id INT, ANIMALS VARCHAR(15));
INSERT INTO Example VALUES (1,'Dog'), (2,'Fish'), (3,'Cat'), (4,'Lizard')
For a variable Test with values "Mammal',"Reptile", "Other":
//WHAT I'D LIKE IN GRAFANA QUERY EDITOR:
IF($Test = "Mammal") THEN
SELECT * FROM Example WHERE Id = 1 OR Id =3;
ELSE
SELECT * FROM Example WHERE Id = 2 OR Id =4;
END IF;
Is this kind of condition based query even possible? If so, what is the proper syntax to get it to work? Is there any way I can use Grafana variables to have a similar effect?
Use query. Query starts with SELECT keyword. Don't use any IF ELSE conditions before query, e.g.:
SELECT *
FROM Example
WHERE
Data IN ( ${variable:csv} )
This WHERE condition syntax will work with single value, multi value Grafana dashboard variables and also with All value (no custom All value, but blank=auto). Of course this condition is mainly for INT column types. STRING types may need different one (e.g. with LIKE and regexp matching).
Code all your logic (dependency on the dashboard variable) in the WHERE section. Use query inspector to see SQL which is generated and tweak it to correct SQL syntax.
Instead of an if, you can use or. It's really useful for conditionally checking variables:
select * from Example
where (Id in (1,3) or '$Test' != 'Mammal')
and (Id in (2,4) or '$Test' == 'Mammal')

Retrieve json value into columns when values are not named

I have a column named Username (varchar max, allows nulls) in a table. The table may have multiple entries that are all stored in this column. It is currently displayed in this format:
["name1","name2"]
I am trying to pull out name1 and name2 into separate rows. I cannot figure out how to do so. Here is the query that I currently have that is closest:
select u.id, a.[Username]
from dbo.Users u
CROSS APPLY OPENJSON(Username) WITH ([Username] varchar(max)) a
This is pulling them out in rows correctly but the rows display NULL instead of the value. I checked that the name matches the column name in syntax and it does.
The OPENJSON function is looking for a column named 'Username'. Instead, try giving OPENJSON 'oj' an alias and opening the JSON without specifying a schema. Then add oj.value to the SELECT list. Something like this
select u.id, oj.[value] as Username
from dbo.Users u
cross apply openjson(u.Username) oj;
The reason for the unexpected results is explained in the documentation: "...OPENJSON matches keys in jsonExpression with the column names in with_clause (in this case, matches keys implies that it is case sensitive). If a column name does not match a key name, you can provide an optional column_path...". Your input JSON is an array of scalar values and there isn't a key "username".
But, you can use OPENJSON() with explicit schema (the WITH clause). Just use the correct path ('$' in your case).
Table:
CREATE TABLE Users (id int, username varchar(max))
INSERT INTO Users (id, username) VALUES (1, '["name1","name2"]')
Statement:
SELECT u.id, a.[username]
FROM Users u
CROSS APPLY OPENJSON (u.[username]) WITH ([username] varchar(max) '$') a
Result:
id username
1 name1
1 name2

How to use a mysql variable from one table as a subquery in another, but casting it as a string (from int)

This is the query i'm trying to run:
UPDATE files set refcount=
(
SELECT count(*)
FROM comments WHERE data=files.id
)
WHERE id=?;
The problem is, comments.data is a text column (for other reasons). So I need to cast files.id as a STRING instead of what it is (an INT), because otherwise the comments.data index won't be used.
For example, this query runs fine:
SELECT count(*) FROM comments WHERE data='1234';
But this one takes forever (because it cannot use the index, comments has 10M rows):
SELECT count(*) FROM comments WHERE data=1234;
Perhaps I need to use #vars or something? I tried putting the thing in quotes, but that uses the literal "files.id" i think.
UPDATE files set refcount=
(
SELECT count(*)
FROM comments WHERE data='files.id'
)
WHERE id=?;
All you have to do is to cast files.id into string before comparing it to data
something like this :
UPDATE files set refcount=
(
SELECT count(*)
FROM comments WHERE data=CAST(files.id AS vachar)
)
WHERE id=?;
Here's a link that show how you can use cast functions and operators in mysql.
UPDATED: It seems that for some reasons CAST is not working with varchar.Though char might do the trick (whih in case it doesn't as Timh said in the comments below) CONCAT can be used to convert other types to a varchar (when you concat othery types with a string it returns a string and concating with an empty string will act as some sort of conversion :) )