How to use IF...ELSE sql statement with multiple selects? - sql

Simple academic project. Simple procedure. If there is something already in Payments table - then take debt value from that table, if there are no values - take it from Tariff table. But why such conidtion doesn't work?
ALTER PROCEDURE dbo.GetDebt
(
#LoanId int
)
AS
IF NOT EXISTS (SELECT top 1 * FROM Payment WHERE LoanId = #LoanId)
BEGIN
SELECT (TotalLoan + ( ( TotalLoan / 100 ) * Interest)) as Debt FROM Loan L, Tariff T
WHERE L.TariffIf = L.TariffId
END
ELSE
BEGIN
SELECT MIN(Debt) as Debt FROM Loan L
RIGHT OUTER JOIN Payment P -- Joins are cool.
ON L.LoanId = P.LoanId
WHERE P.LoanId = #LoanId
END

If/Else is almost always the entirely wrong approach for sql code. It's hard to give you an exact example without knowing more about your tables, but you really want something more like this:
SELECT COALESCE(P.Debt, TotalLoan + ( ( TotalLoan / 100 ) * Interest)) as Debt
FROM Loan L
LEFT JOIN Tariff T ON T.LoanID = L.LoanID
LEFT JOIN (SELECT LoanID, Min(Debt) As Debt FROM Payment GROUP BY LoanID) P
WHERE L.LoanID = #LoanID
No If/Else required.

use BEGIN and END around your statements like so:
IF (SELECT count(*) FROM Payment WHERE LoanId = #LoanId) = 0
BEGIN
SELECT (TotalLoan + ( ( TotalLoan / 100 ) * Interest)) as Debt FROM Loan L
RIGHT OUTER JOIN Tariff -- TODO: Add correct ON clause here
WHERE L.LoanId = #LoanId
END
ELSE
BEGIN
SELECT MIN(Debt) as Debt FROM Loan L
RIGHT OUTER JOIN Payment P -- Joins are cool.
ON L.LoanId = P.LoanId
WHERE P.LoanId = #LoanId
END
Also note that you are missing an on clause for your right outer joins which will cause errors.
Also it might be more efficient to change
IF (SELECT count(*) FROM Payment WHERE LoanId = #LoanId) = 0
to
IF NOT EXISTS (SELECT * FROM Payment WHERE LoanId = #LoanId)
The keyword being "might"

Related

Optional Where Clause Based On Another Table

I have a stored proc where a table of integers is passed as a parameter. I'm trying to find a reasonable way of writing "give me all the records, but if the parameter table has values in it then limit my results to those values".
Both approaches in the queries below work, but when I use either approach in my real-world proc (with a substantial number of joins and apply clauses and a ton of data) it's quite a bit slower than I would like even when the number of rows in the variable table is limited to 1 or 2 records.
Is there a better way of doing this?
-- Apprroach1 - Weird WHERE clause
IF OBJECT_ID('tempdb.dbo.#list') IS NOT NULL DROP TABLE #list
create table #list(Id int)
insert into #list(id) values (726), (712), (725)
declare #listCount int
select #listCount = count(*) from #list
select * from SalesLT.Product p
where 1 = 1
AND
(
#listCount > 0 AND p.ProductID in (select Id from #list)
OR
#listCount = 0
)
and
-- approach 2 - goofy looking JOIN
IF OBJECT_ID('tempdb.dbo.#list') IS NOT NULL DROP TABLE #list
create table #list(Id int)
insert into #list(id) values (726), (712), (725)
declare #listCount int
select #listCount = count(*) from #list
select * from SalesLT.Product p
inner join #list l on
case when #listCount > 0 and l.Id = p.ProductID Then 1
else 0
end = 1
Generally, case when in joins (ON clause) are avoided as it will make less performant query.
Use the left join approach as follows:
select * from SalesLT.Product p
Left join #list l on l.Id = p.ProductID
Where ( (#listCount > 0 and l.id is not null)
or #listCount = 0)
Try this
IF #listCount > 0
BEGIN
SELECT
*
FROM SalesLT.Product p
------------------------
INNER JOIN #list l ON
------------------------
l.Id = p.ProductID
------------------------
END
-- I assume you want to output everything if #listCount = 0
ELSE IF #listCount = 0
BEGIN
SELECT
*
FROM SalesLT.Product p
END
If you have a bunch of joins using that table outputs, you can store the output and use it on your real join/query.
Example:
IF #listCount > 0
BEGIN
SELECT
*
INTO #TempSalesTbl
FROM SalesLT.Product p
------------------------
INNER JOIN #list l ON
------------------------
l.Id = p.ProductID
------------------------
END
-- In your query
SELECT
*
FROM Table A
INNER JOIN #TempSalesTbl ON
...

TSQL not pulling in complete data set

I have a complicated stored procedure that worked great until the client wanted to change it.
I am not great with complicated TSQL so I have no idea what is wrong with my code.
Here is the situation. I have three temp tables, Cost, Adjustments, and Payments.
In the end I merge all these tables together in a report table. The problem I am having is even if one or even two of these tables are null, as long as one table has data I need that data to show. I currently have it set up with full outer joins but I'm still not getting the full list, I'm missing probably....50 ish records that should be there.
Can anyone look at this code and tell me what the heck I'm doing wrong? I'm bringing all the data together on #ThisReportAll
UPDATE: So I removed the having clause to see what was going on, and the data for the overdue balance is returning null. So the math isn't...mathing correctly, any ideas?
CODE
CREATE TABLE #BalanceAdjustmentsAll (CustomerId int, Amount decimal(20,2));
CREATE TABLE #AnimalCostsAll (thisIndex int IDENTITY(1,1), AnimalTypeId int, Cost decimal(20,2));
CREATE TABLE #TotalAnimalCostAll (thisIndex int IDENTITY(1,1), YearSetupId int, AnimalTypeId int, AnimalType varchar(max), OwnerId int, CustomerId int, AnimalCount int, TtlSpeciesCost decimal(20,2));
CREATE TABLE #CustomerPaymentsAll (thisIndex int IDENTITY(1,1), CustomerID nvarchar(max), TtlPayments decimal(20,2));
CREATE TABLE #CustomerInfoAll (thisIndex int IDENTITY(1,1), OwnerId int, CustomerId int, FName nvarchar(200), LName nvarchar(200),BName nvarchar(200));
CREATE TABLE #ThisReportAll (thisIndex int IDENTITY(1,1), CustomerID nvarchar(max), Year char(4), OverdueBalance decimal(20,2), YearSetupId int);
INSERT INTO #BalanceAdjustmentsAll (CustomerId, Amount)
SELECT CustomerId, SUM(Amount)
FROM BalanceAdjustment
WHERE YearSetupId = 3
GROUP BY CustomerId;
/* GET Costs per Animal for 'This' yearID */
INSERT INTO #AnimalCostsAll (AnimalTypeId, Cost)
SELECT AnimalTypeId, Cost
FROM PerCapitaFee
WHERE YearSetupId = 3;
/* GET animal type totals for owner per year */
INSERT INTO #TotalAnimalCostAll (yearSetupId,AnimalTypeId,AnimalType,OwnerId,CustomerId,AnimalCount,TtlSpeciesCost)
SELECT YearSetup.YearSetupId,AnimalCount.AnimalTypeId,AnimalType.ShortDescription,Owner.OwnerId,Report.CustomerId,AnimalCount.Count,(ac.Cost * AnimalCount.Count)
FROM AnimalCount
INNER JOIN #AnimalCostsAll as ac
ON ac.AnimalTypeId = AnimalCount.AnimalTypeId
INNER JOIN AnimalType
ON AnimalCount.AnimalTypeId=AnimalType.AnimalTypeId
INNER JOIN AnimalLocation
ON AnimalLocation.AnimalLocationid=AnimalCount.AnimalLocationId
INNER JOIN Owner
ON Owner.OwnerId=AnimalLocation.OwnerId
AND Owner.OwnerType = 'P'
INNER JOIN Report
ON Report.ReportId=Owner.ReportId
INNER JOIN YearSetup
ON Report.YearSetupId=YearSetup.YearSetupId
INNER JOIN County
ON County.CountyId=AnimalLocation.CountyId
WHERE YearSetup.YearSetupId = 3 AND Report.Completed IS NOT NULL AND Report.CustomerId IS NOT NULL
/* Get The total payments a customer has made */
INSERT INTO #CustomerPaymentsAll (CustomerID,TtlPayments)
SELECT BPS.CustomerId,SUM(BPS.Amount)
FROM BatchPaymentSplit BPS
LEFT JOIN BatchPayment bp ON BPS.BatchPaymentId=bp.BatchPaymentId
LEFT JOIN Batch b ON bp.BatchId=b.BatchId
WHERE BPS.CustomerId IS NOT NULL
AND
(
((b.BatchTypeId = 'M' OR b.BatchTypeId = 'C' OR b.BatchTypeId = 'E') AND (b.BatchStatusId = 'S'))
OR
((b.BatchTypeId = 'B' OR b.BatchTypeId = 'N' OR b.BatchTypeId = 'R' OR b.BatchTypeId = 'T') AND (b.BatchStatusId = 'S' OR b.BatchStatusId='C'))
)
AND
BPS.YearSetupId = 3
GROUP BY BPS.CustomerId;
/* Deal with the name/id stuff */
INSERT INTO #CustomerInfoAll(FName, LName, BName, OwnerId, CustomerId)
SELECT
o.FirstName AS FName,
o.LastName AS LName,
o.BusinessName AS BName,
o.OwnerId AS OwnerId,
r.CustomerId AS CustomerId
FROM Owner o
INNER JOIN Report r
ON o.ReportId = r.ReportId
AND o.OwnerType = 'P'
WHERE r.CustomerId IN (SELECT CustomerId FROM #TotalAnimalCostAll)
AND r.Completed IS NOT NULL
AND r.YearSetupId = 3
AND NOT EXISTS(
SELECT 1 FROM Report
WHERE r.CustomerId = Report.CustomerId
AND Report.Completed IS NOT NULL
AND r.ReportId != Report.ReportId
AND r.YearSetupId = Report.YearSetupId
AND (
r.Completed < Report.Completed
OR (
r.Completed = Report.Completed
AND r.ReportId < Report.ReportId
)
)
)
ORDER BY CustomerId;
/** MAKE IT SO #1 **************************************************/
/* Simply Joining The Customer Info to the calculated totals to avoid any aggregation shenanigans... */
INSERT INTO #ThisReportAll (CustomerID,Year,OverdueBalance,YearSetupId)
SELECT COALESCE(t.CustomerId,cp.CustomerId,ba.CustomerID), ys.Name AS Year,
CASE
WHEN (SUM(t.TtlSpeciesCost) < 5 AND SUM(t.TtlSpeciesCost) > 0) AND (ys.Name='2015' OR ys.Name='2016')
THEN (5) - Isnull(cp.TtlPayments,0) + Isnull(ba.Amount,0)
ELSE SUM(t.TtlSpeciesCost) - Isnull(cp.TtlPayments,0) + Isnull(ba.Amount,0)
END
AS TtlOwnerCost, t.YearSetupId AS YearSetupId
FROM #TotalAnimalCostAll t
FULL OUTER JOIN #CustomerPaymentsAll cp ON t.CustomerId=cp.CustomerID
FULL OUTER JOIN #BalanceAdjustmentsAll ba ON COALESCE(t.CustomerId,cp.CustomerId)=ba.CustomerID
LEFT JOIN YearSetup ys ON COALESCE(t.CustomerId,cp.CustomerId,ba.CustomerID) = ys.YearSetupId
GROUP BY COALESCE(t.CustomerId,cp.CustomerId,ba.CustomerID),ys.Name,cp.TtlPayments, ba.Amount, t.YearSetupId
HAVING
CASE WHEN (SUM(t.TtlSpeciesCost) < 5 AND SUM(t.TtlSpeciesCost) > 0) AND (ys.Name='2015' OR ys.Name='2016')
THEN SUM(5) - Isnull(cp.TtlPayments,0) + Isnull(ba.Amount,0)
ELSE SUM(t.TtlSpeciesCost) - Isnull(cp.TtlPayments,0) + Isnull(ba.Amount,0)
END < 0;
/* Return some meaningful report data */
SELECT r.Year AS [YearName],r.CustomerID,left(ci.FName,20) AS [FirstName], left(ci.LName,40) AS [LastName], left(ci.BName,40) AS [BusinessName],r.OverdueBalance AS [Balance],r.YearSetupId
FROM #ThisReportAll r
LEFT JOIN #CustomerInfoAll ci ON r.CustomerID = ci.CustomerId
ORDER BY CAST(r.CustomerID as int) ASC;
DROP TABLE #BalanceAdjustmentsAll;
DROP TABLE #AnimalCostsAll;
DROP TABLE #TotalAnimalCostAll;
DROP TABLE #CustomerPaymentsAll;
DROP TABLE #CustomerInfoAll;
DROP TABLE #ThisReportAll;
Found it. I didn't have a default value for t.TtlSpeciesCost if it was null
SUM(t.TtlSpeciesCost) - Isnull(cp.TtlPayments,0) + Isnull(ba.Amount,0)
to
SUM(ISNULL(t.TtlSpeciesCost,0)) - Isnull(cp.TtlPayments,0) + Isnull(ba.Amount,0)
Some missing records may be found here:
by adjusting /* Get The total payments a customer has made */
INSERT INTO #CustomerPaymentsAll (CustomerID,TtlPayments)
SELECT BPS.CustomerId,SUM(BPS.Amount)
FROM BatchPaymentSplit BPS
LEFT JOIN BatchPayment bp
ON BPS.BatchPaymentId=bp.BatchPaymentId
LEFT JOIN Batch b
ON bp.BatchId=b.BatchId
AND ((b.BatchTypeId IN ('M', 'C', 'E') AND b.BatchStatusId = 'S')
OR (b.BatchTypeId IN ('B','N','R','T') AND (b.BatchStatusId IN ('S','C')))
WHERE BPS.CustomerId IS NOT NULL
AND BPS.YearSetupId = 3
GROUP BY BPS.CustomerId;
The WHERE on B would have negated the left join causing null records to be omitted. or made the left join to behave like an inner join.
To know for certain we need sample data from your tables showing which records are being omitted that you need to retain.
I also refactored the OR's and made them "IN"s to improve readability.

SQL: performantly find the percent overlap between two foreign key child tables

Assume I have the following tables/fields:
CREATE TABLE tbl_projects (
prjc_id int PRIMARY KEY
)
CREATE TABLE tbl_project_requirements (
preq_prjc_id int -- Foreign key to tbl_projects
preq_type_id int -- A standardized requirement category
)
Given a specific project, I would like to find other projects that have nearly similar requirement categories... or let's say at least a 75% overlap on their requirements.
I could do the following:
DECLARE #prjc_id int = 1
CREATE TABLE #project_reqs (type_id int)
INSERT INTO #project_reqs
SELECT preq_req_type_id
FROM tbl_project_requirements
WHERE preq_prjc_id = #prjc_id
SELECT prjc_id
FROM tbl_projects
CROSS APPLY (
SELECT CASE
WHEN COUNT(*) = 0 THEN 0.0
ELSE COALESCE(SUM(CASE WHEN type_id = prjc_type_id THEN 1.0 ELSE 0.0 END), 0.0)
/ CONVERT(float, COUNT(*))
END AS similarity
FROM #project_reqs
FULL OUTER JOIN (
SELECT prjc_type_id
FROM tbl_project_requirements
WHERE preq_prjc_id = prjc_id
) reqs ON preq_type_id = type_id
) reqs
WHERE prjc_id != #prjc_id
AND similarity >= 0.75
In the above, I'm dividing the matched requirement categories by the total distinct requirement categories between each two projects to get the % overlap.
While this works, I sense code smells, and don't think this will scale very well. Is there any sort of method that exists to performantly calculate overlap of child records between two items? Maybe some sort of partial hash matching or...?
Update
I think I found a performant solution:
DECLARE #prjc_id int = 1
CREATE TABLE #project_reqs (type_id int)
INSERT INTO #project_reqs
SELECT preq_req_type_id
FROM tbl_project_requirements
WHERE preq_prjc_id = #prjc_id
DECLARE #project_req_count float
SELECT #project_req_count = COUNT(*)
FROM #project_reqs
CREATE TABLE #projects (
pj_prjc_id int,
pj_func_count float,
pj_func_common float
)
INSERT INTO #projects
SELECT preq_prjc_id,
COUNT(*),
COUNT(type_id)
FROM tbl_project_requirements
LEFT OUTER JOIN #project_reqs
ON preq_type_id = type_id
GROUP BY preq_prjc_id
HAVING COUNT(type_id) != 0
SELECT pj_prjc_id
FROM #projects
WHERE pj_func_common / (pj_func_count + #project_req_count - pj_func_common) >= 0.75
DROP TABLE #project_reqs
DROP TABLE #projects
There is more elegant way to find common requirements.
;with proj as (
select preq_prjc_id pr, count(preq_type_id) typeCnt
from tbl_project_requirements
group by preq_prjc_id
)
,crossProj as (
select p1.pr proj1,p2.pr proj2, p1.typeCnt
from proj p1
cross join proj p2 --make Cartesian product
where p1.pr <> p2.pr
)
,req as (
select preq_type_id, cp.proj1, cp.proj2, cp.typeCnt
from tbl_project_requirements pq
inner join crossProj cp on pq.preq_prjc_id=cp.proj1
intersect -- what is common
select preq_type_id, cp.proj1, cp.proj2, cp.typeCnt
from tbl_project_requirements pq
inner join crossProj cp on pq.preq_prjc_id=cp.proj2
)
--calculate final result
select proj1, proj2,
count(preq_type_id) commonPreq,
--percent of common requirements relative to proj1
count(preq_type_id) * 100.00 / typeCnt [percentage]
from req
group by proj1, proj2, typeCnt
having count(preq_type_id) * 100.00 / typeCnt >75
order by [percentage] desc
Update
;with proj as (
select preq_prjc_id pr, count(preq_type_id) typeCnt
from tbl_project_requirements
group by preq_prjc_id
)
,crossProj as (
select p1.pr proj1,p2.pr proj2, p1.typeCnt
from proj p1
cross join proj p2 --make Cartesian product
where p1.pr <> p2.pr
)
,req as (
select preq_type_id, cp.proj1, cp.proj2
from tbl_project_requirements pq
inner join crossProj cp on pq.preq_prjc_id=cp.proj1
intersect -- what is common
select preq_type_id, cp.proj1, cp.proj2
from tbl_project_requirements pq
inner join crossProj cp on pq.preq_prjc_id=cp.proj2
)
--calculate final result
select proj1, proj2,
count(preq_type_id) commonPreq,
--percent of common requirements relative to proj1
count(preq_type_id) * 100.00 /(p1.typeCnt + p2.typeCnt - count(preq_type_id)) [percentage]
from req
inner join proj p1 on req.proj1=p1.pr
inner join proj p2 on req.proj2=p2.pr
group by proj1, proj2,p1.typeCnt, p2.typeCnt
having count(preq_type_id) * 100.00 /(p1.typeCnt + p2.typeCnt - count(preq_type_id)) >75
order by [percentage] desc
If you're only doing this for one project then you should already know the number of matches that are required for a 75% match (or at least you can calculate it easily and quickly) enough:
DECLARE #num_matches_required INT
SELECT #num_matches_required = CEILING(COUNT(*) * 0.75)
FROM
tbl_Project_Requirements
WHERE
preq_prjc_id = #preq_prjc_id
SELECT
R2.preq_prjc_id -- One reason not to use abbreviations... I have to think, "Is it proj? prj? prjc? prjct?"
FROM
tbl_Project_Requirements R1
INNER JOIN tbl_Project_Requirements R2 ON
R2.preq_type_id = R1.preq_type_id AND
R2.preq_prjc_id <> #preq_prjc_id
WHERE
R1.preq_prjc_id = #preq_prjc_id
GROUP BY
R2.preq_prjc_id
HAVING
COUNT(*) >= #num_matches_required

How to declare and assign value to a variable before query?

I have a complicated query that has to use a number called SubUnitRate. This variable comes from another table with special condition. in short we have:
DECLARE
SUBUNITRATE NUMBER;
BEGIN
SELECT NVL (NULLIF (CU.SUBUNITRATE, 0), 1)
INTO SUBUNITRATE
FROM CURRENCYS CU
JOIN ACCOUNTS ACC ON CU.ID = ACC.CURRENCY
WHERE ACC.ID = :ACCOUNTID;
END;
SELECT SUBUNITRATE * 100 FROM DUAL;
My goal is to acquire the result of(in simple case):
SELECT SUBUNITRATE * 100 FROM DUAL;
But how is that possible?
Assuming you want to use the value of SUBUNITRATE multiple times in the same query you could use the WITH clause:
with cte as (
select case
when CU.SUBUNITRATE = 0 then 1
else CU.SUBUNITRATE
end as SUBUNITRATE
FROM CURRENCYS CU
JOIN ACCOUNTS ACC ON CU.ID = ACC.CURRENCY
WHERE ACC.ID = :ACCOUNTID
)
select cte.SUBUNITRATE * 100
from cte;
A PL/SQL block cannot return the results of a query as a query. Instead, you can print the results out.
So, does this do what you want?
DECLARE
SUBUNITRATE NUMBER;
BEGIN
SELECT NVL(NULLIF(CU.SUBUNITRATE, 0), 1)
INTO SUBUNITRATE
FROM CURRENCYS CU JOIN
ACC
ON CU.ID = ACC.CURRENCY
WHERE ACC.ID = :ACCOUNTID;
DBMS_OUTPUT.PUT_LINE(SUBUNITRATE * 100)
END;
No need for PL. APC' solution, simplified and in a form you can use directly in your query (wherever you would say ... = subunitrate, say ... = (select sur from cte) instead - including the parentheses):
with cte_prelim as (select subunitrate from ... etc.),
cte (sur) as select case when subunit rate is null or subunitrate = 0 then 100
else subunitrate * 100 end from cte_prelim)
select... (your query where you need to use the value)

Display Y/N column if record found in detail table

I'm trying to create a query so that I can have a column show Y/N if a particular item was ordered for a group of orders. The item I'm looking for would be OLI.id = '538'.
So my results would be:
Order#, Customer#, FreightPaid
12345, 00112233, Y
12346, 00112233, N
I cannot figure out if I need to use a subquery or the where exists function ?
Here's my current query:
SELECT distinct
OrderID,
Accountuid as Customerno
FROM [SMILEWEB_live].[dbo].[OrderLog] OL
inner join Orderlog_item OLI on OLI.orderlogkey = OL.[key]
inner join Account A on A.uid = OL.Accountuid
where A.GroupId = 'X9955'
and OL.CreateDate >= GETDATE() - 60
I would suggest an exists clause instead of a join:
select ol.OrderID, ol.Accountuid as Customerno,
(case when exists (select 1
from Orderlog_item OLI join
Account A
on A.uid = OL.Accountuid
where OLI.orderlogkey = OL.[key] and A.GroupId = 'X9955'
)
then 1 else 0
end) as flag
from [SMILEWEB_live].[dbo].[OrderLog] OL
where OL.CreateDate >= GETDATE() - 60;
This prevents a couple of problems. First, duplicate rows which are caused when there are multiple matching rows (and select distinct add unnecessary overhead). Second, missing rows, which happen when you use inner join instead of an outer join.