Multiple properties on root level when nesting FOR JSON PATH? - json

Running SQL Server 2016. Consider the sample below. Nesting FOR JSON PATH is easy as long as you give each query an alias. In my case, I want many (but not all) properties to belong to the root - i.e. no alias!
With unwanted alias a:
DECLARE #SomeID int = 1
SELECT
(SELECT TOP 1 ID, A1, A2 FROM A WHERE ID = #SomeID
FOR JSON PATH) AS 'a', -- Unwanted!
(SELECT TOP 1 ID, B1, B2 FROM B WHERE ID = #SomeID
FOR JSON PATH, WITHOUT_ARRAY_WRAPPER) AS 'b'
FOR JSON PATH, WITHOUT_ARRAY_WRAPPER
If you remove the alias, you get this error when running the query:
Column expressions and data sources without names or aliases cannot be
formatted as JSON text using FOR JSON clause. Add alias to the unnamed
column or table.
No alias. Repetitive queries:
SELECT
-- Wanted! But tedious for more complex queries...
(SELECT TOP 1 ID FROM A WHERE ID = #SomeID) AS 'id',
(SELECT TOP 1 A1 FROM A WHERE ID = #SomeID) AS 'a1',
(SELECT TOP 1 A2 FROM A WHERE ID = #SomeID) AS 'a2',
(SELECT TOP 1 ID, B1, B2 FROM B WHERE ID = #SomeID
FOR JSON PATH, WITHOUT_ARRAY_WRAPPER) AS 'b'
FOR JSON PATH, WITHOUT_ARRAY_WRAPPER
The latter produces the right JSON. However, in my complex database I cannot repeat the statements like that. Hence, I need a better construct to put many properties on the root - without an alias. How can this be achieved?
(For completeness. Script to create sample tables below.)
CREATE TABLE A(ID int, A1 int, A2 int)
GO
INSERT INTO A(ID, A1, A2)
SELECT 1, 0, 0
UNION
SELECT 1, 1, 1
CREATE TABLE B(ID int, B1 int, B2 int)
GO
INSERT INTO B(ID, B1, B2)
SELECT 1, 100, 100
UNION
SELECT 1, 101, 101

This should produce the JSON you are after, without repeating the queries.
select
top 1
id,
a1,
a2,
(SELECT TOP 1 ID, B1, B2 FROM #B WHERE ID = #SomeID FOR JSON PATH, WITHOUT_ARRAY_WRAPPER) AS 'b'
from a
where id = #someid
for json path, without_array_wrapper

Related

Get bottom-up nested json for query in postgresql

Given the following table, I want to find a category by ID, then get a JSON object containing its parent row as JSON. if I look up category ID 999 I would like the following json structure.
How can I achieve this?
{
id: 999
name: "Sprinting",
slug: "sprinting",
description: "sprinting is fast running",
parent: {
id: 2
name: "Running",
slug: "running ",
description: "All plans related to running.",
parent: {
id: 1
name: "Sport",
slug: "sport ",
description: null,
}
}
}
CREATE TABLE public.categories (
id integer NOT NULL,
name text NOT NULL,
description text,
slug text NOT NULL,
parent_id integer
);
INSERT INTO public.categories (id, name, description, slug, parent_id) VALUES (1, 'Sport', NULL, 'sport', NULL);
INSERT INTO public.categories (id, name, description, slug, parent_id) VALUES (2, 'Running', 'All plans related to running.', 'running', 1);
INSERT INTO public.categories (id, name, description, slug, parent_id) VALUES (999, 'Sprinting', 'sprinting is fast running', 'sprinting', 2);```
demo:db<>fiddle
(Explanation below)
WITH RECURSIVE hierarchy AS (
SELECT id, parent_id
FROM categories
WHERE id = 999
UNION
SELECT
c.id, c.parent_id
FROM categories c
JOIN hierarchy h ON h.parent_id = c.id
),
jsonbuilder AS (
SELECT
c.id,
h.parent_id,
jsonb_build_object('id', c.id, 'name', c.name, 'description', c.description, 'slug', c.slug) as jsondata
FROM hierarchy h
JOIN categories c ON c.id = h.id
WHERE h.parent_id IS NULL
UNION
SELECT
c.id,
h.parent_id,
jsonb_build_object('id', c.id, 'name', c.name, 'description', c.description, 'slug', c.slug, 'parent', j.jsondata)
FROM hierarchy h
JOIN categories c ON c.id = h.id
JOIN jsonbuilder j ON j.id = h.parent_id
)
SELECT
jsondata
FROM jsonbuilder
WHERE id = 999
Generally you need a recursive query to create nested JSON objects. The naive approach is:
Get record with id = 999, create a JSON object
Get record with id = parent_id of record with 999 (id = 2), build JSON object, add this als parent attribute to previous object.
Repeat step 2 until parent is NULL
Unfortunately I saw no simple way to add a nested parent. Each step nests the JSON into deep. Yes, I am sure, there is a way to do this, storing a path of parents and use jsonb_set() everytime. This could work.
On the other hand, it's much simpler to put the currently created JSON object into a new one. So to speak, the approach is to build the JSON from the deepest level. In order to do this, you need the parent path as well. But instead create and store it while creating the JSON object, you could create it first with a separate recursive query:
WITH RECURSIVE hierarchy AS (
SELECT id, parent_id
FROM categories
WHERE id = 999
UNION
SELECT
c.id, c.parent_id
FROM categories c
JOIN hierarchy h ON h.parent_id = c.id
)
SELECT * FROM hierarchy
Fetching the record with id = 999 and its parent. Afterwards fetch the record of the parent, its id and its parent_id. Do this until parent_id is NULL.
This yields:
id | parent_id
--: | --------:
999 | 2
2 | 1
1 | null
Now we have a simple mapping list which shows the traversal tree. What is the difference to our original data? If your data contained two or more children for record with id = 1, we would not know which child we have to take to finally reach child 999. However, this result lists exactly only the anchestor relations and would not return any siblings.
Well having this, we are able to traverse the tree from the topmost element which can be embedded at the deepest level:
Fetch the record which has no parent. Create a JSON object from its data.
Fetch the child of the previous record. Create a JSON object from its data and embed the previous JSON data as parent.
Continue until there is no child.
How does it work?
This query uses recursive CTEs. The first part is the initial query, the first record, so to speak. The second part, the part after UNION, is the recursive part which usually references to the WITH clause itself. This is always a reference to the previous turn.
The JSON part is simply creating a JSON object using jsonb_build_object() which takes an arbitrary number of values. So we can use the current record data and additionally for the parent attribute the already created JSON data from the previous turn.

Sql select where array in column

In my query I use join table category_attributes. Let's assume we have such rows:
category_id|attribute_id
1|1
1|2
1|3
I want to have the query which suites the two following needs. I have a variable (php) of allowed attribute_id's. If the array is subset of attribute_id then category_id should be selected, if not - no results.
First case:
select * from category_attributes where (1,2,3,4) in category_attributes.attribute_id
should give no results.
Second case
select * from category_attributes where (1,2,3) in category_attributes.attribute_id
should give all three rows (see dummy rows at the beginning).
So I would like to have reverse side of what standard SQL in does.
Solution
Step 1: Group the data by the field you want to check.
Step 2: Left join the list of required values with the records obtained in the previous step.
Step 3: Now we have a list with required values and corresponding values from the table. The second column will be equal to required value if it exist in the table and NULL otherwise.
Count null values in the right column. If it is equal to 0, then it means table contains all the required values. In that case return all records from the table. Otherwise there must be at least one required value is missing in the table. So, return no records.
Sample
Table "Data":
Required values:
10, 20, 50
Query:
SELECT *
FROM Data
WHERE (SELECT Count(*)
FROM (SELECT D.value
FROM (SELECT 10 AS value
UNION
SELECT 20 AS value
UNION
SELECT 50 AS value) T
LEFT JOIN (SELECT value
FROM Data
GROUP BY value) D
ON ( T.value = D.value )) J
WHERE value IS NULL) = 0;
You can use group by and having:
select ca.category_id
from category_attributes ca
where ca.attribute_id in (1, 2, 3, 4)
group by ca.category_id
having count(*) = 4; -- "4" is the size of the list
This assumes that the table has no duplicates (which is typical for attribute mapping tables). If that is a possibility, use:
having count(distinct ca.attribute_id) = 4
You can aggregate attribute_id into array and compare two array from php.
SELECT category_id FROM
(select category_id, group_concat(attribute_id) as attributes from category_attributes
order by attribute_id) t WHERE t.attributes = (1, 2, 3);
But you need to find another way to compare arrays or make sure that array is always sorted.

Convert Excel Rows Into SQL Columns

I have a client that inherited an Excel spreadsheet full of contact information for a mailing list. Apparently, his predecessor attempted to format the spreadsheet for printing pin-feed mailing labels. It uses one column, and has the address info in separate rows:
Name
Street Address
City, ST ZIP
Name
Street Address
City, ST ZIP
Name
Street Address
City, ST ZIP
What I need to do is transpose the data into columns, with separate columns for City, ST, and ZIP (for carrier route sorting). My initial thought was to use Pivot, but I couldn't seem to get that to work.
There are 1,800+ names in this mailing list, so manually fixing it is not an option. I also had an idea to export them as a CSV, and then use a series of find and replace operations to replace line breaks with commas, but that seems bush league. There has to be a proper data solution.
Ideas?
Thanks,
ty
This is my solution. It is based on the assumption that the structure that you've described is fixed and consistent.
It is in written T-SQL (SQL Server). You shouldn't have any problem converting it to my-sql.
I am against the vba solution. SQL is powerful enough.
If you have hard time implementing this, I have a few more ideas. (I'm just not sure what's available in my-sql and have to check it out). Any way - It should work.
--DROP TABLE Example_Table;
CREATE TABLE Example_Table
(
Val VARCHAR(1000),
Row_Num INT IDENTITY(1,1)
);
INSERT INTO Example_Table
SELECT 'John' AS Val UNION ALL
SELECT 'Elm St.' AS Val UNION ALL
SELECT 'Tel Aviv, 151515' AS Val UNION ALL
SELECT 'Doe' AS Val UNION ALL
SELECT 'Manhatten St.' AS Val UNION ALL
SELECT 'Jerusalem, 344343' AS Val UNION ALL
SELECT 'Fox' AS Val UNION ALL
SELECT 'Mulder St.' AS Val UNION ALL
SELECT 'San Francisco, 3243424' AS Val UNION ALL
SELECT 'Jean Lic' AS Val UNION ALL
SELECT 'Picard St.' AS Val UNION ALL
SELECT 'Enterprise City, 3904734' AS Val;
SELECT Name_Details.Val AS Name
, Street_Details.Val AS Street
, SUBSTRING(City_Details.Val, 1, CHARINDEX(',', City_Details.Val, 0) - 1) AS City
, SUBSTRING(City_Details.Val, CHARINDEX(',', City_Details.Val, 0) + 2, LEN(City_Details.Val) - CHARINDEX(',', City_Details.Val, 0)) AS Zip
, City_Details.Val AS City_And_Zip
FROM Example_Table AS Name_Details
INNER JOIN
(SELECT VAL, Row_Num FROM Example_Table AS E WHERE Row_Num % 3 = 2) AS Street_Details
ON (Street_Details.Row_Num = Name_Details.Row_Num + 1)
INNER JOIN
(SELECT VAL, Row_Num FROM Example_Table AS E WHERE Row_Num % 3 = 0) AS City_Details
ON (City_Details.Row_Num = Name_Details.Row_Num + 2)
WHERE Name_Details.Row_Num % 3 = 1 ;
DROP TABLE Example_Table;

Searching for data in SQL

Please take a look at the following table:
I am building a search engine which returns card_id values, based on search of category_id and value_id values.
To better explain the search mechanism, imagine that we are trying to find a car (card_id) by supplying information what part (value_id) the car should has in every category (category_id).
In example, we may want to find a car (card_id), where category "Fuel Type" (category_id) has a value "Diesel" (value_id), and category "Gearbox" (category_id) has a value "Manual" (value_id).
My problem is that my knowledge is not sufficient to build a query, which will returns card_ids which contains more than one pair of category_id and value_id.
For example, if I want to search a car with diesel engine, I could build a query like this:
SELECT card_id FROM cars WHERE category_id=1 AND value_id=2
where category_id = 1 is a category "Fuel Type" and value_id = 2 is "Diesel".
My question is, how can I build a query, which will look for more category-value pairs? For example, I want to look for diesel cars with manual gearbox.
Any help will be very appreciated. Thank you in advance.
You can do this using aggregation and a having clause:
SELECT card_id
FROM cars
GROUP BY card_id
HAVING SUM(category_id = 1 AND value_id = 2) > 0 AND
SUM(category_id = 3 and value_id = 43) > 0;
Each condition in the having clause counts the number of rows that match a given condition. You can add as many conditions as you like. The first, for instance, says that there is at least one row where the category is 1 and the value is 2.
SQL Fiddle
Another approach is to create a user defined function that takes a table of attribute/value pairs and returns a table of matching cars. This has the advantage of allowing an arbitrary number of attribute/value pairs without resorting to dynamic SQL.
--Declare a "sample" table for proof of concept, replace this with your real data table
DECLARE #T TABLE(PID int, Attr Int, Val int)
--Populate the data table
INSERT INTO #T(PID , Attr , Val) VALUES (1,1,1), (1,3,5),(1,7,9),(2,1,2),(2,3,5),(2,7,9),(3,1,1),(3,3,5), (3,7,9)
--Declare this as a User Defined Table Type, the function would take this as an input
DECLARE #C TABLE(Attr Int, Val int)
--This would be populated by the code that calls the function
INSERT INTO #C (Attr , Val) VALUES (1,1),(7,9)
--The function (or stored procedure) body begins here
--Get a list of IDs for which there is not a requested attribute that doesn't have a matching value for that ID
SELECT DISTINCT PID
FROM #T as T
WHERE NOT EXISTS (SELECT C.ATTR FROM #C as C
WHERE NOT EXISTS (SELECT * FROM #T as I
WHERE I.Attr = C.Attr and I.Val = C.Val and I.PID = T.PID ))

SQL Server to concatenate query results and insert the into another table

I have table A and a table B. Table A contains the product data, Table B contains the images for each product. One line per image.
I need a SQL statement created to Loop through table A, get the product ID, then concatenate the result set and insert it into one field in table A.
Table A:
P-ID, Value1, Value2, Value3, ImageLocation
35 Name Price description
Table B:
P-ID, ImageLocation
35 /upload/directory/imagename.jpg
35 /upload/directory/imagename2.jpg
35 /upload/directory/imagename3.jpg
End result:
TableA:
P-ID, Value1, Value2, Value3, ImageLocation
35 Name Price description /up/dir/image.jpg,/up/dir/image2.jpg,/up/dir/image3.jpg
How on earth do I SQL-ize this?
Thanks from the newbie!!!!
Multivalued columns are frowned upon... and there's not a graceful way to do this in Sql Server - but, you can use FOR XML PATH:
SELECT PID, Value1, Value2, Value3, STUFF((
SELECT ',' + ImageLocation
FROM TableB b
WHERE b.PID = a.PID
FOR XML PATH('')
), 1, 1, '') AS ImageLocation
FROM TableA a
Sql Fiddle
But again, do you really need to be doing this in your Sql query, or is this something that you can handle in your application / presentation?
You could try something along these lines:
Select p.id, location || '/' ||......
From
(
Select * from tableA a, tableB b
Where a.id = b.id
)
I don't know which DBMS you are using, so please see which string con. is appropriate