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
Related
I have data that looks like after writing the recursive CTE:
| EPC | ParentEPC | SerialEventId| Level| #ItemName|
|--------|--------------|--------------:|:----------:|--------------|
| a| NULL|5557|0|[PALLET] - 7 UNITS|
| b| a|5557|1|[CARTON] - 1 UNIT|
| c| a|5557|1|[CASE] - 3 UNITS|
| d| c|5557|2|[CARTON] - 1 UNIT|
| e| c|5557|2|[CARTON] - 1 UNIT|
| f| c|5557|2|[CARTON] - 1 UNIT|
I want to write a T-SQL query in SQL Server to return the data like this:
<Items>
<Item ItemName="[PALLET] - 7 UNITS">
<Item ItemName="[CARTON] - 1 UNIT" />
<Item ItemName="[CASE] - 3 UNITS">
<Item ItemName="[CARTON] - 1 UNIT" />
<Item ItemName="[CARTON] - 1 UNIT" />
<Item ItemName="[CARTON] - 1 UNIT" />
</Item>
</Item>
</Items>
I have tried XML PATH but couldn't able to get the nesting part in XML based on the level like below
SELECT *
,(
SELECT epc."#ItemName"
FROM #EPC_items epc
WHERE epc.SerialEventID = se.SerialEventID
FOR XML PATH('Item')
,ROOT('Items')
,TYPE
)
FROM #SerialEvents se
Here is the recursive CTE query that I used to get the result table shown above
IF OBJECT_ID('tempdb..#SerialEvents') IS NOT NULL DROP TABLE #SerialEvents
GO
IF OBJECT_ID('tempdb..#EPC_items') IS NOT NULL DROP TABLE #EPC_items
GO
SELECT DISTINCT se.SerialEventID, se.OrderType, se.ASWRefNum, se.ASWLineNum
into #SerialEvents
FROM dbo.SerialEvent se
WHERE 1=1
AND SerialEventTypeId not in (1,13,8,9,10)
AND SerialEventDateTime >= DATEADD(d,-2, GETDATE())
ORDER BY 1 DESC;
;WITH CTE
as (
SELECT
et.EPC,et.ParentEPC,
et.SerialEventId ,
0 AS [Level],
' [' + UPPER(e.UnitType) + '] - '
+ CAST(e.ChildQuantity as VARCHAR)
+
CASE
WHEN e.ChildQuantity > 1 THEN ' UNITS'
ELSE ' UNIT'
END AS "#ItemName"
FROM dbo.EPCTRansaction et
INNER JOIN dbo.SerialEvent se on et.SerialEventId = se.SerialEventId
INNER JOIN dbo.vwEPC e ON et.EPC = e.EPC
INNER JOIN #SerialEvents sep on et.SerialEventId = sep.SerialEventId and et.SerialEventID =5557
WHERE
1=1
AND et.ParentEPC IS NULL
UNION ALL
SELECT
et.EPC,
CTE.EPC as ParentEPC,
et.SerialEventId,
cte.Level + 1,
' [' + UPPER(e.UnitType) + '] - '
+ CAST(e.ChildQuantity as VARCHAR)
+
CASE
WHEN e.ChildQuantity > 1 THEN ' UNITS'
ELSE ' UNIT'
END AS "#ItemName"
FROM dbo.EPCTRansaction et
INNER JOIN dbo.SerialEvent se on et.SerialEventId = se.SerialEventId
INNER JOIN dbo.vwEPC e ON et.EPC = e.EPC
INNER JOIN CTE on et.ParentEPC = CTE.EPC and et.SerialEventId = CTE.SerialEventId
WHERE
1=1
)
select * into #EPC_items from CTE
select * from #EPC_items
Recursing in rows is very easy in SQL Server. On the other hand, what you are trying to do is grouped recursion: on each level you want to group up the data and place it inside its parent. This is much harder.
The easiest method I have found is to use (horror of horrors!) a scalar UDF.
Unfortunately I can't test this as you haven't given proper sample data for all your tables. It's also unclear which joins are needed.
CREATE FUNCTION dbo.GetXml (#ParentEPC int)
RETURNS xml
AS
BEGIN
RETURN (
SELECT
CONCAT(
'[',
UPPER(e.UnitType),
'] - ',
e.ChildQuantity,
CASE
WHEN e.ChildQuantity > 1 THEN ' UNITS'
ELSE ' UNIT'
END
) AS [#ItemName],
dbo.GetXml(et.EPC) -- do not name this column
FROM dbo.EPCTRansaction et
INNER JOIN dbo.SerialEvent se on et.SerialEventId = se.SerialEventId
INNER JOIN dbo.vwEPC e ON et.EPC = e.EPC
INNER JOIN #SerialEvents sep on et.SerialEventId = sep.SerialEventId and et.SerialEventID = 5557
WHERE
EXISTS (SELECT et.ParentEPC INTERSECT SELECT #ParentEPC) -- nullable compare
FOR XML PATH('Item'), TYPE
);
END;
SELECT
dbo.GetXml(NULL)
FOR XML PATH('Items'), TYPE;
I have multiple use cases whereby I need to effectively create a virtual sql server table from a csv source and I need to do it preferably using pure sql on the fly- ie not using a procedure. To provide some context, I need to wire up this code in a third party reporting engine; which is talking to sql server in addition to other multiple data sources such as oracle etc.
This can be done in POSTGRE using a simple function split_part because that function includes the ability to search the csv concate string for the position of the field delimiter; example ',' comma separator. Unfortunately, sql server has a similar function STRING_SPLIT ( string , separator ) but notice it does NOT have the position aspect SPLIT_PART(string, delimiter, position) offered in POSTGre.
Example source csv:
Row 1 a,b,c,d
Row 2 e,f,g,h
etc
Output - using POSTGre db but require same for sql server
select split_part(p.Lines, ',',1) As Col1,
split_part(p.Lines, ',',2) As Col2,
split_part(p.Lines, ',',3) As Col3,
split_part(p.Lines, ',',4) As Col4
from ( select unnest(string_to_array(:csvdata, chr(10))) as Lines)p
Any solution?
SQL Server allows to query *.csv file as a virtual DB table on a file system.
Here is a minimal reproducible example.
SQL
SELECT *
FROM OPENROWSET(BULK 'e:\Temp\Quark.csv'
, FORMATFILE = 'e:\Temp\Quark.xml'
, ERRORFILE = 'e:\Temp\Quark.err'
, FIRSTROW = 2 -- real data starts on the 2nd row
, MAXERRORS = 100
) AS tbl;
Quark.csv
"ID"|"Name"|"Color"|"LogDate"|"Unknown"
41|Orange|Orange|2018-09-09 16:41:02.000|
42|Cherry, Banana|Red,Yellow||
43|Apple|Yellow|2017-09-09 16:41:02.000|
Quark.xml
<?xml version="1.0"?>
<BCPFORMAT xmlns="http://schemas.microsoft.com/sqlserver/2004/bulkload/format" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<RECORD>
<FIELD ID="1" xsi:type="CharTerm" TERMINATOR='|' MAX_LENGTH="70"/>
<FIELD ID="2" xsi:type="CharTerm" TERMINATOR='|' MAX_LENGTH="70" COLLATION="SQL_Latin1_General_CP1_CI_AS"/>
<FIELD ID="3" xsi:type="CharTerm" TERMINATOR='|' MAX_LENGTH="70" COLLATION="SQL_Latin1_General_CP1_CI_AS"/>
<FIELD ID="4" xsi:type="CharTerm" TERMINATOR='|' MAX_LENGTH="70" COLLATION="SQL_Latin1_General_CP1_CI_AS"/>
<FIELD ID="5" xsi:type="CharTerm" TERMINATOR='\r\n' MAX_LENGTH="70" COLLATION="SQL_Latin1_General_CP1_CI_AS"/>
</RECORD>
<ROW>
<COLUMN SOURCE="1" NAME="ID" xsi:type="SQLVARYCHAR"/>
<COLUMN SOURCE="2" NAME="Name" xsi:type="SQLVARYCHAR"/>
<COLUMN SOURCE="3" NAME="Color" xsi:type="SQLVARYCHAR"/>
<COLUMN SOURCE="4" NAME="LogDate" xsi:type="SQLVARYCHAR"/>
<COLUMN SOURCE="5" NAME="Unknown" xsi:type="SQLVARYCHAR"/>
</ROW>
</BCPFORMAT>
Output
+----+----------------+------------+-------------------------+---------+
| ID | Name | Color | LogDate | Unknown |
+----+----------------+------------+-------------------------+---------+
| 41 | Orange | Orange | 2018-09-09 16:41:02.000 | NULL |
| 42 | Cherry, Banana | Red,Yellow | NULL | NULL |
| 43 | Apple | Yellow | 2017-09-09 16:41:02.000 | NULL |
+----+----------------+------------+-------------------------+---------+
Unless I am mistaken - split_part parses a delimited string into component elements and returns the desired element from that string.
To accomplish this in SQL Server - we can use charindex and substring, or you can use a combination of splitting the string to a table, and rolling the values back up in a GROUP BY.
Here is a function that can be used to return up to 12 elements from a delimited string:
ALTER Function [dbo].[fnSplitString_12Columns] (
#pString varchar(8000)
, #pDelimiter char(1)
)
Returns Table
With schemabinding
As
Return
Select InputString = #pString
, p01_pos = p01.pos
, p02_pos = p02.pos
, p03_pos = p03.pos
, p04_pos = p04.pos
, p05_pos = p05.pos
, p06_pos = p06.pos
, p07_pos = p07.pos
, p08_pos = p08.pos
, p09_pos = p09.pos
, p10_pos = p10.pos
, p11_pos = p11.pos
, p12_pos = p12.pos
, col_01 = ltrim(substring(v.inputString, 1, p01.pos - 2))
, col_02 = ltrim(substring(v.inputString, p01.pos, p02.pos - p01.pos - 1))
, col_03 = ltrim(substring(v.inputString, p02.pos, p03.pos - p02.pos - 1))
, col_04 = ltrim(substring(v.inputString, p03.pos, p04.pos - p03.pos - 1))
, col_05 = ltrim(substring(v.inputString, p04.pos, p05.pos - p04.pos - 1))
, col_06 = ltrim(substring(v.inputString, p05.pos, p06.pos - p05.pos - 1))
, col_07 = ltrim(substring(v.inputString, p06.pos, p07.pos - p06.pos - 1))
, col_08 = ltrim(substring(v.inputString, p07.pos, p08.pos - p07.pos - 1))
, col_09 = ltrim(substring(v.inputString, p08.pos, p09.pos - p08.pos - 1))
, col_10 = ltrim(substring(v.inputString, p09.pos, p10.pos - p09.pos - 1))
, col_11 = ltrim(substring(v.inputString, p10.pos, p11.pos - p10.pos - 1))
, col_12 = ltrim(substring(v.inputString, p11.pos, p12.pos - p11.pos - 1))
From (Values (concat(#pString, replicate(#pDelimiter, 12)))) As v(inputString)
Cross Apply (Values (charindex(#pDelimiter, v.inputString, 1) + 1)) As p01(pos)
Cross Apply (Values (charindex(#pDelimiter, v.inputString, p01.pos) + 1)) As p02(pos)
Cross Apply (Values (charindex(#pDelimiter, v.inputString, p02.pos) + 1)) As p03(pos)
Cross Apply (Values (charindex(#pDelimiter, v.inputString, p03.pos) + 1)) As p04(pos)
Cross Apply (Values (charindex(#pDelimiter, v.inputString, p04.pos) + 1)) As p05(pos)
Cross Apply (Values (charindex(#pDelimiter, v.inputString, p05.pos) + 1)) As p06(pos)
Cross Apply (Values (charindex(#pDelimiter, v.inputString, p06.pos) + 1)) As p07(pos)
Cross Apply (Values (charindex(#pDelimiter, v.inputString, p07.pos) + 1)) As p08(pos)
Cross Apply (Values (charindex(#pDelimiter, v.inputString, p08.pos) + 1)) As p09(pos)
Cross Apply (Values (charindex(#pDelimiter, v.inputString, p09.pos) + 1)) As p10(pos)
Cross Apply (Values (charindex(#pDelimiter, v.inputString, p10.pos) + 1)) As p11(pos)
Cross Apply (Values (charindex(#pDelimiter, v.inputString, p11.pos) + 1)) As p12(pos);
The function also returns the starting position in the string for each element. If you use the function to return only the first 4 elements - the rest of the code will be factored out of the final query.
If you have more than 12 columns you can extend to as many as needed. Performance may be affected by the size of the string (8000 vs MAX) and how many elements are parsed - but the only way to be sure will be to test different methods to see which performs best for your data.
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)
In the following screenshot, I would like to merge the data in the YorumYapanAdsoyad column on a single line.
enter image description here
It should be this way;
8 | Fiat Linea 1.3 Multijet | Ahmet, Selami
12 | Vw Golf | Ertem, Selim
Thanks for the help ;)
Try this ,for earlier versions on SQL (2016 and down)
;WITH Tmp (UrunId, Araclar , YorumYapanSoyad) as
(
SELECT 8 , 'Fiat Line 1.3 Multijet' , 'Ahmet'
UNION ALL
SELECT 8 , 'Fiat Line 1.3 Multijet' , 'Selami'
UNION ALL
SELECT 12 , 'Vw Golf' , 'Ertem'
UNION ALL
SELECT 12 , 'Vw Golf' , 'Selim'
)
SELECT UrunId , Araclar ,
(SELECT STUFF(
(SELECT ', ' + YorumYapanSoyad
FROM Tmp b
WHERE B.Araclar = T.Araclar
AND b.UrunId = t.UrunId
FOR XML PATH (''),TYPE).value('.','nvarchar(max)'),1,2,'')
) YorumYapanSoyad
FROM Tmp t
GROUP BY UrunId , Araclar
Try this
DECLARE #Table TABLE (ID INT,Araclar varchar(100),YorumYapan varchar(20))
INSERT INTO #Table
SELECT 8 , 'Fiat Linea 1.3 Multijet' , 'Ahmet' UNION ALL
SELECT 8 , 'Fiat Linea 1.3 Multijet' , 'Selami' UNION ALL
SELECT 12 , 'Vw Golf' , 'Ertem' UNION ALL
SELECT 12 , 'Vw Golf' , 'Selim'
SELECT DISTINCT ID
,Araclar
,STUFF((SELECT ', '+YorumYapan
FROM #Table i WHERE i.ID=o.ID FOR XML PATH ('')),1,1,'') AS YorumYapan
FROM #Table o
Result
ID Araclar YorumYapan
------------------------------------------
8 Fiat Linea 1.3 Multijet Ahmet, Selami
12 Vw Golf Ertem, Selim
This is the recipe I use. As I don't know your table names, I made this example using sys.tables and sys.columns. Basically, the function STUFF is your friend here.
SELECT
t.name,
STUFF
(
(
SELECT ', ' + c.name
FROM sys.columns c
WHERE c.object_id = t.object_id
FOR XML PATH('')
),
1, /*string start*/
2, /*string length*/
'' /*replaceWith*/
)
FROM sys.tables t
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