SQL: Add a column and classify into categories - sql

I have a table which has transaction_id as the primary key and also contains customer_id which is a foreign key.
Now there is a column type which has two values: 'Card' and 'cash'.
Now some of the customers have used both the methods for payment. I want to add a new column and classify the customers as "Only card" "Only cash" and "Both".
Transaction id Customer id Type
1 100 Card
2 101 Cash
3 102 Card
4 103 Cash
5 101 Card
So in this table I want a new column 'Type of payment' which classifies customer 101 as Both since he has used both the methods of payment.

You can use window functions:
select t.*,
(case when min(type) over (partition by customerid) = max(type) over (partition by customerid)
then 'Only ' + min(type) over (partition by customerid)
else 'both'
end)
from transactions t;

You can do better and remove a bit of redondancy (the values cash only and card only will be repeated in the table, in this case we prefer repeating an ID). So you can create a Table for example payement_methods that will have 2 columns for example id and method, you will populate it with the three options you just mentioned (cash only, card only, both), and you'll have in your transaction table a column payment_method_id for example (instead of the type column you were using).
example
|id | method |
|1 | Cash only |
|2 | Card Only |
|3 | Both |
transaction table
|id | other columns ...|payement method |
|1 | other columns ...|1 |
|2 | other columns ...|3 |
//...
sorry for my english, good luck.

Rather than adding a column to the table, if what you want to do is analyze the payment methods, then doing something like this might be better:
SELECT DISTINCT Table1.[Customer ID], T1.*
FROM Table1
CROSS APPLY (SELECT SUM(CASE WHEN [Type] = 'Cash' THEN 1 ELSE 0 END) AS Cash,
SUM(CASE WHEN [Type] = 'Card' THEN 1 ELSE 0 END) AS Card
FROM Table1 T WHERE T.[Customer ID] = Table1.[Customer ID]) T1
Gives you results like this:
CUSTOMER ID CASH CARD
100 0 1
101 1 1
102 0 1
103 1 0

Create table tran1(Transactionid int , Customerid int , Type varchar(100))
insert into tran1(Transactionid , Customerid , Type ) values
(1 , 100 , 'Card ' ),
(2 , 101 , 'Cash' ),
(3 , 102 , 'Card ' ),
(4 , 103 , 'Cash ' ),
(5 , 101 , 'Card ' )
alter table tran1 add NewType varchar(100)
Update tran1 set NewType ='Only card' where Customerid IN (
select d.custid from (
select Customerid as custid,SUM(case when [Type]='Card' then 1 else 0 end) card
,SUM(case when [Type]='Cash' then 1 else 0 end) cash
from tran1
group by Customerid)d
where d.card=1
)
Update tran1 set NewType ='Only Cash' where Customerid IN (
select d.custid from (
select Customerid as custid,SUM(case when [Type]='Card' then 1 else 0 end) card
,SUM(case when [Type]='Cash' then 1 else 0 end) cash
from tran1
group by Customerid)d
where d.cash=1
)
Update tran1 set NewType ='Both' where Customerid IN (
select d.custid from (
select Customerid as custid,SUM(case when [Type]='Card' then 1 else 0 end) card
,SUM(case when [Type]='Cash' then 1 else 0 end) cash
from tran1
group by Customerid)d
where d.card=1 and cash=1
)

Related

Creating a new column for -ve values from the existing rows

How to write logic for this in SQL Server:
Voucher # Name Amount
-------------------------
123 ABC 910
123 ABC -910
224 XYZ 600
Expected output
Voucher # Name Amount - (Amount)
-------------------------------------------
123 ABC 910 -910
224 XYZ 600 -
Using conditional aggregation is really simple here. No need to query the same table over and over.
select Voucher
, Name
, Amount = sum(case when Amount > 0 then Amount else 0 end)
, [-Amount] = sum(case when Amount < 0 then Amount else 0 end)
from YourTable
group by Voucher
, Name
Here is the code, try it
Group by negative and positive amount and make a left join
DECLARE #tbl TABLE
(
Voucher varchar(10),
Name varchar(100),
Amount int
)
INSERT INTO #tbl
(
Voucher,
Name,
Amount
)
VALUES
(123,'ABC',910),
(123,'ABC',-910),
(224,'XYZ',600)
SELECT t1.Voucher, t1.Name, t1.Amount , ISNULL(t2.Amount,0) AS [(-Amount)] FROM (
SELECT t.Voucher, Name, Sum(t.Amount) Amount
FROM #tbl t
WHERE t.Amount > 0
GROUP BY t.Voucher, t.Name) t1 Left JOIN
(SELECT t.Voucher, Name, Sum(t.Amount) Amount
FROM #tbl t
WHERE t.Amount < 0
GROUP BY t.Voucher, t.Name) t2 ON t1.Voucher = t2.Voucher
If for each (Voucher, Name) there exists only 1 positive Amount and 0 or 1 negative Amount then with a self join:
select t.*, tt.amount NegativeAmount
from tablename t left join tablename tt
on tt.voucher = t.voucher and tt.name = t.name and tt.amount < 0
where t.amount > 0
See the demo.
Results:
> voucher | name | amount | NegativeAmount
> ------: | :--- | -----: | -------------:
> 123 | ABC | 910 | -910
> 224 | XYZ | 600 |

sql where have all multiple conditions in entry table

I've been trying to wrap my brain around this using joins, subquery joins, not exists clauses and I keep failing to come up with a query that produces the correct results.
I have 2 table's Trans and TransEntry
Trans Table (PRIMARY TransID)
TransID | Date
-----------
1 1/1/18
2 1/2/18
3 1/3/18
TransEntry Table (PRIMARY TransEntryID)
TransEntryID | TransID |Item
-----------
1 1 A
2 1 B
3 1 C
4 2 A
5 2 D
6 2 F
7 3 A
8 3 B
9 3 G
10 3 C
I need to have all TransID from TransEntry where the have item A and item C in the Entry, in our sample it will be only TransID (1,3)
or
SELECT TransID FROM TransEntry WHERE Item = 'A'
INTERSECT
SELECT TransID FROM TransEntry WHERE Item = 'C'
I think something like this should work:
SELECT TransID
FROM TransEntry
WHERE Item in ('A','C')
GROUP BY TransID
HAVING Count(DISTINCT Item) = 2;
Here's a sqlfiddle showing this
CREATE TABLE TransEntry(
TransEntryID INTEGER NOT NULL PRIMARY KEY
,TransID INTEGER NOT NULL
,Item VARCHAR(1) NOT NULL
);
INSERT INTO TransEntry(TransEntryID,TransID,Item) VALUES
(1,1,'A')
,(2,1,'B')
,(3,1,'C')
,(4,2,'A')
,(5,2,'D')
,(6,2,'F')
,(7,3,'A')
,(8,3,'B')
,(9,3,'G')
,(10,3,'C');
SELECT TransID
FROM TransEntry
WHERE Item in ('A','C')
GROUP BY TransID
HAVING Count(DISTINCT Item) = 2
+---------+
| TransID |
+---------+
| 1 |
| 3 |
+---------+
My answer wasn't as succinct as Aaron's, but in case it's helpful:
SELECT
TransID
FROM
Trans
WHERE
TransID IN ( SELECT TransID FROM TransEntry WHERE Item = 'A' ) AND
TransID IN ( SELECT TransID FROM TransEntry WHERE Item = 'C' )
you can go for something like this:
select transId
from
(
select
TransID ,
case when item = 'A' then 1 else 0 end as HasA,
case when item = 'C' then 1 else 0 end as HasC
from TransEntry
) a
group by transId
having sum(hasA) > 0 and sum(hasC) > 0
You can use GROUP BY :
SELECT TransID
FROM TransEntry
GROUP BY TransID
HAVING SUM(CASE WHEN item = 'A' THEN 1 ELSE 0 END) > 0 AND
SUM(CASE WHEN item = 'C' THEN 1 ELSE 0 END) > 0;
SELECT TransEntry.TransID, TransEntry.Item, GROUP_CONCAT(TransEntry.Item) AS items
FROM TransEntry
group by TransEntry.TransID Having Find_In_Set('A',items)>0 and Find_In_Set('C',items)>0
see the jsfiddle

Mutually exclusive counts in SQL

The table that I need to query looks like this
ID - Account - Product
1 - 002 - Bike
2 - 003 - Bike
4 - 003 - Motor
5 - 004 - Car
I need to be able to retrieve the number of accounts that purchased the each of the products and combinations of the products, like this
Bike | Car | Motor | Bike&Car | Bike&Motor | Car&Motor | Bike&Car&Motor
Note that an account that purchased a combination of products will be counted as 1.
Please help me in retrieving this data.
You can do this using two levels of aggregation. One method puts the values on different rows:
select has_bike, has_motor, has_car, count(*)
from (select account,
max(case when product = 'bike' then 1 else 0 end) as has_bike,
max(case when product = 'motor' then 1 else 0 end) as has_motor,
max(case when product = 'car' then 1 else 0 end) as has_car
from t
group by account
) t
group by has_bike, has_motor, has_car;
Or in columns:
select sum(has_bike * (1 - has_motor) * (1 - has_car)) as has_only_bike,
sum((1 - has_bike) * has_motor * (1 - has_car)) as has_only_motor,
sum((1 - has_bike) * (1 - has_motor) * has_car) as has_only_car,
. . .
from (select account,
max(case when product = 'bike' then 1 else 0 end) as has_bike,
max(case when product = 'motor' then 1 else 0 end) as has_motor,
max(case when product = 'car' then 1 else 0 end) as has_car
from t
group by account
) t;
If you only have a limited set of Products, then you can use this:
-- Create sample data
CREATE TABLE #tbl(
ID INT,
Account VARCHAR(10),
Product VARCHAR(10)
);
INSERT INTO #tbl VALUES
(1, '002', 'Bike'),
(2, '003', 'Bike'),
(3, '003', 'Motor'),
(4, '004', 'Car');
WITH Cte AS(
SELECT t1.Account, a.Products
FROM #tbl t1
CROSS APPLY (
SELECT STUFF((
SELECT '&' + t2.Product
FROM #tbl t2
WHERE t2.Account = t1.Account
ORDER BY t2.Product
FOR XML PATH(''), type).value('.[1]','nvarchar(max)'),
1, 1, '') AS Products
) a
GROUP BY t1.Account, a.Products
)
SELECT
Bike = SUM(CASE WHEN Products = 'Bike' THEN 1 ELSE 0 END),
Car = SUM(CASE WHEN Products = 'Car' THEN 1 ELSE 0 END),
Motor = SUM(CASE WHEN Products = 'Motor' THEN 1 ELSE 0 END),
[Bike&Car] = SUM(CASE WHEN Products = 'Bike&Car' THEN 1 ELSE 0 END),
[Bike&Motor] = SUM(CASE WHEN Products = 'Bike&Motor' THEN 1 ELSE 0 END),
[Car&Motor] = SUM(CASE WHEN Products = 'Car&Motor' THEN 1 ELSE 0 END),
[Bike&Car&Motor] = SUM(CASE WHEN Products = 'Bike&Car&Motor' THEN 1 ELSE 0 END)
FROM Cte;
DROP TABLE #tbl; -- Remove sample data
The idea is to generate 1 row for each Account, together with a comma-delimited Products. If you execute the query inside the CTE, you will get:
Account Products
---------- ---------------
002 Bike
003 Bike&Motor
004 Car
With that, you can do a conditional aggregation. The above uses a static solution, if you do not know the number of Products, you may need to come up with a dynamic approach.
ONLINE DEMO

Is there a better way to flatten out a table to take up fewer rows by moving fields in rows with duplicate keys into empty (NULL) fields?

I have a table with the recorded date, time and quantity of each item a child was given. My end goal is to pivot on that data, but preserve each individual quantity being given out according to date/time and child.
This is easy to achieve without a pivot, but it still takes up an entire row for each instance. What I want, is to flatten out the results to take up fewer rows. There isn't a huge functional difference, I'm just doing this to take up less real estate in the report that will end up using this data.
Updated to include a query for sample data:
DECLARE #Items TABLE (Name VARCHAR(10), Date DATETIME, ItemID INT, Quantity INT)
INSERT INTO #Items VALUES ('Jimmy', '01/23/2017 10:00:00', 1, 2),
('Jimmy', '01/23/2017 12:00:00', 1, 1),
('Jimmy', '01/23/2017 15:00:00', 2, 2),
('Billy', '01/23/2017 09:00:00', 1, 1),
('Billy', '01/23/2017 10:00:00', 2, 3)
This is what my starting table looks like:
Name Date ItemID Quantity
Jimmy 2017-01-23 10:00:00.000 1 2
Jimmy 2017-01-23 12:00:00.000 1 1
Jimmy 2017-01-23 15:00:00.000 2 2
Billy 2017-01-23 09:00:00.000 1 1
Billy 2017-01-23 10:00:00.000 2 3
I use a join to sum up the quantities for each day, sort the quantities into their own respective columns, and then drop the time:
SELECT d.Name, CAST(d.Date AS DATE) AS Date,
SUM(CASE WHEN s.ItemID = 1 THEN s.Quantity ELSE NULL END) AS SumBooks,
SUM(CASE WHEN s.ItemID = 2 THEN s.Quantity ELSE NULL END) AS SumPencils,
MAX(CASE WHEN d.ItemID = 1 THEN d.Quantity ELSE NULL END) AS Books,
MAX(CASE WHEN d.ItemID = 2 THEN d.Quantity ELSE NULL END) AS Pencils
FROM #Items d
INNER JOIN #Items s ON s.Name = d.Name AND CAST(s.Date AS DATE) = CAST(d.Date AS DATE)
GROUP BY d.Name, d.Date
This is the resulting data:
Name Date SumBooks SumPencils Books Pencils
Billy 2017-01-23 1 3 1 NULL
Billy 2017-01-23 1 3 NULL 3
Jimmy 2017-01-23 3 2 2 NULL
Jimmy 2017-01-23 3 2 1 NULL
Jimmy 2017-01-23 3 2 NULL 2
This is the structure I am trying to achieve:
Name Date SumBooks SumPencils Books Pencils
Billy 2017-01-23 1 3 1 3
Jimmy 2017-01-23 3 2 2 2
Jimmy 2017-01-23 3 2 1 NULL
I was able to do this using a cursor to iterate over each row and check a new table for any matches of Date, Name, and Books = NULL. If a match was found, I update that row with the quantity. Else, I insert a new row with the Books quantity and a NULL value in the Pencils field, later to be updated with a Pencils quantity, and so on.
So, I am able to get the results I need, but this check has to be done for every item column. For just a couple items, it isn't a big deal. When there's a dozen or more items and the result has 30+ columns, it ends up being a lot of declared variables and large, repeating IF/ELSE statements.
I'm not sure if this is commonly done, but if it is, I'm lacking the proper verbiage to find out on my own. Thanks in advance for any Suggestions.
If we trade the inner join for an outer apply() or a left join
and include those values to the group by we can get the results you are looking for based on the test data provided.
;with cte as (
select
i.Name
, [Date] = convert(date,i.[Date])
, SumBooks = sum(case when ItemId = 1 then Quantity else null end)
, SumPencils = sum(case when ItemId = 2 then Quantity else null end)
, Books = b.Books
, Pencils = max(case when ItemId = 2 then Quantity else null end)
, rn = row_number() over (
partition by i.Name, convert(varchar(10),i.[Date],120)
order by b.booksdate
)
from #Items i
outer apply (
select Books = Quantity, BooksDate = b.[Date]
from #Items b
where b.ItemId = 1
and b.Name = i.Name
and convert(date,b.[Date])=convert(date,i.[Date])
) as b
group by
i.Name
, convert(date,i.[Date])
, b.Books
, b.BooksDate
)
select
Name
, Date
, SumBooks
, SumPencils
, Books
, Pencils = Pencils + case when rn > 1 then null else 0 end
from cte
alternate left join for b:
left join (
select Books = Quantity, BooksDate = b.[Date], Name, Date
from Items b
where b.ItemId = 1
) as b on b.Name = i.Name and convert(date,b.[Date])=convert(date,i.[Date])
test setup: http://rextester.com/IXHU81911
create table Items (
Name varchar(64)
, Date datetime
, ItemID int
, Quantity int
);
insert into Items values
('Jimmy','2017-01-23 10:00:00.000',1,2)
, ('Jimmy','2017-01-23 12:00:00.000',1,1)
, ('Jimmy','2017-01-23 13:00:00.000',1,1) /* Another 1 Book */
, ('Jimmy','2017-01-23 15:00:00.000',2,2)
, ('Billy','2017-01-23 09:00:00.000',1,1)
, ('Billy','2017-01-23 10:00:00.000',2,3)
, ('Zim' ,'2017-01-23 10:00:00.000',2,1) /* No books */
query:
;with cte as (
select
i.Name
, [Date] = convert(varchar(10),i.[Date],120)
, SumBooks = sum(case when ItemId = 1 then Quantity else null end)
, SumPencils = sum(case when ItemId = 2 then Quantity else null end)
, Books = b.Books
, Pencils = max(case when ItemId = 2 then Quantity else null end)
, rn = row_number() over (
partition by i.Name, convert(varchar(10),i.[Date],120)
order by b.booksdate
)
from Items i
outer apply (
select Books = Quantity, BooksDate = b.[Date]
from Items b
where b.ItemId = 1
and b.Name = i.Name
and convert(date,b.[Date])=convert(date,i.[Date])
) as b
group by
i.Name
, convert(varchar(10),i.[Date],120)
, b.Books
, b.BooksDate
)
select
Name
, Date
, SumBooks
, SumPencils
, Books
, Pencils = Pencils + case when rn > 1 then null else 0 end
from cte
note: convert(varchar(10),i.[Date],120) is used on rextester to override default formatting of date. Use convert(date,i.[Date]) or cast(i.[Date] as date) outside of rextester.
results:
+-------+------------+----------+------------+-------+---------+
| Name | Date | SumBooks | SumPencils | Books | Pencils |
+-------+------------+----------+------------+-------+---------+
| Billy | 2017-01-23 | 1 | 3 | 1 | 3 |
| Jimmy | 2017-01-23 | 4 | 2 | 1 | 2 |
| Jimmy | 2017-01-23 | 4 | 2 | 1 | NULL |
| Jimmy | 2017-01-23 | 4 | 2 | 2 | NULL |
| Zim | 2017-01-23 | NULL | 1 | NULL | 1 |
+-------+------------+----------+------------+-------+---------+

Combine multiple rows into 1 row

Say for example I have a table that contains a description of a customer's activities while in a cafe. (Metaphor of the actual table I am working on)
Customer Borrowed Book Ordered Drink Has Company
1 1
1 1
1 Yes
2 1
3 1
3 Yes
4 1 1
4 1
I wish to combine the rows in this way
Customer Borrowed Book Ordered Drink Has Company
1 1 1 Yes
2 1
3 1 Yes
4 1 2
I did self join with coalesce, but it did not give my desired results.
You can do this by group by,
select Customer,sum([borrowed book]), sum([ordered drink]), max([has company])
from customeractivity group by Customer
As per your comment, initial table is a temp table,
Try to make the result as a cte result, then do aggregation on that, like the below query.
; WITH cte_1
AS
( //your query to return the result set)
SELECT customer,sum([borrowed book]) BorrowedBook,
sum([ordered drink]) OrderedDrink,
max([has company]) HasCompany
FROM cte_1
GROUP BY Customer
Use Group By:
DECLARE #tblTest as Table(
Customer INT,
BorrowedBook INT,
OrderedDrink INT,
HasCompany BIt
)
INSERT INTO #tblTest VALUES
(1,1,NULL,NULL)
,(1,NULL,1,NULL)
,(1,NULL,NULL,1)
,(2,NULL,1,NULL)
,(3,NULL,1,NULL)
,(3,NULL,NULL,1)
,(4,1,1,NULL)
,(4,NULL,1,NULL)
SELECT
Customer,
SUM(ISNULL(BorrowedBook,0)) AS BorrowedBook,
SUM(ISNULL(OrderedDrink,0)) AS OrderedDrink,
CASE MIN(CAST(HasCompany AS INT)) WHEN 1 THEN 'YES' ELSE '' END AS HasCompany
FROM #tblTest
GROUP BY Customer
Not sure, why you are getting error with group by.
Your coalesce should be correct. Refer below way.
Select customer
, case when [borrowed] = 0 then NULL else [borrowed] end as [borrowed]
, case when [ordered] = 0 then NULL else [ordered] end as [ordered]
, case when [company] = 1 then 'Yes' end as company
from
(
Select customer,
coalesce(
case when (case when borrowed = '' then null else borrowed end) = 1 then 'borrowed' end,
case when (case when ordered = '' then null else ordered end) = 1 then 'ordered' end,
case when (case when company = '' then null else company end) = 'Yes' then 'company' end
) val
from Table
) main
PIVOT
(
COUNT (val)
FOR val IN ( [borrowed], [ordered], [company] )
) piv
OUTPUT:
customer | borrowed | ordered | company
---------------------------------------
1 1 1 Yes
2 NULL 1 NULL
3 NULL 1 Yes