How to SET Default value in SQL Server case statement? - sql-server

I have a scenario like to display Status and Count. Look at the below model
Status Counts
--------------- --------
Completed 10
In Progress 6
Others 0
I have this code:
SELECT
CASE
WHEN Status = 'I' THEN 'IN PROGRESS'
WHEN Status = 'O' THEN 'Others'
WHEN Status = 'C' THEN 'COMPLETED'
END AS ' Status',
COUNT(Status) AS ' Counts'
FROM
table1
I have values for only Completed and InProgress. Currently I am getting only two rows. But I need three rows including Others and it's count should be 0 - how to do it in SQL?

Add an "else", like this:
SELECT CASE
WHEN Status= 'I' THEN 'IN PROGRESS'
WHEN Status= 'C' THEN 'COMPLETED'
ELSE 'Others'
END AS ' Status'
FROM table1

From my understanding you have a main table with status values in it, which I'll simplify to this:
CREATE TABLE #MainTable
(
id INT,
[status] NVARCHAR(1)
)
Your problem is that if there are no rows with the status O for Others, you're not getting a 0 when you group the rows to get counts.
I suggest you create a Status table to link to with a RIGHT JOIN. This will also get rid of the need for your CASE statements.
Full solution:
CREATE TABLE #MainTable
(
id INT ,
[status] NVARCHAR(1)
);
INSERT INTO #MainTable
( id, [status] )
VALUES ( 1, 'I' ),
( 2, 'I' ),
( 3, 'I' ),
( 4, 'I' ),
( 5, 'C' ),
( 6, 'C' );
CREATE TABLE #status
(
[status] NVARCHAR(1) ,
[statusText] NVARCHAR(15)
);
INSERT INTO #status
( status, statusText )
VALUES ( 'I', 'In Progress' ),
( 'C', 'Completed' ),
( 'O', 'Others' );
SELECT s.statusText ,
COUNT(t.[status]) StatusCount
FROM #MainTable t
RIGHT JOIN #status s ON s.status = t.status
GROUP BY s.statusText;
DROP TABLE #MainTable;
DROP TABLE #status;
Produces:
statusText StatusCount
Completed 2
In Progress 4
Others 0
In the above example it joins on the I,O,C string values, which I would suggest that you replace with ID values. Then you could do this:
CREATE TABLE #MainTable
(
id INT ,
statusId INT
);
CREATE TABLE #status
(
statusId INT ,
statusShort NVARCHAR(1) ,
statusText NVARCHAR(15)
);
SELECT t.id ,
t.statusId ,
s.statusId ,
s.statusShort ,
s.statusText
FROM #MainTable t
RIGHT JOIN #status s ON s.statusId = t.statusId
DROP TABLE #MainTable;
DROP TABLE #status;

Related

how to change effectively the In clause in SQL

we have a employee_master table and a Report_Filter Table, the Report_Filter contains different typs of filter the user enters for eg. status, employee code, nationaality etc. and this report_filter contains the user Information also. The table structure is like this Report_Filter( User_Id, Report_TYpe, Report_Code ) all are character fields. based on the users input the table will have values like
( 'User_1', 'STATUS', '01' )
( 'user_1', 'STATUS', '02' )
( 'User_1', 'EMP_CODE', 'ABC' )
( 'User_1', 'NATIONALITY', 'ALL' ) -- All means he want to see all the nationalities
now currently we are writing the query like this
Select Emp_code, Emp_Name, status, nationlity
From Empolyee_Master m
Where
('All' in ( select report_code from Report_Filter where user_id = #user_id and report_type='STATUS') or m.STATUS in ( select report_code from Report_Filter where user_id = #user_id and report_type='STATUS') ) and
or m.CATEGORY in ( select report_code from Report_Filter where user_id = #user_id and report_type='NATIONALITY') ) and
or m.emp_code in ( select report_code from Report_Filter where user_id = #user_id and report_type='EMP_CODE') )
there are other conditions also we are using, the problem is its take too much time because of the In clauses. how can we effectively change this code for improving performance
we are using MSSQL 2014
Instead of retrieving all values and then seeing if your value is part of it, you should filter by your value. Try if this helps (just one example):
replace
WHERE 'ALL' in (
select report_code from Report_Filter
where user_id = #user_id and report_type='STATUS'
)
with
WHERE EXISTS (
select 1 from Report_Filter
where user_id = #user_id and report_type='STATUS'
and report_code = 'ALL'
)
Now it would also help to include column report_code in an INDEX (you are using INDEXes, I hope?)
Update - to skip filtering in case of 'ALL', that can be done like this:
DECLARE #statusFilter VARCHAR(20) = ... -- 'ALL' or other value
SELECT ...
WHERE #statusFilter = 'ALL'
OR EXISTS (
select 1 from Report_Filter
where user_id = #user_id and report_type='STATUS'
and report_code = #statusFilter
)
You can try to do the filtering in advance:
(For your next question: Such a mockup should be done by you)
DECLARE #ReportFilter TABLE([user_id] VARCHAR(100),report_type VARCHAR(100),report_code VARCHAR(100));
INSERT INTO #ReportFilter VALUES
( 'User_1', 'STATUS', '01' )
,( 'user_1', 'STATUS', '02' )
,( 'User_1', 'EMP_CODE', 'ABC' )
,( 'User_1', 'NATIONALITY', 'ALL' );
DECLARE #Employee_Master TABLE(Emp_Code VARCHAR(100),Emp_Name VARCHAR(100), [status] VARCHAR(100), nationality VARCHAR(100));
INSERT INTO #Employee_Master VALUES
('ABC','test1','01','USA')
,('DEF','test2','99','GB')
,('ABC','test3','02','DE')
,('XYZ','test4','01','FRA');
--This is part of your query
DECLARE #user_id VARCHAR(100)='User_1';
WITH filterSTATUS AS
(
SELECT * FROM #ReportFilter WHERE [user_id]=#user_id AND report_type='STATUS'
AND NOT EXISTS(SELECT 1 FROM #ReportFilter WHERE [user_id]=#user_id AND report_type='STATUS' AND report_code='ALL')
)
,
filterEMPCODE AS
(
SELECT * FROM #ReportFilter WHERE [user_id]=#user_id AND report_type='EMP_CODE'
AND NOT EXISTS(SELECT 1 FROM #ReportFilter WHERE [user_id]=#user_id AND report_type='EMP_CODE' AND report_code='ALL')
)
,filterNATIONALITY AS
(
SELECT * FROM #ReportFilter WHERE [user_id]=#user_id AND report_type='NATIONALITY'
AND NOT EXISTS(SELECT 1 FROM #ReportFilter WHERE [user_id]=#user_id AND report_type='NATIONALITY' AND report_code='ALL')
)
SELECT *
FROM #Employee_Master AS em
WHERE (SELECT COUNT(*) FROM filterSTATUS)=0 OR em.[status] IN (SELECT x.report_code FROM filterSTATUS AS x)
AND (SELECT COUNT(*) FROM filterEMPCODE)=0 OR em.Emp_Code IN (SELECT x.report_code FROM filterEMPCODE AS x)
AND (SELECT COUNT(*) FROM filterNATIONALITY)=0 OR em.nationality IN (SELECT x.report_code FROM filterNATIONALITY AS x);
For the give set this returns
Emp_Code Emp_Name status nationality
ABC test1 01 USA
ABC test3 02 DE

Leave Approval Status in SQL Server

I am using SQL Server 2008 R2, I want to display leave application status that can be either apply, approve, reject or cancel.
If all leave approve then status = approve, like other leaves
but if there are mix status e.g some leave approve , some rejected then status = Partial.
I have written the code but I feel it is complicated, can I get it in one, single query?
create table #t
(
employeeID int,
LeaveCode nvarchar(10),
status nvarchar(50)
)
insert into #t
values(1, 'PL', 'Approve'), (1, 'PL', 'Reject'), (1, 'PL', 'Approve')
;with ct1 as
(
select status, count(status) Cnt
from #t
group by status
),
counters as
(
select count(*) as TotalLeave
from #t
)
select top(1)
CASE
WHEN C1.Cnt = C2.TotalLeave
THEN C1.status
ELSE 'Partial'
END [status]
from
ct1 C1
cross join
counters C2
drop table #t
try this,
create table #t
(
employeeID int,
LeaveCode nvarchar(10),
status nvarchar(50)
)
insert into #t values(1,'PL','Approve'),(1,'PL','Reject'),(1,'PL','Approve')
insert into #t values(2,'PL','Approve'),(2,'PL','Approve'),(2,'PL','Approve')
SELECT
employeeID,
CASE WHEN count(DISTINCT status) = 1 THEN MAX(status) ELSE 'Partail' END [status]
FROM #t
GROUP BY employeeID
drop table #t
SELECT
CASE
WHEN COUNT(DISTINCT t.status) > 1 THEN 'Partial'
ELSE MAX(t.status)
END OveralLeaveStatus
FROM #t t
simply group by employee and check for max(status) <> min(status), if it is difference than it means at least one the status is different
select employeeID,
[status] = case when min([status]) <> max([status]) then 'Partial'
else min([status])
end
from #t
group by employeeID

How to convert YES/NO to BIT automatically in SQL Server?

I want to override system defined conversion in SQL Server. is that possible.
Problem Statement
I have a large csv file to upload data in database. There is a column with BIT type contains True/False, Yes/No, 1/0
When I used bulk insertion True/False and '1/0' will be consider as right value but Yes/No (as expected) will throw an conversion error.
Is there any way to convert Yes/No without any looping or one by one value?
Edit
Custom data type PanelMemberType
CREATE TYPE [dbo].[PanelMemberType] AS TABLE(
[Email] [nvarchar](255) NOT NULL,
[LocationName] [nvarchar](255) NOT NULL,
[OptInPermission] [nvarchar](255) NOT NULL
)
GO
Stored procedure code:
ALTER PROCEDURE [dbo].[PanelMemberBulkUpdate]
#tblPanelMember PanelMemberType READONLY,
#PanelID int,
#UserID nvarchar(128)
AS
BEGIN
SET NOCOUNT ON;
MERGE INTO PanelMember p1
USING #tblPanelMember p2 ON p1.Email= p2.Email
WHEN MATCHED THEN
UPDATE SET
p1.PanelID = #PanelID,
p1.LocationID = (SELECT TOP(1) [LocationID]
FROM [dbo].[Location]
WHERE [LocationName] = p2.LocationName),
p1.Email = p2.Email,
p1.OptInPermission = CONVERT(BIT, p2.OptInPermission),
p1.DateAdded = p1.DateAdded,
p1.DateLastUpdated = (SELECT GETDATE()),
p1.LastUpdateUserID = #UserID
WHEN NOT MATCHED THEN
INSERT (PanelID, LocationID, Email, OptInPermission, DateAdded)
VALUES (#PanelID, (SELECT TOP(1) [LocationID]
FROM [dbo].[Location]
WHERE [LocationName] = p2.LocationName),
p2.Email, CONVERT(BIT, p2.OptInPermission),
(SELECT GETDATE()));
END
Just apply a bit more logic rather than trying to blindly convert to bit:
p1.OptInPermission = convert( bit,
CASE p2.OptInPermission
WHEN 'Yes' THEN 1
WHEN 'No' THEN 0
ELSE p2.OptInPermission END)
(Or, to avoid having to duplicate this logic in both branches of the MERGE, do it in the source:
USING (select Email,LocationName,convert( bit,
CASE p2.OptInPermission
WHEN 'Yes' THEN 1
WHEN 'No' THEN 0
ELSE p2.OptInPermission END as OptInPermission from #tblPanelMember) p2
)
Inserts records from CSV file into staging table first.
Then run the actual insert into your table and your select statement should look like this:
INSERT INTO ActualTable(column1, column2, column3)
SELECT column1
, column2
, CAST(CASE column3
WHEN 'Yes' THEN 1
WHEN 'No' THEN 0
ELSE column3
END AS BIT) AS YourBitColumn
FROM StagingTable;
Based on your approach, this should be working:
ALTER PROCEDURE [dbo].[PanelMemberBulkUpdate]
(
#tblPanelMember PanelMemberType READONLY
, #PanelID INT
, #UserID NVARCHAR(128)
)
AS
BEGIN TRY
SET NOCOUNT ON;
MERGE INTO PanelMember p1
USING #tblPanelMember p2
ON p1.Email = p2.Email
WHEN MATCHED THEN
UPDATE
SET p1.PanelID = #PanelID
, p1.LocationID = (SELECT TOP (1) [LocationID] FROM [dbo].[Location] WHERE [LocationName] = p2.LocationName)
, p1.Email = p2.Email
, p1.OptInPermission = CONVERT(BIT, CASE p2.OptInPermission
WHEN 'Yes' THEN 1
WHEN 'No' THEN 0
ELSE p2.OptInPermission
END)
, p1.DateAdded = p1.DateAdded
, p1.DateLastUpdated = GETDATE()
, p1.LastUpdateUserID = #UserID
WHEN NOT MATCHED THEN
INSERT (PanelID, LocationID, Email, OptInPermission, DateAdded)
VALUES
(
#PanelID
, (SELECT TOP (1) [LocationID] FROM [dbo].[Location] WHERE [LocationName] = p2.LocationName)
, p2.Email
, CONVERT(BIT, CASE p2.OptInPermission
WHEN 'Yes' THEN 1
WHEN 'No' THEN 0
ELSE p2.OptInPermission
END)
, GETDATE()
);
END TRY
BEGIN CATCH
THROW;
END CATCH
You cannot override default function, but you can write your own one. for instance:
CREATE FUNCTION dbo.ConvertBit
(
#Input VARCHAR(5)
)
RETURNS TABLE AS RETURN
SELECT CONVERT(BIT, CASE #Input
WHEN 'True' THEN 1
WHEN 'Yes' THEN 1
WHEN '1' THEN 1
WHEN 'False' THEN 0
WHEN 'No' THEN 0
WHEN '0' THEN 0
END AS BitColumn;
And then just query it this way:
ALTER PROCEDURE [dbo].[PanelMemberBulkUpdate]
(
#tblPanelMember PanelMemberType READONLY
, #PanelID INT
, #UserID NVARCHAR(128)
)
AS
BEGIN TRY
SET NOCOUNT ON;
MERGE INTO PanelMember p1
USING (
SELECT T.Email
, T.LocationName
, B.OptInPermission
FROM #tblPanelMember AS T
CROSS APPLY dbo.ConvertBit(T.OptInPermission) AS B(OptInPermission)
) AS p2
ON p1.Email = p2.Email
WHEN MATCHED THEN
UPDATE
SET p1.PanelID = #PanelID
, p1.LocationID = (SELECT TOP (1) [LocationID] FROM [dbo].[Location] WHERE [LocationName] = p2.LocationName)
, p1.Email = p2.Email
, p1.OptInPermission = p2.OptInPermission
, p1.DateAdded = p1.DateAdded
, p1.DateLastUpdated = GETDATE()
, p1.LastUpdateUserID = #UserID
WHEN NOT MATCHED THEN
INSERT (PanelID, LocationID, Email, OptInPermission, DateAdded)
VALUES
(
#PanelID
, (SELECT TOP (1) [LocationID] FROM [dbo].[Location] WHERE [LocationName] = p2.LocationName)
, p2.Email
, p2.OptInPermission
, GETDATE()
);
END TRY
BEGIN CATCH
THROW;
END CATCH

Return tag string based on two conditions

I have a table with a Tags column, which contains items like so: 'server, network, location1'...
I need to find all records where the item 'storage' is present
AND
where any of these locations are present ('location1', location2, location3')
AND
where no other items are present (i.e. only 'storage' and a location OR only 'storage').
I'm using a function to split the string into items, so a select statement for the code below using Tags for the entire string and Item for the items will help me a lot.
SELECT cardid, item, tags, count(1) as Total
, result =
case when lower(item) = 'storage' and
lower(item) in 'location1, location2, location3'
then 'yes'
else 'no'
end
FROM myTable
CROSS APPLY dbo.fn_SplitString(Tags, ',')
GROUP BY cardid, item, tags
order by item desc
Here is the function I use:
CREATE FUNCTION [dbo].[fn_SplitString]
(
#String VARCHAR(8000),
#Delimiter VARCHAR(255)
)
RETURNS
#Results TABLE
(
ID INT IDENTITY(1, 1),
Item VARCHAR(8000)
)
AS
BEGIN
INSERT INTO #Results (Item)
SELECT SUBSTRING(#String+#Delimiter, Number,
CHARINDEX(#Delimiter, #String+#Delimiter, Number) - Number)
FROM Numbers
WHERE Number <= LEN(REPLACE(#String,' ','|'))
AND SUBSTRING(#Delimiter + #String,
Number,
LEN(REPLACE(#delimiter,' ','|'))) = #Delimiter
ORDER BY Number RETURN
END
"to find all records where ..." your query should be changed to this:
SELECT MT.cardid, MT.tags, count(1) as Total
FROM myTable AS MT
CROSS APPLY dbo.fn_SplitString(Tags, ',') AS TagList
GROUP BY MT.cardid, MT.tags
HAVING COUNT( * ) =
COUNT( CASE WHEN TagList.Item IN( 'location1', 'location2', 'location3' ) THEN 1 ELSE NULL END )
+ COUNT( CASE WHEN TagList.Item = 'storage' THEN 1 ELSE NULL END )
AND COUNT( CASE WHEN TagList.Item = 'storage' THEN 1 ELSE NULL END ) >= 1
ORDER BY MT.cardid DESC
The above query is using "Conditional Aggregation". I have added HAVING clause where for each group COUNT of all tags is compared to the sum of "location" tags COUNT and "storage" tag COUNT. The next condition checks that the COUNT of "storage" is greater or equal to 1 (if you need only 1 storage element than change it to = 1)
Note: I made an assumption that your dbo.fn_SplitString function returns a table with a column called "Item".
Note 2: notice that I have given an alias ("AS TagList") to the function.
If you want the query to return all rows and simply indicate if a row matches your tag condition (like the example query you have given) then move all HAVING conditions to your CASE statement and remove HAVING
SELECT MT.cardid, MT.tags, COUNT(*) as Total
, result =
CASE
WHEN COUNT( * ) =
COUNT( CASE WHEN TagList.Value IN( 'location1', 'location2', 'location3' ) THEN 1 ELSE NULL END )
+ COUNT( CASE WHEN TagList.Value = 'storage' THEN 1 ELSE NULL END )
AND COUNT( CASE WHEN TagList.Item = 'storage' THEN 1 ELSE NULL END ) >= 1
THEN 'yes'
ELSE 'no'
END
FROM myTable AS MT
CROSS APPLY dbo.fn_SplitString(Tags, ',') AS TagList
GROUP BY MT.cardid, MT.tags
ORDER BY MT.cardid DESC
Below is a working example:
DECLARE #Sample TABLE( GroupID INT, Tag VARCHAR( 20 ))
INSERT INTO #Sample( GroupID, Tag )
VALUES
( 1, 'location1' ),( 1, 'location2' ), ( 1, 'storage' ),
( 2, 'location1' ),( 2, 'location2' ), ( 2, 'something else' ),
( 3, 'storage' )
SELECT GroupID
FROM #Sample AS TagList
GROUP BY GroupID
HAVING COUNT( * ) =
COUNT( CASE WHEN TagList.Tag IN( 'location1', 'location2', 'location3' ) THEN 1 ELSE NULL END )
+ COUNT( CASE WHEN TagList.Tag = 'storage' THEN 1 ELSE NULL END )
AND COUNT( CASE WHEN TagList.Tag = 'storage' THEN 1 ELSE NULL END ) >= 1
In the future please provide a sample table like the one above.
Update (to answer your comments):
Notice that the function below is a table valued function which returns a table with one column: #Results TABLE( Value VARCHAR( MAX )). The name of the column is "Value" (in your case it may be a different name). So in your query to access data returned by this function you specify the name of this column (in my case it is "Value").
CREATE FUNCTION dbo.fn_SplitString
(
#Source VARCHAR( MAX ),
#Delimiter VARCHAR( 100 )
)
RETURNS #Results TABLE( Value VARCHAR( MAX ))
AS
BEGIN
-- The main body of code has been excluded for brevity
END
Links to similar questions that contain explanations:
T-SQL SUM All with a Conditional COUNT
Create a view with totals from multiple columns
SQL Server - conditional aggregation with correlation

Avoid referring table two times in the WHERE clause

Following is a simplified version of my database in SQL Server 2005. I need to select employees based on business units. Each employee has home department, parent department and visiting department. Based on the department, business unit can be found out.
For an employee, if the HomeDeptID = ParentDeptID, then
#SearchBusinessUnitCD should be present for the VisitingDeptID.
If HomeDeptID <> ParentDeptID, then #SearchBusinessUnitCD should be
present for the ParentDeptID.
Following query works fine. But it has scan on the #DepartmentBusinesses table two times. Is there a way to use the table #DepartmentBusinesses only once by making it as a CASE statement or similar?
DECLARE #SearchBusinessUnitCD CHAR(3)
SET #SearchBusinessUnitCD = 'B'
--IF HomeDeptID = ParentDeptID, then #SearchBusinessUnitCD should be present for the VisitingDeptID
--IF HomeDeptID <> ParentDeptID, then #SearchBusinessUnitCD should be present for the ParentDeptID
CREATE TABLE #DepartmentBusinesses (DeptID INT, BusinessUnitCD CHAR(3))
INSERT INTO #DepartmentBusinesses
SELECT 1, 'A' UNION ALL
SELECT 2, 'B'
CREATE NONCLUSTERED INDEX IX_DepartmentBusinesses_DeptIDBusinessUnitCD ON #DepartmentBusinesses (DeptID,BusinessUnitCD)
DECLARE #Employees TABLE (EmpID INT, HomeDeptID INT, ParentDeptID INT, VisitingDeptID INT)
INSERT INTO #Employees
SELECT 1, 1, 1, 2 UNION ALL
SELECT 2, 2, 1, 3
SELECT *
FROM #Employees
WHERE
(
HomeDeptID = ParentDeptID
AND
EXISTS (
SELECT 1
FROM #DepartmentBusinesses
WHERE DeptID = VisitingDeptID
AND BusinessUnitCD = #SearchBusinessUnitCD)
)
OR
(
HomeDeptID <> ParentDeptID
AND
EXISTS (
SELECT 1
FROM #DepartmentBusinesses
WHERE DeptID = ParentDeptID
AND BusinessUnitCD = #SearchBusinessUnitCD
)
)
DROP TABLE #DepartmentBusinesses
Plan
SELECT *
FROM #Employees e
WHERE EXISTS (
SELECT 1
FROM #DepartmentBusinesses t
WHERE t.BusinessUnitCD = #SearchBusinessUnitCD
AND (
(e.HomeDeptID = e.ParentDeptID AND t.DeptID = e.VisitingDeptID)
OR
(e.HomeDeptID != e.ParentDeptID AND t.DeptID = e.ParentDeptID)
)
)
You can give this a try:
SELECT e.*
FROM #Employees AS e
INNER JOIN #DepartmentBusinesses AS d
ON (d.DeptID = e.VisitingDeptID AND e.HomeDeptID = e.ParentDeptID) OR
(d.DeptID = e.ParentDeptID AND e.HomeDeptID <> e.ParentDeptID)
WHERE d.BusinessUnitCD = #SearchBusinessUnitCD

Resources