Bigquery: Extract data from an array of json - json

(This is an extension to this question, but my reputation is too low to comment or ask more questions on that topic...)
We work on bigquery, hence limited in importing packages or using other languages. And, as per the link above, js is a solution, but not what I'm looking for here. I implemented it in js, and it was too slow for our needs.
Suppose one of our columns is a string that look like this (array of json):
[{"location":[22.99902,66.000],"t":1},{"location":[55.32168,140.556],"t":2},{"location":[85.0002,20.0055],"t":3}]
I want to extract from the column the json for which "t":2
Where:
some columns don't have elements "t":2
Some columns have several elements "t":2
The number of json elements in each string can change
element "t":2 is not always in second position.
I don't know regexp well enough for this. We tried regexp_extract with this pattern: r'(\{.*?\"t\":2.*?\})')), but that doesn't work. It extracts everything that precedes "t":2, including the json for "t":2. We only want the json of element "t":2.
Could you advise a regexp pattern that would work?
EDIT:
I have a preference for a solution that gives me 1 match. Suppose I have this string:
[{"location":[22.99902,66.000],"t":1},{"location":[55.32168,140.556],"t":2},{"location":[55.33,141.785],"t":2}],
I would prefer receiving only 1 answer, the first one.
In that case perhaps regexp is less appropriate, but I'm really not sure?

How about this:
(?<=\{)(?=.*?\"t\"\s*:\s*2).*?(?=\})
As seen here

There is another solution but it is not regexp based (as I had originally asked). So this should not count as the final answer to my own question, nonetheless could be useful.
It is based on a split of the string in array and then chosing the element in the array that satisfies my needs.
Steps:
transform the string into something better for splits (using '|' as seperator):
replace(replace(replace(my_field,'},{','}|{'),'[{','{'),'}]','}')
split it using split(), which yields an array of strings (each one a json element)
find the relevant element ("t":2) - in my case, the first one is good enough, so I limit the query to 1: array( select data from unnest(split(replace(replace(replace(my_field,'},{','}|{'),'[{','{'),'}]','}'),'|')) as data where data like '%"t":2%' limit 1)
Convert that into a useable string with array_to_string() and use json_extract on that string to extract the relevant info from the element that I need (say for example, location coordinate x).
So putting it all together:
round(safe_cast(json_extract(array_to_string(array( select data from unnest(split(replace(replace(replace(my_field,'},{','}|{'),'[{','{'),'}]','}'),'|')) as data where data like '%"t":2%' limit 1),''),'$.location[0]') as float64),3) loc_x

May 1st, 2020 Update
A new function, JSON_EXTRACT_ARRAY, has been just added to the list of JSON
functions. This function allows you to extract the contents of a JSON document as
a string array.
so in below you can replace use of json2array UDF with just in-built function JSON_EXTRACT_ARRAY as in below example
#standardSQL
SELECT id,
(
SELECT x
FROM UNNEST(JSON_EXTRACT_ARRAY(json, '$')) x
WHERE JSON_EXTRACT_SCALAR(x, '$.t') = '2'
) extracted
FROM `project.dataset.table`
==============
Below is for BigQuery Standard SQL
#standardSQL
CREATE TEMP FUNCTION json2array(json STRING)
RETURNS ARRAY<STRING>
LANGUAGE js AS """
return JSON.parse(json).map(x=>JSON.stringify(x));
""";
SELECT id,
(
SELECT x
FROM UNNEST(json2array(JSON_EXTRACT(json, '$'))) x
WHERE JSON_EXTRACT_SCALAR(x, '$.t') = '2'
) extracted
FROM `project.dataset.table`
You can test, play with above using dummy data as in below example
#standardSQL
CREATE TEMP FUNCTION json2array(json STRING)
RETURNS ARRAY<STRING>
LANGUAGE js AS """
return JSON.parse(json).map(x=>JSON.stringify(x));
""";
WITH `project.dataset.table` AS (
SELECT 1 id, '[{"location":[22.99902,66.000],"t":1},{"location":[55.32168,140.556],"t":2},{"location":[85.0002,20.0055],"t":3}]' json UNION ALL
SELECT 2, '[{"location":[22.99902,66.000],"t":11},{"location":[85.0002,20.0055],"t":13}]'
)
SELECT id,
(
SELECT x
FROM UNNEST(json2array(JSON_EXTRACT(json, '$'))) x
WHERE JSON_EXTRACT_SCALAR(x, '$.t') = '2'
) extracted
FROM `project.dataset.table`
with output
Row id extracted
1 1 {"location":[55.32168,140.556],"t":2}
2 2 null
Above assumes that there is no more than one element with "t":2 in json column. In case if there can be more than one - you should add ARRAY as below
SELECT id,
ARRAY(
SELECT x
FROM UNNEST(json2array(JSON_EXTRACT(json, '$'))) x
WHERE JSON_EXTRACT_SCALAR(x, '$.t') = '2'
) extracted
FROM `project.dataset.table`

Even though, you have posted a work around your issue. I believe this answer will be informative. You mentioned that one of the answer selected more than what you needed, I wrote the query below to reproduce your case and achieve aimed output.
WITH
data AS (
SELECT
" [{ \"location\":[22.99902,66.000]\"t\":1},{\"location\":[55.32168,140.556],\"t\":2},{\"location\":[85.0002,20.0055],\"t\":3}] " AS string_j
UNION ALL
SELECT
" [{ \"location\":[22.99902,66.000]\"t\":1},{\"location\":[55.32168,140.556],\"t\":3},{\"location\":[85.0002,20.0055],\"t\":3}] " AS string_j
UNION ALL
SELECT
" [{ \"location\":[22.99902,66.000]\"t\":1},{\"location\":[55.32168,140.556],\"t\":3},{\"location\":[85.0002,20.0055],\"t\":3}] " AS string_j
UNION ALL
SELECT
" [{ \"location\":[22.99902,66.000]\"t\":1},{\"location\":[55.32168,140.556],\"t\":3},{\"location\":[85.0002,20.0055],\"t\":3}] " AS string_j ),
refined_data AS (
SELECT
REGEXP_EXTRACT(string_j, r"\{\"\w*\"\:\[\d*\.\d*\,\d*\.\d*\]\,\"t\"\:2\}") AS desired_field
FROM
data )
SELECT
*
FROM
refined_data
WHERE
desired_field IS NOT NULL
Notice that I have used the dummy described in the temp table, populated inside the WITH method. As below:
Afterwords, in the table refined_data, I used the REGEXP_EXTRACT to extract the desired string from the column. Observe that for the rows which there is not a match expression, the output is null. Thus, the table refined_data is as follows :
As you can see, now it is just needed a simple WHERE filter to obtain the desired output, which was done in the last select.
In addition you can see the information about the regex expression I provided here.

Related

MYSQL REGEXP_SUBSTR: get string or ip out of text

I struggle through a complex query and need help with a REGEXP_SUBSTR command being used in the WHERE CLAUSE:
In deed this are two questions.
given three possible records, I want to get the part between the brackets, but only if they match something that looks like an ip. In one case the question is simply between brackets like "[192.168.178.21]"
other case is the text is like "sender=<some192-168-178-12.example.here>"
Case 1:
SELECT REGEXP_SUBSTR('This is a sample of [192.168.178.12] containing relevant data','(?<=[ ]\[)[^\]]#]+') AS sender
SELECT REGEXP_SUBSTR('This is a sample of [dyn-192-168-178-12.example.com] containing relevant data','(?<=[ ]\[)[^\]]#]+') AS sender
SELECT REGEXP_SUBSTR('This is a sample of [only.example com] containing relevant data','(?<=[ ]\[)[^\]]#]+') AS sender
The problem above is that escaping does not work the way I've expected.
Expected would be:
127.0.0.1
dyn.127.0.0.1.example.com
NULL
Case 2:
If I search in the field using the WHERE CLAUSE I have limited success with
WHERE ( sndtext RLIKE ' sender=\\<.*[0-9]{1,3}(.|-|_)[0-9]{1,3}(.|-|_)[0-9]{1,3}(.|-|_)[0-9]{1,3}.*\\>')
but the match is too broad, it needs to stop at the '>'
How can I achieve both solution in a WHERE CLAUSE?
Might not be perfekt but my solution was (less escaping):
SELECT
REGEXP_SUBSTR(msgtext, '(?<=[[])[^]]+') AS ip,
REGEXP_SUBSTR(msgtext,'(?<=[ ]sndtext=<)[^>]+') AS txt
FROM txtmessage m
WHERE msgtext RLIKE ' sndtext=<.*([0-9]{1,3}[[:punct:]]){3}[0-9]{1,3}[^>]'
This works in the MySQL online tester if I'm understanding what you are looking for correctly.
with tbl(id, data) as (
select 1, 'This is a sample of [192.168.178.12] containing relevant data' from dual union all
select 2, 'This is a sample of [dyn-192-168-178-12.example.com] containing relevant data' from dual union all
select 3, 'This is a sample of [only.example com] containing relevant data' from dual union all
select 4, 'sender=<some192-168-178-12.example.here>' from dual
)
select id, data
from tbl
where ( data RLIKE '^.*(\\[|\\<).*[0-9]{1,3}(\\.|\\-)[0-9]{1,3}(\\.|\\-)[0-9]{1,3}(\\.|\\-)[0-9]{1,3}.*(\\]|\\>).*$');

How to find variable pattern in MySql with Regex?

I am trying to pull a product code from a long set of string formatted like a URL address. The pattern is always 3 letters followed by 3 or 4 numbers (ex. ???### or ???####). I have tried using REGEXP and LIKE syntax, but my results are off for both/I am not sure which operators to use.
The first select statement is close to trimming the URL to show just the code, but oftentimes will show a random string of numbers it may find in the URL string.
The second select statement is more rudimentary, but I am unsure which operators to use.
Which would be the quickest solution?
SELECT columnName, SUBSTR(columnName, LOCATE(columnName REGEXP "[^=\-][a-zA-Z]{3}[\d]{3,4}", columnName), LENGTH(columnName) - LOCATE(columnName REGEXP "[^=\-][a-zA-Z]{3}[\d]{3,4}", REVERSE(columnName))) AS extractedData FROM tableName
SELECT columnName FROM tableName WHERE columnName LIKE '%___###%' OR columnName LIKE '%___####%'
-- Will take a substring of this result as well
Example Data:
randomwebsite.com/3982356923abcd1ab?random_code=12480712_ABC_DEF_ANOTHER_CODE-xyz123&hello_world=us&etc_etc
In this case, the desired string is "xyz123" and the location of said pattern is variable based on each entry.
EDIT
SELECT column, LOCATE(column REGEXP "([a-zA-Z]{3}[0-9]{3,4}$)", column), SUBSTR(column, LOCATE(column REGEXP "([a-zA-Z]{3}[0-9]{3,4}$)", column), LENGTH(column) - LOCATE(column REGEXP "^.*[a-zA-Z]{3}[0-9]{3,4}", REVERSE(column))) AS extractData From mainTable
This expression is still not grabbing the right data, but I feel like it may get me closer.
I suggest using
REGEXP_SUBSTR(column, '(?<=[&?]random_code=[^&#]{0,256}-)[a-zA-Z]{3}[0-9]{3,4}(?![^&#])')
Details:
(?<=[&?]random_code=[^&#]{0,256}-) - immediately on the left, there must be & or &, random_code=, and then zero to 256 chars other than & and # followed with a - char
[a-zA-Z]{3} - three ASCII letters
[0-9]{3,4} - three to four ASCII digits
(?![^&#]) - that are followed either with &, # or end of string.
See the online demo:
WITH cte AS ( SELECT 'randomwebsite.com/3982356923abcd1ab?random_code=12480712_ABC_DEF_ANOTHER_CODE-xyz123&hello_world=us&etc_etc' val
UNION ALL
SELECT 'randomwebsite.com/3982356923abcd1ab?random_code=12480712_ABC_DEF_ANOTHER_CODE-xyz4567&hello_world=us&etc_etc'
UNION ALL
SELECT 'randomwebsite.com/3982356923abcd1ab?random_code=12480712_ABC_DEF_ANOTHER_CODE-xyz89&hello_world=us&etc_etc'
UNION ALL
SELECT 'randomwebsite.com/3982356923abcd1ab?random_code=12480712_ABC_DEF_ANOTHER_CODE-xyz00000&hello_world=us&etc_etc'
UNION ALL
SELECT 'randomwebsite.com/3982356923abcd1ab?random_code=12480712_ABC_DEF_ANOTHER_CODE-aaaaa11111&hello_world=us&etc_etc')
SELECT REGEXP_SUBSTR(val,'(?<=[&?]random_code=[^&#]{0,256}-)[a-zA-Z]{3}[0-9]{3,4}(?![^&#])') output
FROM cte
Output:
I'd make use of capture groups:
(?<=[=\-\\])([a-zA-Z]{3}[\d]{3,4})(?=[&])
I assume with [^=\-] you wanted to capture string with "-","\" or "=" in front but not include those chars in the result. To do that use "positive lookbehind" (?<=.
I also added a lookahead (?= for "&".
If you'd like to fidget more with regex I recommend RegExr

Postgres - search in json array of elements

My table:
id | name | open_days
Column open_days is a json column with following structure:
{
daysOfWeek: ['mon','tue','sat']
months: ['may','november']
}
I would like to find rows where daysOfWeek contain searching element. Something like
SELECT * FROM places WHERE :day=ANY(open_days->'daysOfWeek')
But above query is not correct.
Pleas help how to search is json array.
Thank you.
Assuming open_days is a jsonb column (which it should be), then you can use the contains operator ?:
select *
from places
where open_days -> 'daysOfWeek' ? :day
If it's a json column, you need to cast it open_days::jsonb -> ...
If you want to search for values that contain multiple weekdays, you can use the ?& operator:
select *
from places
where open_days -> 'daysOfWeek' ?& array['mon','tue']
the above would return all rows that contain mon and tue
One way is to use json_array_elements().
SELECT *
FROM places
WHERE :day = ANY (SELECT json_array_elements(open_days->'daysOfWeek')#>>'{}');
Yet another way to do this is with the containment operator.
select * from places where open_days #> '{"daysOfWeek":["mon"]}'
An advantage over using -> ... ? is that containment can use the default JSONB index, while the other would require a specialized index.

Searching Database with partial keyword

I'm trying to do a search of all the columns of a specific table and I want to return the result that contains certain characters. For example
Entered Search Value: "Josh"
Output Values: Josh, Joshua, Joshie, Rich Joshua
I want to return all values containing the characters Josh. I'm trying to use FreeTextTable however it only returns exact words like this
Entered Search Value: "Josh"
Output Values: Josh
I'm using the following code.
DECLARE #nameSearch NVARCHAR(100) = 'Josh';
SELECT MAX(KEY_TBL.RANK) as RANK, FT_TBL.ID
FROM Property FT_TBL
INNER JOIN (SELECT Rank, [KEY]
from FREETEXTTABLE(Property, *, #nameSearch)) AS KEY_TBL
ON FT_TBL.ID = KEY_TBL.[KEY]
GROUP BY FT_TBL.ID
I know this will be possible by using LIKE or CONTAINS but I have a lot of rows in that table and it would take time before it returns the result. So I need to use FreeTextTable to get the Rank and Key. However I can't achieve my goal here. I need help. Thanks!

Return jsonb_array_elements result as comma-separated list

I am accessing an array (a json object called 'choice_values') in a jsonb field, and would like to parse its contents into a comma-separated text field.
SELECT
jsonb_array_elements(doc -> 'form_values' -> '8189' -> 'choice_values')
FROM
field_data.exports;
That jsonb_array_elements function returns a "setof text", which I would like converted to a comma separated list of the array values, contained within a single field.
Thank you.
Set returning functions (like jsonb_array_elements_text()) can be called in SELECT list, but then they cannot be used in aggregate functions.
This is a good practice to call set returning functions in FROM clause, often in a lateral join like in this example:
with the_data as (
select '["alfa", "beta", "gamma"]'::jsonb as js
)
select string_agg(elem, ',')
from
the_data,
jsonb_array_elements_text(js) elem;
string_agg
-----------------
alfa,beta,gamma
(1 row)
So your query should look like this:
select string_agg(elem, ',')
from
field_data.exports,
jsonb_array_elements_text(doc -> 'form_values' -> '8189' -> 'choice_values') elem;
Using the string_agg aggregate function with a sub-select from jsonb_array_elements_text seems to work (tested on PG 9.5). Note the use of jsonb_array_elements_text, added in PostgreSQL 9.4, rather than jsonb_array_elements, from PostgreSQL 9.3.
with exports as (
select $${"form_values": {"8189": {"choice_values": ["a","b","c"]}}}$$::jsonb as doc
)
SELECT
string_agg(values, ', ')
FROM
exports, jsonb_array_elements_text(doc -> 'form_values' -> '8189' -> 'choice_values') values
GROUP BY
exports.doc;
Output:
'a, b, c'
Also see this question and its answers.
Maybe not best practice: convert the json array to text, then remove the brackets.
WITH input AS (
SELECT '["text1","text2","text3"]'::jsonb as data
)
SELECT substring(data::text,2,length(data::text)-2) FROM input
It has the advantage that it converts "in-place", not by aggregating. This could be handy if you can only access part of the query, e.g. for some synchronization tool where there's field-based conversion rules, or something like the following:
CREATE TEMP TABLE example AS (SELECT '["text1","text2","text3"]'::jsonb as data);
ALTER TABLE example ALTER COLUMN data TYPE text USING substring(data::text,2,length(data::text)-2);