How can I solve this query in sql server? - sql

Sql Fiddle example
I have this table structure:
CREATE TABLE IF NOT EXISTS `client` (
`id` int(6) unsigned NOT NULL,
`name` varchar(200),
`balance` decimal NOT NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARSET=utf8;
INSERT INTO `client` (`id`, `name`, `balance`) VALUES
('1', 'Pepito', '500');
CREATE TABLE IF NOT EXISTS `balance_Movements` (
`id` int(6) unsigned NOT NULL,
`clientId` int(6),
`movementType` varchar(200),
`import` decimal NOT NULL,
`dateMovement` datetime,
PRIMARY KEY (`id`)
) DEFAULT CHARSET=utf8;
INSERT INTO `balance_Movements` (`id`, `clientid`, `movementType`, `import`, `dateMovement`) VALUES
('1', '1', 'payment', '50', '2018/05/11'),
('2', '1', 'refund', '25.05', '2018/05/10'),
('3', '1', 'refund', '60', '2018/04/06'),
('4', '1', 'payment', '100', '2018/04/03');
Client has at the start : 500€ --> so the declare variable will be like this:
declare #total_balance as decimal;
set #total_balance = (select balance from client where id = 1);
Result
------
500
I need to change after the result of #total_balance, taking the value for the last row:
Example:
Table_balance_Movements
------------------------
Total
-----
450
475.05
535.05
435.05
Explanation:
450 --> The client start with 500, so if the type of movement is payment I need to substract the balance of client to the import movement, and save the data in #total_balance to use it later = 500 - 50 = 450
475.05 --> I get the value for #total_balance and sum 25.05 because in this line the type of movement is refund = 450 + 25.05 = 475.05
535.05 --> the same thing, I get the value from the #total_balance variable and see what type of movement is and substract or sum the import = 475.05 + 60 = 535.05
435.05 --> 535.05 - 100 = 435.05
I want to do something similar to this concept:
declare #total_balance as decimal;
set #total_balance = (select balance from client where id = 1);
select (case when movementType = 'payment' then (#total_balance = #total_balance - import)
when movementType = 'refund' then (#total_balance = #total_balance + import) end) as total
from balance_Movements;
it would be possible? thanks

You can solve this with the cumulative sum window functions. The following will work in SQL Server and MySQL 8+
select c.*,
(c.balance +
sum(case when bm.movement_type = 'payment' then - import
when bm.movement_type = 'refund' then import
end) over (partition by c.id order by bm.datemovement)
) as net_balance
from client c join
balance_movements bm
on bm.clientid = c.id

I think the simplest way is using SUM with condition aggregate function window function to get accumulate the add with balance
SELECT c.balance +SUM(CASE WHEN movementType = 'payment' THEN - import
WHEN movementType = 'refund' THEN import
ELSE 0 END
) OVER(ORDER BY b.id) Total
FROM client c
JOIN balance_Movements b on c.id = b.clientid
But if your dbms didn't support window function, you can try to use correctly subquery in select
SELECT c.balance +
(
SELECT SUM(CASE WHEN movementType = 'payment' THEN - import
WHEN movementType = 'refund' THEN import
ELSE 0 END)
FROM balance_Movements bb
WHERE bb.id <= b.id and b.clientid = bb.clientid
) Total
FROM client c
JOIN balance_Movements b on c.id = b.clientid
sqlfiddle

You could just add the sum of refunds and subtract the sum of payments like this:
SELECT 500 +
(SELECT SUM(import) FROM balance_Movements WHERE movementType = 'refund')
- (SELECT SUM(import) FROM balance_Movements WHERE movementType = 'payment') AS total
Result: 435.05

Give this a shot...
IF OBJECT_ID('tempdb..#balance_Movements', 'U') IS NOT NULL
BEGIN DROP TABLE #balance_Movements; END;
CREATE TABLE #balance_Movements (
id INT NOT NULL,
clientId INT,
movementType VARCHAR (200),
import DECIMAL (9, 2) NOT NULL,
dateMovement DATETIME,
PRIMARY KEY (id)
);
INSERT INTO #balance_Movements (id, clientid, movementType, import, dateMovement) VALUES
('1', '1', 'payment', '50', '2018/05/11'),
('2', '1', 'refund', '25.05', '2018/05/10'),
('3', '1', 'refund', '60', '2018/04/06'),
('4', '1', 'payment', '100', '2018/04/03');
--=============================================================
DECLARE #_start DECIMAL(9,2) = 500;
SELECT
*,
running_total = #_start - SUM(CASE WHEN bm.movementType = 'refund' THEN -1 * bm.import ELSE bm.import END) OVER (PARTITION BY bm.clientId ORDER BY bm.dateMovement desc)
FROM
#balance_Movements bm;
Results...
id clientId movementType import dateMovement running_total
----------- ----------- ------------ ---------- ----------------------- --------------
1 1 payment 50.00 2018-05-11 00:00:00.000 450.00
2 1 refund 25.05 2018-05-10 00:00:00.000 475.05
3 1 refund 60.00 2018-04-06 00:00:00.000 535.05
4 1 payment 100.00 2018-04-03 00:00:00.000 435.05

Related

Designing a slowly changing dimension type 2 script with postgresql

Let's say I have the following target table:
CREATE TABLE DimCustomer (
CustomerKey serial PRIMARY KEY,
CustomerNum int NOT NULL,
CustomerName varchar(25) NOT NULL,
Planet varchar(25) NOT NULL,
RowIsCurrent char(1) NOT NULL DEFAULT 'Y',
RowStartDate date NOT NULL DEFAULT CURRENT_TIMESTAMP,
RowEndDate date NOT NULL DEFAULT '12/31/9999'
);
INSERT INTO DimCustomer
(CustomerNum, CustomerName, Planet, RowStartDate)
VALUES (101,'Anakin Skywalker', 'Tatooine', CURRENT_TIMESTAMP - INTERVAL '101 days'),
(102,'Yoda', 'Coruscant', CURRENT_TIMESTAMP - INTERVAL '100 days'),
(103,'Obi-Wan Kenobi', 'Coruscant', CURRENT_TIMESTAMP - INTERVAL '100 days')
And I have a following staging table:
CREATE TABLE Staging_DimCustomer
(
CustomerNum int NOT NULL,
CustomerName varchar(25) NOT NULL,
Planet varchar(25) NOT NULL,
ChangeDate date NOT NULL DEFAULT CURRENT_TIMESTAMP,
RankNo int NOT NULL DEFAULT 1
)
INSERT INTO Staging_DimCustomer(CustomerNum, CustomerName, Planet, ChangeDate)
VALUES
(103,'Ben Kenobi', 'Coruscant', CURRENT_TIMESTAMP - INTERVAL '99 days')
In the staging table, it looks like 'Obi-Wan Kenobi'(customernum 103) changed his name to
'Ben Kenobi'. I want to create a script that implements scd type 2 and produces the following result(slowly changing dimension type 2):
Following is my attempt:
INSERT INTO DimCustomer (
CustomerNum, CustomerName, Planet, RowIsCurrent, RowStartDate, RowEndDate
)
select CustomerNum, CustomerName, Planet, 'Y', ChangeDate, '12/31/9999'
from Staging_DimCustomer
ON CONFLICT (CustomerNum) and RowIsCurrent = 'Y'
DO UPDATE SET
CustomerName = EXCLUDED.CustomerName,
Planet = EXCLUDED.Planet,
RowIsCurrent = 'N',
RowEndDate = EXCLUDED.ChangeDate
I dont know how to look for the values that changed, update the existing rows to retire it and then insert the new rows with rowiscurrent = 'Y' flag. I am trying to model my solution based on this sql server article
http://www.made2mentor.com/2013/08/how-to-load-slowly-changing-dimensions-using-t-sql-merge/.
Assuming the changes are all on the most current row, then you can update the current row and then insert:
with u as (
update dimCustomer c
set RowIsCurrent = 'N',
RowEndDate = sc.ChangeDate
from Staging_DimCustomer sc
where sc.CustomerNum = c.CustomerNum and
c.RowIsCurrent = 'Y'
)
insert into dimCustomer (CustomerNum, CustomerName, Planet, RowIsCurrent, RowStartDate, RowEndDate
)
select CustomerNum, CustomerName, Planet, 'Y', ChangeDate, '9999-12-31'::date
from Staging_DimCustomer sc;
This assumes that the changes take place on the most current record. It is rather trickier to implement historic changes, and I'm guessing that is not necessary.
Note that you might want an additional check that the row being inserted is actually different from the current row.
EDIT:
If you want to avoid changes for rows that already exist, you can do:
with sc as (
select *
from Staging_DimCustomer
where not exists (select 1
from DimCustomer c
where c.CustomerNum = sc.CustomerNum and
c.CustomerName = sc.CustomerName and
. . . -- whatever other columns you want to check
)
),
u as (
update dimCustomer c
set RowIsCurrent = 'N',
RowEndDate = sc.ChangeDate
from sc
where sc.CustomerNum = c.CustomerNum and
c.RowIsCurrent = 'Y'
)
insert into dimCustomer (CustomerNum, CustomerName, Planet, RowIsCurrent, RowStartDate, RowEndDate
)
select CustomerNum, CustomerName, Planet, 'Y', ChangeDate, '9999-12-31'::date
from sc;
I think this should work fine , not updating or inserting the already existing records:
with us as (
update dimCustomer c
set RowIsCurrent = 'N',
RowEndDate = sc.ChangeDate
from Staging_DimCustomer sc
where sc.CustomerNum = c.CustomerNum and
c.RowIsCurrent = 'Y' and
sc.customername <> c.customername
),
u as (
select stg.customernum,stg.customername,stg.planet ,stg.changedate from Staging_DimCustomer stg
Inner join DimCustomer dim on dim.customernum=stg.customernum and dim.rowiscurrent='Y'
and (dim.customername <> stg.customername
or dim.planet <> stg.planet
)
UNION
select stg.customernum,stg.customername,stg.planet ,stg.changedate from Staging_DimCustomer stg
where stg.customernum not IN(select dim.customernum from DimCustomer dim where dim.rowiscurrent='Y')
)
insert into dimCustomer (CustomerNum, CustomerName, Planet, RowIsCurrent, RowStartDate, RowEndDate
)
select CustomerNum, CustomerName, Planet, 'Y', ChangeDate, '9999-12-31'::date
from u ;

Opening and Closing Balance Sum

I have this table and sample data and I want to calculate the opening and closing balance. I want it to be in 6 column
i.e CreditOpening, DebitOpening, Credit, Debit, CreditClosing, DebitClosing
Here is the table structure and sample data
DROP TABLE Transactions;
CREATE TABLE Transactions
(
ID INT,
COATitle VARCHAR(35),
ConfigurationCode INT,
DebitAmount NUMERIC,
CreditAmount NUMERIC,
TransactionDate Date
)
INSERT INTO Transactions VALUES (1, 'Sales', 24, '2400', NULL, '2018-08-24');
INSERT INTO Transactions VALUES (2, 'Items', 24, NULL, '1200', '2018-08-24');
INSERT INTO Transactions VALUES (3, 'Bank', 24, NULL, '1200', '2018-08-24');
INSERT INTO Transactions VALUES (4, 'Meezan', 24, '1500', NULL, '2018-08-25');
INSERT INTO Transactions VALUES (5, 'Items', 24, NULL, '1500', '2018-08-25');
INSERT INTO Transactions VALUES (6, 'Bank', 24, NULL, '1200', '2018-08-26');
INSERT INTO Transactions VALUES (7, 'Sales', 24, '5400', NULL, '2018-08-26');
INSERT INTO Transactions VALUES (8, 'Items', 24, NULL, '1200', '2018-08-26');
INSERT INTO Transactions VALUES (9, 'Bank', 24, NULL, '3000', '2018-08-26');
I have this query and it's output as below:
;WITH CTE AS (
SELECT *
FROM
Transactions
)
SELECT
COATitle, SUM([D].[DebitAmount]) DrAmount, SUM([D].[CreditAmount]) CrAmount
FROM(
SELECT *,
SUM(ISNULL(NULLIF(DebitAmount, 0), 0)-ISNULL(NULLIF(CreditAmount, 0), 0)) OVER (PARTITION BY CONFIGURATIONCODE ORDER BY ID) as Amount
FROM CTE
WHERE [TransactionDate] BETWEEN CAST('Aug 25 2018 11:21AM' AS DATE) AND CAST('Aug 25 2018 11:21AM' AS DATE)
)D
GROUP BY COATitle
OutPut :
COATitle DrAmount CrAmount
Items NULL 1500
Meezan 1500 NULL
Now the data should look like this
COATitle OpeningDebit OpeningDebit DrAmount CrAmount ClosingDebit ClosingCredit
Bank 0 0 NULL 1200 0 1200
Items 0 0 NULL 1200 0 1200
Sales 0 0 2400 NULL 2400 0
But once I run the query between dates 25 and 26 the result should be something like this
COATitle OpeningDebit OpeningCredit DrAmount CrAmount ClosingDebit ClosingCredit
Bank 0 1200 NULL 4200 0 5400
Items 0 1200 NULL 2700 0 3900
Sales 0 0 5400 NULL 7800 0
Meezan 0 0 1500 NULL 1500 0
Meezan will not have opening balance as there was no in previous date. Now in case if there is Debit amount is given for any COATitle which was given Credit in previous dates it will subtract Credit From Debit.
You don't need the CTE in this case. It is redundant.
You don't need to convert the date in string to date data type. Just specify the date in ISO format YYYY-MM-DD will do.
I am may be wrong but your expected data does not matches with the sample data.
Note : i excluded the ConfigurationCode in the query as i am not sure how that column play a part in your requirement.
DECLARE #Date_Fr DATE = '2018-08-25',
#Date_To DATE = '2018-08-25'
SELECT t.COATitle,
OpeningDebit = ISNULL(o.OpeningDebit, 0),
OpeningCredit = ISNULL(o.OpeningCredit, 0),
t.DrAmount, t.CrAmount,
ClosingDebit = ISNULL(o.OpeningDebit, 0) + t.DrAmount,
ClosingCredit = ISNULL(o.OpeningCredit, 0) + t.CrAmount
FROM (
SELECT COATitle,
DrAmount = SUM(CASE WHEN [TransactionDate] >= #Date_Fr THEN DebitAmount ELSE 0 END),
CrAmount = SUM(CASE WHEN [TransactionDate] >= #Date_Fr THEN CreditAmount ELSE 0 END)
FROM Transactions t
WHERE [TransactionDate] <= #Date_To
GROUP BY COATitle
) t
OUTER APPLY
(
SELECT OpeningDebit = SUM(DebitAmount), OpeningCredit = SUM(CreditAmount)
FROM Transactions x
WHERE x.COATitle = t.COATitle
AND x.[TransactionDate] < #Date_Fr
) o
WHERE o.OpeningDebit IS NOT NULL
OR o.OpeningCredit iS NOT NULL
This may help. I put additional check, because I don't know if situation when opening balance is debit and closing is credit for example, is valid.
DECLARE
#from date = '25-08-2018',
#to date = '26-08-2018'
;
WITH Items (COATitle) AS (
SELECT DISTINCT COATitle
FROM dbo.[Transactions]
), OpeningBalance (COATitle, OpeningAmount) AS (
SELECT COATitle, SUM(ISNULL(DebitAmount, 0)) - SUM(ISNULL(CreditAmount, 0))
FROM dbo.[Transactions]
WHERE TransactionDate < #from
GROUP BY COATitle
), DebitCredit(COATitle, DebitAmount, CreditAmount) AS (
SELECT COATitle, SUM(ISNULL(DebitAmount, 0)), SUM(ISNULL(CreditAmount, 0))
FROM dbo.[Transactions]
WHERE (#from <= TransactionDate) AND (TransactionDate <= #to)
GROUP BY COATitle
)
SELECT
i.COATitle,
OpeningDebitAmount = (CASE WHEN SUM(ISNULL(ob.OpeningAmount, 0)) < 0 THEN 0 ELSE SUM(ISNULL(ob.OpeningAmount, 0)) END),
OpeningCreditAmount = (CASE WHEN SUM(ISNULL(ob.OpeningAmount, 0)) < 0 THEN -SUM(ISNULL(ob.OpeningAmount, 0)) ELSE 0 END),
DebitAmount = SUM(ISNULL(dc.DebitAmount, 0)),
CreditAmount = SUM(ISNULL(dc.CreditAmount, 0)),
ClosingDebitAmount = (CASE WHEN SUM(ISNULL(ob.OpeningAmount,0)+ISNULL(dc.DebitAmount,0)-ISNULL(dc.CreditAmount, 0)) < 0 THEN 0 ELSE SUM(ISNULL(ob.OpeningAmount,0)+ISNULL(dc.DebitAmount,0)-ISNULL(dc.CreditAmount, 0)) END),
ClosingCreditAmount = (CASE WHEN SUM(ISNULL(ob.OpeningAmount,0)+ISNULL(dc.DebitAmount,0)-ISNULL(dc.CreditAmount, 0)) < 0 THEN -SUM(ISNULL(ob.OpeningAmount,0)+ISNULL(dc.DebitAmount,0)-ISNULL(dc.CreditAmount, 0)) ELSE 0 END)
FROM Items i
LEFT JOIN OpeningBalance ob ON (i.COATitle = ob.COATitle)
LEFT JOIN DebitCredit dc ON (i.COATitle = dc.COATitle)
GROUP BY i.COATitle
Here's a possible solution for you. First, sample data:
declare #Transactions table
(
ID int,
COATitle varchar(35),
ConfigurationCode int,
DebitAmount money,
CreditAmount money,
TransactionDate date
);
insert #Transactions values
(1, 'Sales', 24, 2400, NULL, '20180824'),
(2, 'Items', 24, NULL, 1200, '20180824'),
(3, 'Bank', 24, NULL, 1200, '20180824'),
(4, 'Meezan', 24, 1500, NULL, '20180825'),
(5, 'Items', 24, NULL, 1500, '20180825'),
(6, 'Bank', 24, NULL, 1200, '20180826'),
(7, 'Sales', 24, 5400, NULL, '20180826'),
(8, 'Items', 24, NULL, 1200, '20180826'),
(9, 'Bank', 24, NULL, 3000, '20180826');
A couple of things to note here. There's no need to use quotes to delimit numeric literals as you've done in the original question, and I would generally advise against using the numeric data type without specifying the precision and scale explicitly. I've chosen the money type for debit and credit amounts instead.
Next, I'll use a couple of local variables to control the operation of the query. For your first test case we'll use:
declare #BeginDate date = '20180801';
declare #EndDate date = '20180824';
Here's the implementation:
with RawDataCTE as
(
select
T.COATitle,
OpeningDebit = coalesce(sum(case when T.TransactionDate < #BeginDate then T.DebitAmount end), 0),
OpeningCredit = coalesce(sum(case when T.TransactionDate < #BeginDate then T.CreditAmount end), 0),
DrAmount = sum(case when T.TransactionDate between #BeginDate and #EndDate then T.DebitAmount end),
CrAmount = sum(case when T.TransactionDate between #BeginDate and #EndDate then T.CreditAmount end)
from
#Transactions T
group by
T.COATitle
)
select
R.COATitle,
R.OpeningDebit,
R.OpeningCredit,
R.DrAmount,
R.CrAmount,
ClosingDebit = R.OpeningDebit + coalesce(R.DrAmount, 0),
ClosingCredit = R.OpeningCredit + coalesce(R.CrAmount, 0)
from
RawDataCTE R
where
R.OpeningDebit > 0 or
R.OpeningCredit > 0 or
R.DrAmount > 0 or
R.CrAmount > 0;
The CTE groups everything by COATitle, and as I've assumed from your desired results, produces non-null results for the opening balances but may produce null results for the sum of debits and credits that fall within the desired time frame. The CTE doesn't try to decide which COATitle records should be included or excluded because we need to have the data aggregated before making that decision.
The query outside the CTE builds the closing balances from the CTE data and omits any COATitle for which there are no opening balances and no entries made during the time period (and therefore no closing balances). When I run the query for 2018-08-01 through 2018-08-24, the result is:
And for 2018-08-25 through 2018-08-26, the result set is:
Technically you could do this without a CTE if you wanted; you'd just have to use a HAVING clause instead of a WHERE to determine which accounts are to be included, and you'd have to define ClosingDebit and ClosingCredit independently rather than as sums of the other fields in the result set. For instance, this will work:
select
T.COATitle,
OpeningDebit = coalesce(sum(case when T.TransactionDate < #BeginDate then T.DebitAmount end), 0),
OpeningCredit = coalesce(sum(case when T.TransactionDate < #BeginDate then T.CreditAmount end), 0),
DrAmount = sum(case when T.TransactionDate between #BeginDate and #EndDate then T.DebitAmount end),
CrAmount = sum(case when T.TransactionDate between #BeginDate and #EndDate then T.CreditAmount end),
ClosingDebit = coalesce(sum(case when T.TransactionDate <= #EndDate then T.DebitAmount end), 0),
ClosingCredit = coalesce(sum(case when T.TransactionDate <= #EndDate then T.CreditAmount end), 0)
from
#Transactions T
group by
T.COATitle
having
sum(case when T.TransactionDate <= #EndDate and (T.DebitAmount > 0 or T.CreditAmount > 0) then 1 else 0 end) > 0;
I find the CTE version a little easier to read and understand, but your mileage may vary.

sql compute difference between 2 rows

I'm looking for a methodology to compare the difference between 2 rows in the same table. From what I found here (How to get difference between two rows for a column field?) it's almost what I wanted. I have done the following code:
create table #tmpTest
(
id_fund int null,
id_ShareType int null,
ValueDate datetime null,
VarNAV float null,
FundPerf float null,
)
insert into #tmpTest(id_fund, id_ShareType, ValueDate, VarNAV)
values(1,1,'20140101',100)
insert into #tmpTest(id_fund, id_ShareType, ValueDate, VarNAV)
values(1,1,'20140102',20)
update #tmpTest
set hrc.FundPerf = (isnull(hrn.VarNAV, 0) - hrc.VarNAV)/hrc.VarNAV
from #tmpTest hrc
left join #tmpTest hrn on hrn.ValueDate = (select min(ValueDate) from #tmpTest where ValueDate > hrc.ValueDate)
and hrc.id_fund = hrn.id_fund and hrc.id_ShareType = hrn.id_ShareType
My issue is that the result I'm computing starts on line 1 instead of line 2.
Hereunder the result I'm obtaining:
id_fund id_ShareType ValueDate VarNAV FundPerf
------- ------------ ------------------- ------- -----------------------------
1 1 2014-01-01 00:00:00 100 -0.8
1 1 2014-01-02 00:00:00 20 -1
whereas I'd like it to be that way:
id_fund id_ShareType ValueDate VarNAV FundPerf
------- ------------ ------------------- ------- -----------------------------
1 1 2014-01-01 00:00:00 100 -1
1 1 2014-01-02 00:00:00 20 -0.8
What's wrong with my approach?
You are not restricting the minimum to the same fund and share type.
update #tmpTest
set hrc.FundPerf = (isnull(hrn.VarNAV, 0) - hrc.VarNAV)/hrc.VarNAV
from #tmpTest hrc left join
#tmpTest hrn
on hrn.ValueDate = (select min(ValueDate)
from #tmpTest tt
where tt.ValueDate > hrc.ValueDate and
hrc.id_fund = tt.id_fund and hrc.id_ShareType = tt.id_ShareType
) and
hrc.id_fund = hrn.id_fund and hrc.id_ShareType = hrn.id_ShareType ;
Try this:
update hrn
set FundPerf = (isnull(hrn.VarNAV, 0) - hrc.VarNAV)/hrc.VarNAV
from #tmpTest hrc
left join #tmpTest hrn on hrn.ValueDate = (select min(ValueDate) from #tmpTest where ValueDate > hrc.ValueDate)
and hrc.id_fund = hrn.id_fund and hrc.id_ShareType = hrn.id_ShareType
Hi you can achieve this using by CTE (Common Table Expression)
create table #tmpTest
(
id_fund int null,
id_ShareType int null,
ValueDate datetime null,
VarNAV float null,
FundPerf float null,
)
insert into #tmpTest(id_fund, id_ShareType, ValueDate, VarNAV)
values(1,1,'20140101',100)
insert into #tmpTest(id_fund, id_ShareType, ValueDate, VarNAV)
values(1,1,'20140102',20)
;With tbl as
( Select Row_Number() OVER (Order by T.ValueDate) as RowNumber,*
From #tmpTest T
)SELECT Cur.*,(ISNULL(Cur.VarNAV,0) - ISNULL(Prv.VarNAV,0))/Prv.VarNAV as [Col Name]
FROM tbl Cur
LEFT OUTER JOIN tbl Prv ON Cur.RowNumber = Prv.RowNumber+1
ORDER BY Cur.ValueDate

How do I write this crosstab type query in Linq (tables & data provided)

This is a simple sort of crosstab query. I am having difficulty transferring my SQL knowledge to Linq and because it is hard to test (I have not gotten linqpad running properly for WP7 development) I can't really "play" easily.
CREATE TABLE Store
(
StoreID INT NOT NULL,
StoreName VARCHAR(10) NOT NULL
)
INSERT INTO Store (StoreID,StoreName) VALUES (1,'Store A')
INSERT INTO Store (StoreID,StoreName) VALUES (2,'Store B')
CREATE TABLE ProductType
(
ProductTypeID INT NOT NULL,
ProductTypeName VARCHAR(20) NOT NULL
)
INSERT INTO ProductType (ProductTypeID,ProductTypeName) VALUES (1,'Clothing')
INSERT INTO ProductType (ProductTypeID,ProductTypeName) VALUES (2,'Food')
CREATE TABLE Product
(
ProductID INT NOT NULL,
ProductTypeID INT NOT NULL,
ProductName VARCHAR(20) NOT NULL
)
INSERT INTO Product (ProductID,ProductTypeID,ProductName) VALUES (1,1,'Hat')
INSERT INTO Product (ProductID,ProductTypeID,ProductName) VALUES (2,1,'Shirt')
INSERT INTO Product (ProductID,ProductTypeID,ProductName) VALUES (3,1,'Pant')
INSERT INTO Product (ProductID,ProductTypeID,ProductName) VALUES (4,2,'Orange')
INSERT INTO Product (ProductID,ProductTypeID,ProductName) VALUES (5,2,'Apple')
CREATE TABLE Purchase
(
PurchaseID INT NOT NULL,
ProductID INT NOT NULL,
StoreID INT NOT NULL,
Quantity INT NULL,
Amount INT NULL
)
INSERT INTO Purchase (PurchaseID,ProductID,StoreID,Quantity,Amount) VALUES (1,1,1,5,10)
INSERT INTO Purchase (PurchaseID,ProductID,StoreID,Quantity,Amount) VALUES (2,2,1,4,12)
INSERT INTO Purchase (PurchaseID,ProductID,StoreID,Quantity,Amount) VALUES (3,3,1,1,10)
INSERT INTO Purchase (PurchaseID,ProductID,StoreID,Quantity,Amount) VALUES (4,4,1,3,16)
INSERT INTO Purchase (PurchaseID,ProductID,StoreID,Quantity,Amount) VALUES (5,5,1,7,12)
INSERT INTO Purchase (PurchaseID,ProductID,StoreID,Quantity,Amount) VALUES (6,1,2,10,22)
INSERT INTO Purchase (PurchaseID,ProductID,StoreID,Quantity,Amount) VALUES (7,2,2,8,26)
SELECT s.StoreName
,SUM(CASE
WHEN t.ProductTypeID=1 THEN Amount
ELSE 0
END ) as ClothingAmount
,SUM(CASE
WHEN t.ProductTypeID=2 THEN Amount
ELSE 0
END )AS FoodQuantity
,SUM(CASE
WHEN t.ProductTypeID=1 THEN Quantity
ELSE 0
END ) as ClothingAmount
,SUM(CASE
WHEN t.ProductTypeID=2 THEN Quantity
ELSE 0
END )AS FoodQuantity
FROM Purchase u
INNER JOIN Product r
ON u.ProductID=r.ProductID
INNER JOIN ProductType t
ON r.ProductTypeID=t.ProductTypeID
INNER JOIN Store s
ON s.StoreID=u.StoreID
GROUP BY s.StoreName
The following is a 1:1 translation of your SQL query statement in LINQ:
from u in Purchases
join r in Products on u.ProductID equals r.ProductID
join t in ProductTypes on r.ProductTypeID equals t.ProductTypeID
join s in Stores on u.StoreID equals s.StoreID
group new {Purchase = u, Product = r, ProductType = t, Store = s} by s.StoreName into grouped
select new {
StoreName = grouped.Key,
ClothingAmount = grouped.Sum(entry => entry.ProductType.ProductTypeID == 1 ? entry.Purchase.Amount : 0),
FoodAmount = grouped.Sum(entry => entry.ProductType.ProductTypeID == 2 ? entry.Purchase.Amount : 0),
ClothingQuantity = grouped.Sum(entry => entry.ProductType.ProductTypeID == 1 ? entry.Purchase.Quantity : 0),
FoodQuantity = grouped.Sum(entry => entry.ProductType.ProductTypeID == 2 ? entry.Purchase.Quantity : 0)
}
which gets translated into the following SQL:
SELECT SUM(
(CASE
WHEN [t2].[ProductTypeID] = #p0 THEN [t0].[Amount]
ELSE #p1
END)) AS [ClothingAmount], SUM(
(CASE
WHEN [t2].[ProductTypeID] = #p2 THEN [t0].[Amount]
ELSE #p3
END)) AS [FoodAmount], SUM(
(CASE
WHEN [t2].[ProductTypeID] = #p4 THEN [t0].[Quantity]
ELSE #p5
END)) AS [ClothingQuantity], SUM(
(CASE
WHEN [t2].[ProductTypeID] = #p6 THEN [t0].[Quantity]
ELSE #p7
END)) AS [FoodQuantity], [t3].[StoreName]
FROM [Purchase] AS [t0]
INNER JOIN [Product] AS [t1] ON [t0].[ProductID] = [t1].[ProductID]
INNER JOIN [ProductType] AS [t2] ON [t1].[ProductTypeID] = [t2].[ProductTypeID]
INNER JOIN [Store] AS [t3] ON [t0].[StoreID] = [t3].[StoreID]
GROUP BY [t3].[StoreName]
Note however that it is highly recommended to set proper primary and foreign keys on the tables. Once that's done (and the Model is updated accordingly) the same query can be reduced to:
from purchase in Purchases
group purchase by purchase.Store.StoreName into grouped
select new {
StoreName = grouped.Key,
ClothingAmount = grouped.Sum(purchase => purchase.Product.ProductTypeID == 1 ? purchase.Amount : 0),
FoodAmount = grouped.Sum(purchase => purchase.Product.ProductTypeID == 2 ? purchase.Amount : 0),
ClothingQuantity = grouped.Sum(purchase => purchase.Product.ProductTypeID == 1 ? purchase.Quantity : 0),
FoodQuantity = grouped.Sum(purchase => purchase.Product.ProductTypeID == 2 ? purchase.Quantity : 0)
}

Help with a complex self referency query accross multiple colums

I am having difficulties with a complicated (for me any way) query.
The table I'm querying has 3 colums, ClientID (int Not Null), ProductID (int Not Null) and ExpiryDate (smalldatetime nullable)
Given two client ID's Master and Consolidated I need to perform the following business logic to return a single data set:
Select the ClientID with the greater
expiry date for a product where expiry
dates for both clientIDs are not null
Select the ClientID with a null expiry
date for a product where one expiry is
null and the other not null
Select the MasterID for a product
where both expiry dates are null or
both expiry dates are the same.
I have tried the following, but get stuck...
Create Table #ProductSub (ClientID int NOT NULL,
ProductID int NOT NULL,
ExpiryDate smalldatetime)
/* In real life there is a Clustered Primary Key On ClientID and ProductID
Load Up Some Test Data */
Insert into #ProductSub Values (1, 100, null)
Insert into #ProductSub Values (2, 100, null)
Insert into #ProductSub Values (1, 101, null)
Insert into #ProductSub Values (2, 102, null)
Insert into #ProductSub Values (1, 200, null)
Insert into #ProductSub Values (2, 200, '2009-01-01')
Insert into #ProductSub Values (1, 300, '2009-01-01')
Insert into #ProductSub Values (2, 300, null)
Insert into #ProductSub Values (1, 400, '2009-01-01')
Insert into #ProductSub Values (2, 400, '2008-01-01')
Insert into #ProductSub Values (1, 500, '2008-01-01')
Insert into #ProductSub Values (2, 500, '2009-01-01')
Insert into #ProductSub Values (1, 600, '2009-01-01')
Insert into #ProductSub Values (2, 600, '2009-01-01')
--Select * from #ProductSub
Declare #MasterClient int,
#ConsolClient int
Select #MasterClient = 1, #ConsolClient = 2
Select * from #ProductSub t1
/* Use Master Client ID When Expiry Date is Null) */
Where (ClientID = #MasterClient and ExpiryDate is null)
/* Use Consol ClientID if Expiry Date is null nut Expiry Date for Master Client ID is not */
OR (ClientID = #ConsolClient and ExpiryDate is null and ProductID not in (
Select ProductID from #ProductSub t2
Where (ClientID = #MasterClient and ExpiryDate is null))
)
OR -- OH NO my head exploded
/* OR EXISTS (Select 1
from #ProductSub t3
)*/
Drop Table #ProductSub
/********** Expected Output ************************
ClientID ProductID ExpiryDate
1 100 NULL
1 101 NULL
2 102 NULL
1 200 NULL
2 300 NULL
1 400 2009-01-01 00:00:00
2 500 2009-01-01 00:00:00
1 600 2009-01-01 00:00:00
Any and all help greatly appreciated
EDIT: Although it sounds like it, this is not homework but a real life problem I am hoping to find a real life solution to, I could do this myself, but all my solutions are leading down the path to temp tables. I should point out the production environment is SQLServer 7!
Here I've moved the conditions to a subquery. The subquery joins the rows for Consol and Master, so you can access columns from both rows. The condition is still a little complex because either row can be missing.
select ps.*
from #ProductSub ps
inner join (
select
CASE
WHEN c.ClientID is null THEN m.ClientID
WHEN m.ClientID is null THEN c.ClientID
WHEN m.ExpiryDate is not null and c.ExpiryDate is not null THEN
CASE
WHEN c.ExpiryDate > m.ExpiryDate THEN c.ClientID
ELSE m.ClientID
END
WHEN m.ExpiryDate is null THEN m.ClientID
WHEN c.ExpiryDate is null THEN c.ClientID
ELSE m.ClientID
END as ClientId,
COALESCE(m.ProductId, c.ProductId) as ProductId
from #ProductSub m
full outer join #ProductSub c
on m.ProductID = c.ProductID
and m.ClientID <> c.ClientID
where IsNull(m.clientid,#MasterClient) = #MasterClient
and IsNull(c.clientid,#ConsolClient) = #ConsolClient
) filter
on filter.clientid = ps.clientid
and filter.productid = ps.productid
order by ps.ProductId
The question isn't clear, but as I interpret it, perhaps something like:
DECLARE #MasterExpiry smalldatetime, #ConsolExpiry smalldatetime
SELECT #MasterExpiry = ExpiryDate FROM #ProductSub WHERE ClientID = #MasterClient
SELECT #ConsolExpiry = ExpiryDate FROM #ProductSub WHERE ClientID = #ConsolClient
SELECT CASE
WHEN #MasterExpiry IS NULL AND #ConsolExpiry IS NULL THEN #MasterClient
WHEN #MasterExpiry IS NULL THEN #MasterClient
WHEN #ConsolExpiry IS NULL THEN #ConsolClient
WHEN #MasterExpiry >= #ConsolExpiry THEN #MasterClient
ELSE #ConsolClient END AS [Client]
If you need the row data, then select that into a variable and do a separate SELECT?
DECLARE #FinalClient int
SELECT #FinalClient = CASE
WHEN #MasterExpiry IS NULL AND #ConsolExpiry IS NULL THEN #MasterClient
WHEN #MasterExpiry IS NULL THEN #MasterClient
WHEN #ConsolExpiry IS NULL THEN #ConsolClient
WHEN #MasterExpiry >= #ConsolExpiry THEN #MasterClient
ELSE #ConsolClient END
SELECT * FROM #ProductSub WHERE ClientID = #FinalClient