How to retrieve data from XML column in SQL - sql

I have the following table layout:
OrderDetails:
ItemID (PK, int, not null)
ItemName (nvarchar(450), null)
OrderID (FK, int, not null)
Discounts (nvarchar(max), null)
The Discounts column is declared nvarchar(max) but the data is really XML - no idea why it wasn't declared as XML. I need to query the table to show the OrderID, ItemName, prorated discounts, all other discounts, and total of prorated + all other discounts. Here is an example of some records.
ItemID ItemName OrderID Discounts
8610 Item 1 4227 SEE XML 4227 BELOW
8615 Item 2 4227 <DocumentElement></DocumentElement> //no discounts for this row
8620 Item 3 9387 SEE XML 9387 BELOW
XML OrderId = 4227:
<DocumentElement>
<DiscountsTable>
<DiscountDisplayName>Bundle A</DiscountDisplayName>
<DiscountValue>6.00</DiscountValue>
</DiscountsTable>
<DiscountsTable>
<DiscountDisplayName>Bundle B</DiscountDisplayName>
<DiscountValue>25.00</DiscountValue>
</DiscountsTable>
</DocumentElement>
XML for OrderId = 9387:
<DocumentElement>
<DiscountsTable>
<DiscountDisplayName>Prorated Discount</DiscountDisplayName>
<DiscountValue>6.45</DiscountValue>
</DiscountsTable>
<DiscountsTable>
<DiscountDisplayName>Bundle A</DiscountDisplayName>
<DiscountValue>5.61</DiscountValue>
</DiscountsTable>
<DiscountsTable>
<DiscountDisplayName>Bundle B</DiscountDisplayName>
<DiscountValue>23.39</DiscountValue>
</DiscountsTable>
</DocumentElement>
So, What I need is a query that will return the ItemID, ItemName, aggregated prorated discounts, aggregated bundled discounts, and a total of the discounts added together (prorated + bundled = total discounts). For the 3 records above, the query result should look like this:
Item ID Item Name Prorated Discounts Other Discounts Total Discounts
8610 Item 1 0.00 31.00 31.00
8615 Item 2 0.00 0.00 0.00
8620 Item 3 6.45 29.00 35.45
I've tried using the value() method but I get an error stating it can not be used with nvarchar(max). How do I parse the XML column to pull aggregated values?

You need to convert the column to XML first to use SQLXML features. Something like this.
;with tbl as (
select ItemID, ItemName, OrderID,
convert(xml, Discounts) as Discounts
from OrderDetails
)
select ItemID, ItemName, OrderID,
t.v.value('DiscountDisplayName[1]','varchar(100)') DiscountDisplayName,
t.v.value('DiscountValue[1]','float') DiscountValue
from tbl cross apply Discounts.nodes('DocumentElement/DiscountsTable') t(v)
Elaborate a little
;with tbl as (
select ItemID, ItemName, OrderID,
convert(xml, Discounts) as Discounts
from OrderDetails
),
tbl1 as (
select ItemID, ItemName, OrderID,
t.v.value('(DiscountsTable[DiscountDisplayName="Prorated Discount"]/DiscountValue)[1]','float') Prorated, -- filter
t.v.value('(DiscountsTable[DiscountDisplayName="Bundle A"]/DiscountValue)[1]','float') BundleA,
t.v.value('(DiscountsTable[DiscountDisplayName="Bundle B"]/DiscountValue)[1]','float') BundleB
from tbl cross apply Discounts.nodes('DocumentElement') t(v) --do not go deeper
)
select ItemID, ItemName, OrderID, isnull(Prorated, 0) Prorated,
isnull(BundleA, 0) + isnull(BundleB, 0) Other,
isnull(Prorated, 0) + isnull(BundleA, 0) + isnull(BundleB, 0) Total
from tbl1

I would solve it as follows:
WITH Conv AS
(
SELECT ItemID,ItemName,OrderID,CONVERT(xml,Discounts) XmlVal
FROM Src
)
SELECT ItemID, ItemName, OrderID, Prorated, Other, Prorated+Other Total
FROM Conv
CROSS APPLY
(
SELECT SUM(CASE X.exist('DiscountDisplayName[text() = "Prorated Discount"]')
WHEN 1 THEN X.value('DiscountValue[1]', 'decimal(15,2)')
ELSE 0 END) Prorated,
SUM(CASE X.exist('DiscountDisplayName[text() = "Prorated Discount"]')
WHEN 0 THEN X.value('DiscountValue[1]', 'decimal(15,2)')
ELSE 0 END) Other
FROM XmlVal.nodes('/DocumentElement/DiscountsTable') T(X)
) T(Prorated, Other)
Results:
ItemID ItemName OrderID Prorated Other Total
----------- -------- ----------- ------------ -------- ---------
8610 Item 1 4227 0.00 31.00 31.00
8615 Item 2 4227 NULL NULL NULL
8620 Item 3 9387 6.45 29.00 35.45
You can add COALESCE or ISNULL to remove NULLs.

Related

Query help on sales transaction table

I have a table that contains transaction level sales data. I am trying to satisfy a reporting request as efficiently as possible which I don't think I am succeeding at right now. Here is some test data:
DROP TABLE IF EXISTS TMP_SALES_DATA;
CREATE TABLE TMP_SALES_DATA ([DATE] DATE, [ITEM] INT, [STORE] CHAR(6), [TRANS] INT, [SALES] DECIMAL(8,2));
INSERT INTO TMP_SALES_DATA
VALUES
('9-29-2020',101,'Store1',123,1.00),
('9-29-2020',102,'Store1',123,2.00),
('9-29-2020',103,'Store1',123,3.00),
('9-29-2020',101,'Store1',124,1.00),
('9-29-2020',101,'Store1',125,1.00),
('9-29-2020',103,'Store1',125,3.00),
('9-29-2020',102,'Store1',126,2.00),
('9-29-2020',101,'Store2',88,1.00),
('9-29-2020',102,'Store2',88,2.00),
('9-29-2020',103,'Store2',88,3.00),
('9-29-2020',101,'Store2',89,1.00),
('9-29-2020',101,'Store2',90,1.00),
('9-29-2020',102,'Store2',91,2.00),
('9-29-2020',103,'Store2',91,3.00),
('9-29-2020',101,'Store3',77,1.00);
And I need to represent both individual item sales as well as total transaction sales for every transaction in which the specified items were present. Examples:
-- Item sales
SELECT [ITEM], SUM([SALES]) AS [SALES]
FROM TMP_SALES_DATA
WHERE [ITEM] IN (101,103) AND [STORE] IN ('Store1','Store2' ,'Store3') AND [DATE] = '9-29-2020'
GROUP BY [ITEM]
Returns this:
ITEM SALES
101 7.00
103 12.00
And I can get the total transaction sales in which a single item was present this way:
-- Total transaction sales in which ITEM 101 exists
SELECT SUM(S1.[SALES]) AS [TTL_TRANS_SALES]
FROM TMP_SALES_DATA S1
WHERE EXISTS (SELECT 1 FROM TMP_SALES_DATA S2 WHERE S2.[DATE]=S1.[DATE] AND S2.[STORE]=S1.[STORE] AND S2.[TRANS]=S1.[TRANS] AND S2.[ITEM]=101 AND S2.[STORE] IN ('Store1','Store2','Store3') AND S2.[DATE] = '9-29-2020')
-- Total transaction sales in which ITEM 103 exists
SELECT SUM(S1.[SALES]) AS [TTL_TRANS_SALES]
FROM TMP_SALES_DATA S1
WHERE EXISTS (SELECT 1 FROM TMP_SALES_DATA S2 WHERE S2.[DATE]=S1.[DATE] AND S2.[STORE]=S1.[STORE] AND S2.[TRANS]=S1.[TRANS] AND S2.[ITEM]=103 AND S2.[STORE] IN ('Store1','Store2','Store3') AND S2.[DATE] = '9-29-2020')
But I am failing to find a clean, efficient, and dynamic way to return it all in one query. The end user will be able to specify the items/stores/dates for this report. The end result I would like to see is this:
ITEM SALES TTL_TRANS_SALES
101 7.00 20.00
103 12.00 21.00
If I understand correctly, you can use window functions to summarize by transaction and then aggregate:
select item, sum(sales), sum(trans_sale)
from (select ts.*, sum(sales) over (partition by trans) as trans_sale
from tmp_sales_data ts
) ts
group by item;
Here is a db<>fiddle.
You can add appropriate filtering in the subquery.

SQL JOIN giving doubled values

EDIT ---
SQL FIDDLE sqlfiddle.com/#!18/f08bd/4
I couldn't load all the data into this as running out of limit on SQL FIDDLE but somehow the results are correct there with the little of sample data, but on my database it doubles somehow.
I am trying to display a total quantity of products, their weight and price.
The results come from 4 different tables.
When I do this without join then I get proper values but when I use Join then I get double values on all outputs.
Without join I can't achieve this as these results need to be all in one table.
I tried not using Join and just including those tables in 'FROM' but that thrown me errors about issues converting to numeric. I also tried Union but didn't work.
When using no join and not trying to display all the values, I get the desired output but with missing columns that I missed out on purpose to test this.
SELECT PriceListTest.Description, COUNT(ItemCode) AS Quantity,
SUM(Weight) AS 'Weight', Item.Pieces, PriceListTest.Price,
CAST(SUM( PriceListTest.Price * Weight) as
DECIMAL(10,2)) as 'Unit Price', CAST(SUM(PriceListTest.Price * Weight) as
DECIMAL(10,2))
AS 'Nett Amount'
FROM StockItems
INNER JOIN PriceListTest ON
StockItems.ItemCode = PriceListTest.Description
INNER JOIN Item ON StockItems.ItemCode = Item.ShortCode
WHERE Barcode IN (SELECT DISTINCT Barcode FROM StockOuttbl
WHERE ContainedID = 'isr5063' AND Status ='' GROUP BY Barcode) AND
PriceListTest.CustomerID = (SELECT DISTINCT CustomerID From Customerstbl
WHERE CustomerID ='1')
GROUP BY PriceListTest.Description, Item.Pieces, PriceListTest.Price;
Status in this case is empty, so that's not the issue
I getting these values:
Description Quantity Weight Pieces Price Unit Price Nett Amount
MAJ 52 20242 0 1.23 24897.66 24897.66
FLOCK 50 17206 0 1.23 21163.38 21163.38
This is the output I am looking:
Description Quantity Weight Pieces Price Unit Price Nett Amount
MAJ 26 10121 0 1.23 12448.83‬ 12448.83
‬
FLOCK 25 8603 0 1.23 10581.69 10581.69
When I don't use the PriceListTest in Join then I don't get the doubles, but then it's not exactly what I am looking for.
I get:
Description Quantity Weight Pieces
MAJ 26 10121 0
FLOCK 25 8603 0
EDIT-- Added the data for the tables
PriceListTest---
OID ShortCode Description CustomerID Price
7372 MAJ MAJ 1 1.23
7373 FLOCK FLOCK 1 1.23
StockItems---
TimeStamp DateStamp ItemCode Barcode ID Weight
104414357 20190701 MAJ 20190701104413935 7198 302
125350401 20190701 MAJ 20190701125349979 7220 360
125507063 20190703 MAJ 20190703125506641 7513 336
StockOutTbl---
ID AddedTimeStamp Quant Line UserID Weight Barcode Status Type StockoutTimeStamp StockoutUser TerminalStockOut TerminalAdded AddedDateStamp StockOutDateStamp ContainedID
41 115020205 NULL NULL NULL 336 20190703125506641 NULL 115020208 user 1 TC20 NULL 20190704 20190704 isr5063
Item Table ----
OID ShortCode ScreenCode Description AdminOid Kilos Pieces Inactive CategoryTitleStr BigBale
203 MAJ MAJ MAJ NULL 0 0 0 45 1
204 FLOCK FLOCK FLOCK NULL 0 0 0 45 1
Excuse me for the bad formatting.
I'd appreciate any help with this. Thanks in advance!
use subquery and distinct prcelist from 2nd table in subquery then use join
and there is no need GROUP BY Barcode insside your subquery cause you alredy used distinct
SELECT PriceListTest.Description, COUNT(ItemCode) AS Quantity,
SUM(Weight) AS 'Weight', Item.Pieces, PriceListTest.Price,
CAST(SUM( PriceListTest.Price * Weight) as
DECIMAL(10,2)) as 'Unit Price', CAST(SUM(PriceListTest.Price * Weight) as
DECIMAL(10,2))
AS 'Nett Amount'
FROM StockItems
INNER JOIN( select distinct * from PriceListTest) as PriceListTest ON
StockItems.ItemCode = PriceListTest.Description
INNER JOIN Item ON StockItems.ItemCode = Item.ShortCode
WHERE Barcode IN (SELECT DISTINCT Barcode FROM StockOuttbl
WHERE ContainedID = 'isr5063' AND Status ='' ) AND
PriceListTest.CustomerID = (SELECT DISTINCT CustomerID From Customerstbl
WHERE CustomerID ='1')
GROUP BY PriceListTest.Description, Item.Pieces, PriceListTest.Price;

Query to get output for stock

I need output through query as per given below points
there is stock table contains all items with multiple salemrp
I need those records whose stock is present in multiple salemrp, but if there is only one salemrp for an item, I require also those records.
My query is as follows, please help me, how to manipulate this query.
SELECT
itemid,
grpid,
(SELECT
itemname
FROM sap_itemmASter
WHERE itemid=sap_stockmASter.itemid )AS itemname,
(SELECT grpname
FROM sap_grpmASter
WHERE grpid=sap_stockmASter.grpid) AS grpname,
(SELECT partno
FROM sap_itemmASter
WHERE itemid=sap_stockmASter.itemid) AS partno,
salemrp,
(SELECT brANDname
FROM sap_brANDmASter
WHERE brANDid IN (SELECT brANDid
FROM sap_itemmASter
WHERE itemid=sap_stockmASter.itemid)) AS catname,
(laneno + ' - ' + rackno) AS locno,
isnull(SUM(stkqty),0) AS balqty,
(SELECT top 1 (laneno + ' - ' + rackno)
FROM sap_stockloc
WHERE itemid=sap_stockmASter.itemid) AS storeloc
FROM sap_stockmASter
WHERE laneno!=''
AND itemid>0
AND grpid IN (SELECT grpid
FROM sap_grpmASter
WHERE isactive=1
AND isdel=1)
AND itemid IN (SELECT itemid
FROM sap_itemmASter
WHERE isdel=1
AND isactive=1
AND laneno=sap_stockmASter.laneno
AND rackno=sap_stockmASter.rackno)
AND grpid=37
GROUP BY itemid,grpid,laneno,rackno,salemrp
ORDER BY itemname
More information:
Item Name SaleMrp Qty
ABC 158.00 48
ABC 165.00 -11
ABC 170.00 5
In this I want to not display negative stock, but
XYZ 125.00 0
(I need this record as well, beacause it has only one mrp)
PQR 100.00 -5
(I need this record as well, beacause it has only one mrp)

How to query SQLite to find sum, latest record, grouped by id, in between dates?

Items (itemId, itemName)
Logs (logId, itemId, qtyAdded, qtyRemoved, availableStock, transactionDate)
Sample Data for Items:
itemId itemName
1 item 1
2 item 2
Sample Data for Logs:
logid itemId qtyAdded qtyRemoved avlStock transDateTime
1 2 5405 0 5405 June 1 (4PM)
2 2 1000 0 6405 June 2 (5PM)
3 2 0 6000 405 June 3 (11PM)
I need to see all items from Items table and their SUM(qtyAdded), SUM(qtyRemoved), latest availableStock (there's an option for choosing the range of transactionDate but default gets all records). Order of date in final result does not matter.
Preferred result: (without date range)
itemName qtyAddedSum qtyRemovedSum avlStock
item 1 6405 6000 405
item 2 <nothing here yet>
With date Range between June 2 (8AM) and June 3 (11:01PM)
itemName qtyAddedSum qtyRemovedSum avlStock
item 1 1000 6000 405
item 2 <no transaction yet>
So as you can see, final result is grouped which makes almost all my previous query correct except my availableStock is always wrong. If I focus in the availableStock, I can't get the two sums.
you could use group by sum, and between
select itemName, sum(qtyAdded), sum(qtyRemoved), sum(avlStock)
from Items
left join Logs on logs.itemId = items.itemId
where transDateTime between '2017-06-02 08:00:00' and '2017-06-03 23:00:00'
group by itemId
or
If you need the last avlStock
select itemName, sum(qtyAdded), sum(qtyRemoved), tt.avlStock
from Items
left join Logs on logs.itemId = items.itemId
INNER JOIN (
select logid,avlStock
from logs
inner join (
select itemId, max(transDateTime) max_trans
from Logs
group by itemId
) t1 on logs.itemId = t1.ItemId and logs.transDateTime = t1.max_trans
) tt on tt.logId = Logs.itemId
where transDateTime between '2017-06-02 08:00:00' and '2017-06-03 23:00:00'
group by itemId
Okay, I tried both of these and they worked, can anyone confirm if these are already efficient or if there are some more efficient answers there.
SELECT * FROM Items LEFT JOIN
(
SELECT * FROM Logs LEFT JOIN
(
SELECT SUM(qtyAdd) AS QtyAdded, SUM(qtySub) AS QtyRemoved, availableStock AS Stock
FROM Logs WHERE transactionDate BETWEEN julianday('2017-07-18 21:10:40')
AND julianday('2017-07-18 21:12:00') GROUP BY itemId
)
ORDER BY transactionDate DESC
)
USING (itemId) GROUP BY itemName;
SELECT * FROM Items LEFT JOIN
(
SELECT * FROM Logs LEFT JOIN
(
SELECT SUM(qtyAdd) AS QtyAdded, SUM(qtySub) AS QtyRemoved, availableStock AS Stock
FROM Logs GROUP BY itemId
)
ORDER BY transactionDate DESC
)
USING (itemId) GROUP BY itemName;

Reverse/Blow out a GROUP BY

I am working with data that is grouped by item number with counts. Each record with a count > 2 needs to be blown out into individual records and compared at that level to a different set of data.
The data looks like this (It is stuck in this format. This is the only way the customer can send it.):
OwnerNumber ItemCode ItemNumber CountOfItems
1234 Item1 Item1-001 3
1234 Item1 Item1-002 1
1234 Item1 Item1-003 2
1234 Item2 Item2-001 1
And I need the data formatted like this (dynamically without hardcoding for value of CountOfItems):
OwnerNumber ItemCode ItemNumber
1234 Item1 Item1-001
1234 Item1 Item1-001
1234 Item1 Item1-001
1234 Item1 Item1-002
1234 Item1 Item1-003
1234 Item1 Item1-003
1234 Item2 Item2-001
For some reason I just can't wrap my head around a clean way to do this (or any way).
You can manage with a Common Table Expression
WITH CTE AS (
SELECT OwnerNumber,ItemCode,ItemNumber,CountOfItems FROM table
UNION ALL SELECT OwnerNumber,ItemCode,ItemNumber,CountOfItems-1
FROM CTE
WHERE CountOfItems >= 2
)
SELECT OwnerNumber,ItemCode,ItemNumber
FROM CTE
ORDER BY ItemNumber
OPTION (MAXRECURSION 0);
Edit:
Added MAXRECURSION to handle situations where CountOfItems exceeds default max recursions as pointed out by Dev_etter
Hmmm.... I think I like recursive CTEs for this:
WITH Data (OwnerNumber, ItemCode, ItemNumber, CountOfItems) as (
SELECT OwnerNumber, ItemCode, ItemNumber, CountOfItems
FROM OriginalTable
UNION ALL
SELECT OwnerNumber, ItemCode, ItemNumber, CountOfItems - 1
FROM Data
WHERE CountOfItems > 1)
SELECT OwnerNumber, ItemCode, ItemNumber
FROM Data
ORDER BY OwnerNumber, ItemCode, ItemNumber
You can avoid recursion with the query below, and I think will be more efficient. Here, the table N is any table with at least as many rows as the largest CountOfItems value.
This is a rare example of a query where TOP without ORDER BY is not a bad idea.
select
OwnerNumber,
ItemCode,
ItemNumber
from t
cross apply (
select top (CountOfItems) null
from N
) as N(c)