get values from xml by sql query when several attributes - sql-server

There is xml with several attributes "Num"
DECLARE #XML XML = '
<FileId global_id="1234">
<file id="12aa">
</file>
<file id="12bb">
<Number Num = "1"/>
<Number Num = "2"/>
</file>
</FileId>';
With this sql query only one attribute can be get
SELECT F.[File].value(N'../#global_id','varchar(100)') as id_payment,
F.[File].value('#id', 'varchar(4)') AS id,
F.[File].value('(Number/#Num)[1]', 'int') as [Num]
FROM (VALUES (#XML)) V (X)
CROSS APPLY V.X.nodes('/FileId/file') F([File])
How to get all attributes -- Num = 1 and Num = 2.
Can be a variable amount of attributes.
id_payment id Num
1234 12aa NULL
1234 12bb 1
1234 12bb 2

Much simpler version. (1) No need to use the VALUES clause. (2) The OUTER APPLY simulates LEFT OUTER JOIN. (3) Most efficient way to retrieve the global_id attribute. The credit goes to Shnugo.
SQL
DECLARE #XML XML = N'
<FileId global_id="1234">
<file id="12aa">
</file>
<file id="12bb">
<Number Num="1"/>
<Number Num="2"/>
</file>
</FileId>';
SELECT #xml.value('(/FileId/#global_id)[1]','INT') AS id_payment
, c.value('#id', 'VARCHAR(4)') AS id
, n.value('#Num', 'INT') AS [Num]
FROM #xml.nodes('/FileId/file') AS t(c)
OUTER APPLY t.c.nodes('Number') AS t2(n);
Output
+------------+------+------+
| id_payment | id | Num |
+------------+------+------+
| 1234 | 12aa | NULL |
| 1234 | 12bb | 1 |
| 1234 | 12bb | 2 |
+------------+------+------+

DECLARE #XML XML = '
<FileId global_id="1234">
<file id="12aa">
</file>
<file id="12bb">
<Number Num = "1"/>
<Number Num = "2"/>
<Number Num = "3"/>
<Number Num = "4"/>
<Number Num = "5"/>
<Number Num = "6"/>
</file>
</FileId>';
SELECT F.[File].value(N'../#global_id','varchar(100)') as id_payment,
F.[File].value('#id', 'varchar(4)') AS id,
F.[File].value('(Number/#Num)[1]', 'int') as [Num],
n.num.value('(#Num)[1]', 'int') as [Numxyz]
FROM (VALUES (#XML)) V (X)
CROSS APPLY V.X.nodes('/FileId/file') F([File])
outer apply F.[File].nodes('Number') as n(num)

Related

XML to SQL Table Query

This is my XML stored in a row. How do I convert it to insert into a table using a T-SQL query in the following table format?
<ENVELOPE>
<DSPVCHDATE>16-4-2021</DSPVCHDATE>
<DSPVCHITEMACCOUNT>PRASHANT MEHTA 359244</DSPVCHITEMACCOUNT>
<DSPVCHTYPE>Sale</DSPVCHTYPE>
<DSPINBLOCK>
<DSPVCHINQTY></DSPVCHINQTY>
<DSPVCHINAMT></DSPVCHINAMT>
</DSPINBLOCK>
<DSPOUTBLOCK>
<DSPVCHOUTQTY>1 Pcs</DSPVCHOUTQTY>
<DSPVCHNETTOUTAMT>23046.88</DSPVCHNETTOUTAMT>
</DSPOUTBLOCK>
<DSPCLBLOCK>
<DSPVCHCLQTY></DSPVCHCLQTY>
<DSPVCHCLAMT></DSPVCHCLAMT>
</DSPCLBLOCK>
<DSPEXPLVCHNUMBER>(No. :IV2612)</DSPEXPLVCHNUMBER>
<DSPVCHDATE>19-4-2021</DSPVCHDATE>
<DSPVCHITEMACCOUNT>XYZ Company</DSPVCHITEMACCOUNT>
<DSPVCHTYPE>Purchase</DSPVCHTYPE>
<DSPINBLOCK>
<DSPVCHINQTY>1 Pcs</DSPVCHINQTY>
<DSPVCHINAMT>23437.50</DSPVCHINAMT>
</DSPINBLOCK>
<DSPOUTBLOCK>
<DSPVCHOUTQTY></DSPVCHOUTQTY>
<DSPVCHNETTOUTAMT></DSPVCHNETTOUTAMT>
</DSPOUTBLOCK>
<DSPCLBLOCK>
<DSPVCHCLQTY>0 Pcs</DSPVCHCLQTY>
<DSPVCHCLAMT></DSPVCHCLAMT>
</DSPCLBLOCK>
<DSPEXPLVCHNUMBER>(No. :IV2613)</DSPEXPLVCHNUMBER>
</ENVELOPE>
This is the required output format.
Issue is I do not have a record separator in raw xml. Each new records starts with a <DSPVCHDATE>
Here is another method by using pure XQuery. No need to do any string manipulation, CASTing, etc.
All elements inside the root element <ENVELOPE> constitute an Arithmetic Progression. Elements that grouped by their position: 1 - 7, 8 - 14, etc. should be placed inside the encompassing <row> element.
It creates the following XML on the fly:
<ENVELOPE>
<row>
<DSPVCHDATE>16-4-2021</DSPVCHDATE>
...
<DSPEXPLVCHNUMBER>(No. :IV2612)</DSPEXPLVCHNUMBER>
</row>
<row>
<DSPVCHDATE>19-4-2021</DSPVCHDATE>
...
<DSPEXPLVCHNUMBER>(No. :IV2613)</DSPEXPLVCHNUMBER>
</row>
</ENVELOPE>
SQL
DECLARE #tbl TABLE (ID INT IDENTITY PRIMARY KEY, xmldata XML);
INSERT INTO #tbl (xmldata) VALUES
(N'<ENVELOPE>
<DSPVCHDATE>16-4-2021</DSPVCHDATE>
<DSPVCHITEMACCOUNT>PRASHANT MEHTA 359244</DSPVCHITEMACCOUNT>
<DSPVCHTYPE>Sale</DSPVCHTYPE>
<DSPINBLOCK>
<DSPVCHINQTY></DSPVCHINQTY>
<DSPVCHINAMT></DSPVCHINAMT>
</DSPINBLOCK>
<DSPOUTBLOCK>
<DSPVCHOUTQTY>1 Pcs</DSPVCHOUTQTY>
<DSPVCHNETTOUTAMT>23046.88</DSPVCHNETTOUTAMT>
</DSPOUTBLOCK>
<DSPCLBLOCK>
<DSPVCHCLQTY></DSPVCHCLQTY>
<DSPVCHCLAMT></DSPVCHCLAMT>
</DSPCLBLOCK>
<DSPEXPLVCHNUMBER>(No. :IV2612)</DSPEXPLVCHNUMBER>
<DSPVCHDATE>19-4-2021</DSPVCHDATE>
<DSPVCHITEMACCOUNT>XYZ Company</DSPVCHITEMACCOUNT>
<DSPVCHTYPE>Purchase</DSPVCHTYPE>
<DSPINBLOCK>
<DSPVCHINQTY>1 Pcs</DSPVCHINQTY>
<DSPVCHINAMT>23437.50</DSPVCHINAMT>
</DSPINBLOCK>
<DSPOUTBLOCK>
<DSPVCHOUTQTY></DSPVCHOUTQTY>
<DSPVCHNETTOUTAMT></DSPVCHNETTOUTAMT>
</DSPOUTBLOCK>
<DSPCLBLOCK>
<DSPVCHCLQTY>0 Pcs</DSPVCHCLQTY>
<DSPVCHCLAMT></DSPVCHCLAMT>
</DSPCLBLOCK>
<DSPEXPLVCHNUMBER>(No. :IV2613)</DSPEXPLVCHNUMBER>
</ENVELOPE>');
SELECT ID --, x
, c.value('(DSPVCHDATE/text())[1]','nvarchar(100)') as DSPVCHDATE
,c.value('(DSPVCHITEMACCOUNT/text())[1]','nvarchar(100)') as DSPVCHITEMACCOUNT
,c.value('(DSPVCHTYPE/text())[1]','nvarchar(100)') as DSPVCHTYPE
,c.value('(DSPINBLOCK/DSPVCHINQTY/text())[1]','nvarchar(100)') AS DSPVCHINQTY
,c.value('(DSPINBLOCK/DSPVCHINAMT/text())[1]','decimal(12,2)') AS DSPVCHINAMT
,c.value('(DSPOUTBLOCK/DSPVCHOUTQTY/text())[1]','nvarchar(100)') AS DSPVCHOUTQTY
,c.value('(DSPOUTBLOCK/DSPVCHNETTOUTAMT/text())[1]','decimal(12,2)') AS DSPVCHNETTOUTAMT
,c.value('(DSPEXPLVCHNUMBER/text())[1]','nvarchar(100)') as DSPEXPLVCHNUMBER
--,c.value('(DSPCLBLOCK/DSPVCHCLQTY/text())[1]','nvarchar(100)') AS DSPVCHCLQTY
--,c.value('(DSPCLBLOCK/DSPVCHCLAMT/text())[1]','int') AS DSPVCHCLAMT
FROM #tbl
CROSS APPLY (SELECT xmldata.query('<ENVELOPE>
{
for $x in /ENVELOPE/DSPVCHDATE
let $pos := count(ENVELOPE/DSPVCHDATE[. << $x]) + 1
let $start := 1 + 7 * ($pos -1)
let $end := 7 * $pos
return <row>{/ENVELOPE/*[position() ge $start and position() le $end]}</row>
}
</ENVELOPE>')) AS t1(x)
CROSS APPLY t1.x.nodes('/ENVELOPE/row') AS t2(c);
Output
+----+------------+-----------------------+------------+-------------+-------------+--------------+------------------+------------------+
| ID | DSPVCHDATE | DSPVCHITEMACCOUNT | DSPVCHTYPE | DSPVCHINQTY | DSPVCHINAMT | DSPVCHOUTQTY | DSPVCHNETTOUTAMT | DSPEXPLVCHNUMBER |
+----+------------+-----------------------+------------+-------------+-------------+--------------+------------------+------------------+
| 1 | 16-4-2021 | PRASHANT MEHTA 359244 | Sale | NULL | NULL | 1 Pcs | 23046.88 | (No. :IV2612) |
| 1 | 19-4-2021 | XYZ Company | Purchase | 1 Pcs | 23437.50 | NULL | NULL | (No. :IV2613) |
+----+------------+-----------------------+------------+-------------+-------------+--------------+------------------+------------------+
SQL #2
Based on #Charlieface idea.
WITH rs AS
(
SELECT ID, xmldata
, c.value('for $i in . return count(../*[. << $i]) + 1', 'INT') AS pos
FROM #tbl
CROSS APPLY xmldata.nodes('/ENVELOPE/DSPVCHDATE') AS t(c)
)
SELECT ID
, c.value('(/ENVELOPE/*[sql:column("pos")]/text())[1]','nvarchar(100)') AS DSPVCHDATE
, c.value('(/ENVELOPE/*[sql:column("pos") + 1]/text())[1]','nvarchar(100)') AS DSPVCHITEMACCOUNT
, c.value('(/ENVELOPE/*[sql:column("pos") + 2]/text())[1]','nvarchar(100)') AS DSPVCHTYPE
, c.value('(/ENVELOPE/*[sql:column("pos") + 3]/DSPVCHINQTY/text())[1]','nvarchar(100)') AS DSPVCHINQTY
, c.value('(/ENVELOPE/*[sql:column("pos") + 3]/DSPVCHINAMT/text())[1]','decimal(12,2)') AS DSPVCHINAMT
, c.value('(/ENVELOPE/*[sql:column("pos") + 4]/DSPVCHOUTQTY/text())[1]','nvarchar(100)') AS DSPVCHOUTQTY
, c.value('(/ENVELOPE/*[sql:column("pos") + 4]/DSPVCHNETTOUTAMT/text())[1]','nvarchar(100)') AS DSPVCHNETTOUTAMT
, c.value('(/ENVELOPE/*[sql:column("pos") + 6]/text())[1]','nvarchar(100)') AS DSPEXPLVCHNUMBER
FROM rs
CROSS APPLY xmldata.nodes('/ENVELOPE') AS t(c);
You can use outer apply to navigate the nested elements of xml content.
Given the inconvenient structure of this XML, it can be changed into something useable as follows, by adding a containing node called <ThisNode>.
DECLARE #XML XML = '
<ENVELOPE>
<DSPVCHDATE>16-4-2021</DSPVCHDATE>
<DSPVCHITEMACCOUNT>PRASHANT MEHTA 359244</DSPVCHITEMACCOUNT>
<DSPVCHTYPE>Sale</DSPVCHTYPE>
<DSPINBLOCK>
<DSPVCHINQTY></DSPVCHINQTY>
<DSPVCHINAMT></DSPVCHINAMT>
</DSPINBLOCK>
<DSPOUTBLOCK>
<DSPVCHOUTQTY>1 Pcs</DSPVCHOUTQTY>
<DSPVCHNETTOUTAMT>23046.88</DSPVCHNETTOUTAMT>
</DSPOUTBLOCK>
<DSPCLBLOCK>
<DSPVCHCLQTY></DSPVCHCLQTY>
<DSPVCHCLAMT></DSPVCHCLAMT>
</DSPCLBLOCK>
<DSPEXPLVCHNUMBER>(No. :IV2612)</DSPEXPLVCHNUMBER>
<DSPVCHDATE>19-4-2021</DSPVCHDATE>
<DSPVCHITEMACCOUNT>XYZ Company</DSPVCHITEMACCOUNT>
<DSPVCHTYPE>Purchase</DSPVCHTYPE>
<DSPINBLOCK>
<DSPVCHINQTY>1 Pcs</DSPVCHINQTY>
<DSPVCHINAMT>23437.50</DSPVCHINAMT>
</DSPINBLOCK>
<DSPOUTBLOCK>
<DSPVCHOUTQTY></DSPVCHOUTQTY>
<DSPVCHNETTOUTAMT></DSPVCHNETTOUTAMT>
</DSPOUTBLOCK>
<DSPCLBLOCK>
<DSPVCHCLQTY>0 Pcs</DSPVCHCLQTY>
<DSPVCHCLAMT></DSPVCHCLAMT>
</DSPCLBLOCK>
<DSPEXPLVCHNUMBER>(No. :IV2613)</DSPEXPLVCHNUMBER>
</ENVELOPE>'
This can be converted to useable XML as follows:
WITH
cte AS (Select REPLACE(REPLACE(CONVERT(NVARCHAR(MAX), #XML, 1), N'<DSPVCHDATE>', '
</ThisNode>
<ThisNode>
<DSPVCHDATE>'), N'</ENVELOPE>', N'
</ThisNode>
</ENVELOPE>') AS str)
SELECT #XML = CAST(STUFF(str, CHARINDEX(N'</ThisNode>', str), LEN(N'</ThisNode>'), N'') AS XML)
FROM cte
;
query
SELECT
A.evnt.value('(DSPVCHDATE/text())[1]','nvarchar(100)') as DSPVCHDATE
,A.evnt.value('(DSPVCHITEMACCOUNT/text())[1]','nvarchar(100)') as DSPVCHITEMACCOUNT
,A.evnt.value('(DSPVCHTYPE/text())[1]','nvarchar(100)') as DSPVCHTYPE
,A.evnt.value('(DSPVCHITEMACCOUNT/text())[1]','nvarchar(100)') as DSPVCHITEMACCOUNT
,A.evnt.value('(DSPEXPLVCHNUMBER/text())[1]','nvarchar(100)') as DSPEXPLVCHNUMBER
,B.rec.value('(DSPVCHINQTY/text())[1]','nvarchar(100)') AS DSPVCHINQTY
,B.rec.value('(DSPVCHINAMT/text())[1]','nvarchar(100)') AS DSPVCHINAMT
,C.rec.value('(DSPVCHOUTQTY/text())[1]','nvarchar(100)') AS DSPVCHOUTQTY
,C.rec.value('(DSPVCHNETTOUTAMT/text())[1]','float') AS DSPVCHNETTOUTAMT
,D.rec.value('(DSPVCHCLQTY/text())[1]','nvarchar(100)') AS DSPVCHCLQTY
,D.rec.value('(DSPVCHCLAMT/text())[1]','int') AS DSPVCHCLAMT
FROM #XML.nodes('/ENVELOPE/ThisNode') A(evnt)
OUTER APPLY A.evnt.nodes('DSPINBLOCK') B(rec)
OUTER APPLY A.evnt.nodes('DSPOUTBLOCK') C(rec)
OUTER APPLY A.evnt.nodes('DSPCLBLOCK') D(rec)
demo in db<>fiddle

SQL Server - Each GROUP BY expression must contain at least one column that is not an outer reference

I need to identify all records that have MostRecent=-1, OilWell=-1, plus are duplicate records with the same Api, and join these to get the associated CompanyName.
With the query:
SELECT
BLMAPDCONTACT.CompanyName, APD.Api, APD.ID, APD.MostRecent,
APD.Project_Nu, APD.Unit_Lease, APD.Well_Nu, APD.OilWell
FROM
APD
INNER JOIN
BLMAPDCONTACT ON APD.BLM_APD_Cont = BLMAPDCONTACT.OBJECTID
WHERE
(APD.Api IN (SELECT APD.Api
FROM APD AS Tmp
WHERE APD.MostRecent = -1 AND APD.OilWell = -1
GROUP BY APD.Api
HAVING Count(APD.Api) > 1))
ORDER BY
APD.Api DESC;
I get this error:
Each GROUP BY expression must contain at least one column that is not an outer reference.
This error appeared after I added the JOIN clause; without it, it worked.
Example desired output will match on the following records from the APD table:
APD.Api | APD.MostRecent | APD.OilWell
--------------------------------------
123 | -1 | -1
123 | -1 | -1
And not:
APD.Api | APD.MostRecent | APD.OilWell
--------------------------------------
321 | 0 | -1
321 | -1 | -1
did you try this:
SELECT BLMAPDCONTACT.CompanyName, APD.Api, APD.ID, APD.MostRecent, APD.Project_Nu, APD.Unit_Lease, APD.Well_Nu, APD.OilWell
FROM APD INNER JOIN BLMAPDCONTACT ON APD.BLM_APD_Cont = BLMAPDCONTACT.OBJECTID
WHERE (APD.Api IN
(SELECT tmp.Api
FROM APD As Tmp
WHERE tmp.MostRecent=-1 AND tmp.OilWell=-1
GROUP BY tmp.Api HAVING Count(tmp.Api)>1))
ORDER BY APD.Api DESC;
The aliases and table names are confusing me a bit. If you run the following, do you still get the same error?
SELECT b.CompanyName
, a.Api
, a.ID
, a.MostRecent
, a.Project_Nu
, a.Unit_Lease
, a.Well_Nu
, a.OilWell
FROM APD a INNER JOIN BLMAPDCONTACT b
ON a.BLM_APD_Cont = b.OBJECTID
WHERE a.Api IN (
SELECT tmp.Api
FROM APD As Tmp
WHERE tmp.MostRecent = -1 AND tmp.OilWell = -1
GROUP BY tmp.Api
HAVING Count(tmp.Api) > 1
)
ORDER BY a.Api DESC;
Also, just double check that I've translated tables to aliases correctly.

T-SQL Query for Vertical Table Structure

I'm working on an e-commerce project. Now I have to build a filter for product listing page.
My tables are below.
Products
id title | description | Etc.
-- ---------- | --------------------- | -----------
1 Product 1 | Product 1 description | xxx
2 Product 2 | Product 2 description | xxx
3 Product 3 | Product 3 description | xxx
4 Product 4 | Product 4 description | xxx
5 Product 5 | Product 5 description | xxx
Specifications
id title | Etc.
-- ---------- | ------
1 Color | xxx
2 Display | xxx
ProductSpecifications
id | productId | specificationId | value
----------- | ----------- | --------------- | -----
1 | 1 | 1 | Red
2 | 1 | 2 | LED
3 | 2 | 1 | Red
4 | 2 | 2 | OLED
5 | 3 | 1 | Blue
6 | 3 | 2 | LED
7 | 4 | 1 | Blue
8 | 4 | 2 | OLED
Users of e-commerce must be able to filter multiple options at the same time. I mean, a user may want to search for "(Red or Blue) and OLED" TVs.
I tried something but i couldn't write the right stored procedure. I guess, i'm stuck here and i need some help.
EDIT :
After some answers, I need to update some additional information here.
The specifications are dynamic. So filters are also dynamic. I generate filters by using a bit column named allowFilter. So I cant use strongly typed parameters like #color or #display
Users may not use filter. Or they may use one or more filter. You can find the query that i'm working on here:
ALTER PROCEDURE [dbo].[ProductsGetAll]
#categoryId int,
#brandIds varchar(max),
#specIds varchar(max),
#specValues varchar(max),
#pageNo int,
#pageSize int,
#status smallint,
#search varchar(255),
#sortOrder smallint
as
/*
TODO: Modify query to use sortOrder
*/
select * into #products
from
(
select ROW_NUMBER() OVER (order by p.sortOrder) as rowId,p.*
from Products p left join ProductSpecifications ps on ps.productId = p.id
where
(#status = -1
or (#status = -2 and (p.status = 0 or p.status = 1))
or (p.status = #status)
)
and (#categoryId = -1 or p.categoryId = #categoryId)
and (#brandIds = '' or p.brandId in (select ID from fnStringToBigIntTable(#brandIds,',')))
and (
#search = ''
or p.title like '%' + #search + '%'
or p.description like '%' + #search + '%'
or p.detail like '%' + #search + '%'
)
and (#specIds = ''
or (
ps.specificationId in (select ID from fnStringToBigIntTable(#specIds,','))
and ps.value in (#specValues)
)
)
) x
where
(rowId > #pageSize * (#pageNo - 1) and rowId <= #pageSize * #pageNo)
select * from #products
select * from Categories where id in (select categoryId from #products)
select * from Brands where id in (select brandId from #products)
select count(p.id)
from Products p left join ProductSpecifications ps on ps.productId = p.id
where
(#status = -1
or (#status = -2 and (p.status = 0 or p.status = 1))
or (p.status = #status)
)
and (#categoryId = -1 or p.categoryId = #categoryId)
and (#brandIds = '' or p.brandId in (select ID from fnStringToBigIntTable(#brandIds,',')))
and (
#search = ''
or p.title like '%' + #search + '%'
or p.description like '%' + #search + '%'
or p.detail like '%' + #search + '%'
)
and (#specIds = ''
or (
ps.specificationId in (select ID from fnStringToBigIntTable(#specIds,','))
and ps.value in (#specValues)
)
)
drop table #products
My problem is the part of:
and (#specIds = ''
or (
ps.specificationId in (select ID from fnStringToBigIntTable(#specIds,','))
and ps.value in (#specValues)
)
)
I can totally change of this part and the parameters that used in this part.
Firstly, I have to thank you #alex. I used table valued paramters to solve my problem.
Type:
CREATE TYPE [dbo].[specificationsFilter] AS TABLE(
[specId] [int] NULL,
[specValue] [varchar](50) NULL
)
Stored Procedure:
ALTER PROCEDURE [dbo].[ProductsGetAll]
#categoryId int,
#brandIds varchar(max),
#specifications specificationsFilter readonly,
#pageNo int,
#pageSize int,
#status smallint,
#search varchar(255),
#sortOrder smallint
as
declare #filterCount int
set #filterCount = (select count(distinct specId) from #specifications)
/*
ORDER BY
TODO: Modify query to use sortOrder
*/
select * into #products
from
(
select ROW_NUMBER() OVER (order by p.sortOrder) as rowId,p.*
from Products p
where
(#status = -1
or (#status = -2 and (p.status = 0 or p.status = 1))
or (p.status = #status)
)
and (#categoryId = -1 or p.categoryId = #categoryId)
and (#brandIds = '' or p.brandId in (select ID from fnStringToBigIntTable(#brandIds,',')))
and (
#search = ''
or p.title like '%' + #search + '%'
or p.description like '%' + #search + '%'
or p.detail like '%' + #search + '%'
)
and (#filterCount = 0
or (
p.id in (
select productId
from ProductSpecifications ps, #specifications s
where
ps.specificationId = s.specId
and ps.value = s.specValue
group by productId
having sum(1) >= #filterCount
)
)
)
) x
where
(rowId > #pageSize * (#pageNo - 1) and rowId <= #pageSize * #pageNo)
select * from #products
select * from Categories where id in (select categoryId from #products)
select * from Brands where id in (select brandId from #products)
select count(p.id)
from Products p
where
(#status = -1
or (#status = -2 and (p.status = 0 or p.status = 1))
or (p.status = #status)
)
and (#categoryId = -1 or p.categoryId = #categoryId)
and (#brandIds = '' or p.brandId in (select ID from fnStringToBigIntTable(#brandIds,',')))
and (
#search = ''
or p.title like '%' + #search + '%'
or p.description like '%' + #search + '%'
or p.detail like '%' + #search + '%'
)
and (#filterCount = 0
or (
p.id in (
select productId
from ProductSpecifications ps, #specifications s
where
ps.specificationId = s.specId
and ps.value = s.specValue
group by productId
having sum(1) >= #filterCount
)
)
)
drop table #products
.Net Code to create Data Table paramter:
private DataTable GetSpecificationFilter(string specificationFilter)
{
DataTable table = new DataTable();
table.Columns.Add("specId", typeof(Int32));
table.Columns.Add("specValue", typeof(string));
string[] specifications = specificationFilter.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
foreach(string specification in specifications)
{
string[] specificationParams = specification.Split(new char[] { ':' }, StringSplitOptions.RemoveEmptyEntries);
int specificationId = Convert.ToInt32(specificationParams[0]);
string[] specificationValues = specificationParams[1].Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
foreach(string value in specificationValues)
{
table.Rows.Add(specificationId, value);
}
}
return table;
}
And my query string structure:
?specs=1:Red,Blue;3:LED,OLED
This is a complete solution to filter product specifications in a vertical table sturcture. I used this for an e-commerce project. I hope this solution helps you for similar cases.
You need a way to pass in the specifications and their values. One approach is to use group by and having for the overall query:
select ps.product_id
from product_specifications ps join
specifications s
on ps.specification_id = s.specification_id
where (s.name = #title1 and ps.value = #value1) or
(s.name = #title2 and ps.value = #value2)
having count(*) = 2; -- "2" is the number of specifications you are checking
This version requires adding in the specifications and values as separate variables. There are similar approaches, where you can pass in the value using a temporary variable or values clause. It is unclear what method of passing in the values works best in your particular case.
Update
Table valued parameters should be used in this case.
(See related)
older answer
which appears to be a variation on what was happening in op's original stored procedure.
This is not the best approach, but this should get the job done.
CREATE PROCEDURE GetData
#Color CHAR(2) -- "10" is only red, "01" is only green, "11" is both red and green
, #Display CHAR(2) -- "10" is LED, "01" is OLED, "11" is both LED and OLED
AS
BEGIN
DECLARE #Values TABLE (Value NVARCHAR(10))
IF SUBSTRING(#Color, 1, 1) = '1' BEGIN INSERT INTO #Values (Value) VALUES ('Red') END
IF SUBSTRING(#Color, 2, 1) = '1' BEGIN INSERT INTO #Values (Value) VALUES ('Green') END
IF SUBSTRING(#Display, 1, 1) = '1' BEGIN INSERT INTO #Values (Value) VALUES ('LED') END
IF SUBSTRING(#Display, 2, 1) = '1' BEGIN INSERT INTO #Values (Value) VALUES ('OLED') END
SELECT *
FROM productspecifications ps
INNER JOIN products p
ON p.id = ps.productid
INNER JOIN specifications s
ON ps.specificationid = s.id
WHERE ps.Value IN (SELECT * FROM #Values)
END
This example is very specific to tables you provided in question.
Explanation of how it works
You pass two strings which consist of only zeros and ones (ex.: "0010110"). Your stored procedure will know to interpret 1 at index 0 in string #Color as Red and 1 at index 1 in #Color as Blue. Same thing for LED vs OLED. Your stored procedure will have many IF statements to check for every index in every string and store corresponding values in some temporary table (or temporary table variable if you there are not too many values). Then when you query your tables just put a single WHERE clause which check where value in ProductSpecifications table is present in the temporary table you just created.
How would it work
If you want (red or blue) and LED then #Color = "10" and #Display = "10".
If you want blue and OLED then #Color = "01" and #Display = "01".
If you want all then #Color = "11" and #Display = "11".
Pros
You can achieve that (red or blue) and LED logic effect
Cons
You have to know which index in the passed string corespondent to which value
Logic is "leaking" from stored procedure into code (lack of encapsulation)
Conclusion
This is not a good solution. I personally don't like it, but it would get the job done. If somebody knows how to improve this that would be amazing. I would love to learn a better solution myself.
Also, it appeared to me that you have the need to pass "array" of data as parameter to stored procedure, so I think you may want to look at different ways on how to do that. The one in example I provided is one way of achieving "array passing", but there are many other and better ways.
I think you need FIRST a foreign key to do what you want .
You can add a field to the Products table and call it specification , this will be your foreign key .
After that to do what you want try to use a GROUP BY expression
I think if there is only value parameter this works, or add more search parameters u like
CREATE PROCEDURE usp_ProductSpecifications (#value)
AS
BEGIN
SELECT p.id
,p.NAME
,s.etc
,ps.value
,p.etc
FROM productspecifications ps
INNER JOIN products p
ON p.id = ps.productid
INNER JOIN specifications s
ON ps.specificationid = s.id
WHERE ps.value = #value
END
Please try below suggested solution, hope it helps!!
Create Procedure SearchByCriteria
#Color VARCHAR(100) = NULL,
#Display VARCHAR(100) = NULL
AS
BEGIN
IF #Color IS NOT NULL
SET #Color = '%' + REPLACE (#Color,',','% OR ') + '%'
SELECT
fROM PRoduct p
INNER JOIN ProductSpecification ps ON ps.ProductId = p.productID
LEFT OUTER JOIN specification scolor ON scolor.ID = ps.SpecificationID
and scolor.Id = 1
LEFT OUTER JOIN specification sDisplay ON sdisplay.ID = ps.SpecificationID
and sdisplay.Id = 2
WHERE (#Color IS NULL OR scolor.etc like #Color)
AND (#Display IS NULL OR Sdisplay like #Display)
END
GO

Querying XML file in SQL Server

This is my xml file
<Detials xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Parents>
<Parent id="1234">
<name>
<firstname>ABC</firstname>
<lastname>XYSX</lastname>
</name>
</Parent>
<Parent id="1235">
<name>
<firstname>TFU</firstname>
<lastname>GHY</lastname>
</name>
</Parent>
</Parents>
<Children>
<Child id="457" Parentid="1234">
<name>
<cfirstname>JOHN</cfirstname>
<clastname>SMITH</clastname>
</name>
</Child>
<Child id="459" Parentid="1235">
<name>
<cfirstname>DAVID</cfirstname>
<clastname>SMITH</clastname>
</name>
</Child>
</Children>
</Detials>
I stored it in a table (using Bulk Insert).
When I query like this
SELECT
x.c.value('(./Parents/Parent/#id)[1]', 'nvarchar(4)' ) AS id
FROM
T1 s
CROSS APPLY
s.XMLData.nodes('Detials') AS x(c)
I get the result as
Id
----
1234
When I changed it a little bit
SELECT
x.c.value('#id', 'nvarchar(4)' ) AS id
FROM
T1 s
CROSS APPLY
s.XMLData.nodes('/Detials/Parents/Parent') AS x(c)
I get:
Id
----
1234
1235
I just want to query them to result like this
Parent_id | firstname | lastname | Child_id | Child_First_name | child_last_name
----------+-----------+----------+----------+------------------+-----------
1234 | ABC | XYSX | 457 | JOHN | SMITH
1235 | TFU | GHY | 459 | DAVID | SMITH
How could I do this in query ?
In my result I want total no. of rows which depends on the Parentid. If my xml file contains 6 <parent id> then my query should show all 6 rows for each parent id's along with comparing the Childid's to also be included if the Parentid matches in <child> tag.
Thanks, Jayendran
You were on the right track, but this would require a left join on the two result sets Parent/Children
Example
Declare #T1 table (ID int,XMLData xml)
Insert Into #T1 values
(1,'<Detials xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><Parents><Parent id="1234"><name><firstname>ABC</firstname><lastname>XYSX</lastname></name></Parent><Parent id="1235"><name><firstname>TFU</firstname><lastname>GHY</lastname></name></Parent></Parents><Children><Child id="457" Parentid="1234"><name><cfirstname>JOHN</cfirstname><clastname>SMITH</clastname></name></Child><Child id="459" Parentid="1235"><name><cfirstname>DAVID</cfirstname><clastname>SMITH</clastname></name></Child></Children></Detials>')
Select A.ID
,B.*
From #T1 A
Cross Apply (
Select B1.Parent_id
,B1.First_Name
,B1.Last_Name
,B2.Child_id
,B2.Child_First_name
,B2.child_last_name
From (
Select
Parent_id = x.c.value('#id', 'nvarchar(4)' )
,First_Name = x.c.value('(name/firstname)[1]','varchar(50)')
,Last_Name = x.c.value('(name/lastname)[1]','varchar(50)')
From #T1 s
Cross Apply s.XMLData.nodes('/Detials/Parents/Parent') AS x(c)
) B1
Left Join (
Select
pt_id = x.c.value('#Parentid', 'nvarchar(4)' )
,Child_id = x.c.value('#id', 'nvarchar(4)' )
,Child_First_name = x.c.value('(name/cfirstname)[1]','varchar(50)')
,child_last_name = x.c.value('(name/clastname)[1]','varchar(50)')
From #T1 s
Cross Apply s.XMLData.nodes('/Detials/Children/Child ') AS x(c)
) B2
on B1.Parent_id = B2.pt_id
) B
Returns
I'd solve this in one single go:
SELECT Parent.ID
,p.value('(name/firstname/text())[1]','nvarchar(max)')
,p.value('(name/lastname/text())[1]','nvarchar(max)')
,c.value('#id','int')
,c.value('(name/cfirstname/text())[1]','nvarchar(max)')
,c.value('(name/clastname/text())[1]','nvarchar(max)')
FROM #xml.nodes('/Detials/Parents/Parent') AS A(p)
OUTER APPLY (SELECT p.value('#id','int')) AS Parent(ID)
OUTER APPLY #xml.nodes('/Detials/Children/Child[#Parentid=sql:column("Parent.ID")]') AS B(c);
The trick is, to read the Parentid into a named column via APPLY. This value can be used as predicate to fetch the children via sql:column().

Add count to the root elements using FOR XML PATH

I have a Sql statement that returns xml of products in which root element is products. How can i add count attribute to the root element.
My sql is:
SELECT
id AS 'product_id',
name AS 'product_name'
FROM product
WHERE status = 1 AND ......
ORDER BY productid
FOR XML PATH('product'), ROOT('products')
Result is
<products>
<product>
.
.
</product>
</products>
I want to change result to
<products count="100">
<product>
.
.
</product>
</products>
SELECT
COUNT(*) AS '#count',
(
SELECT *
FROM product c1 FOR XML PATH('product'), TYPE
)
FROM product ct FOR XML PATH('products')
The easiest way to filter is to add condition in both query and sub-query:
SELECT
COUNT(*) AS '#count',
(
SELECT *
FROM product c1
WHERE c1.status = 1 AND ......
FOR XML PATH('product'), TYPE
)
FROM product ct
WHERE ct.status = 1 AND ......
FOR XML PATH('products')
Or use temp-table:
SELECT *
INTO #Temp
FROM product c1
WHERE c1.status = 1 AND ......
SELECT
COUNT(*) AS '#count',
(
SELECT *
FROM #Temp c1
FOR XML PATH('product'), TYPE
)
FROM #Temp ct
FOR XML PATH('products')

Resources