Selecting Parent Rows With Multiple Conditions - sql

I had most of this query down until a new condition arose and it has confounded me. Given the following simplified table schema:
Parent Table:
ID
FName
LName
Child Table:
[Index]
ParentID
Active_Flag
ExpirationDate
What I want to do is get Parent rows for which:
There are no children.
There are children whose Active_Flag is 1 but whose expiration dates are blank or NULL.
There are indeed children but none have the Active_Flag set to 1.
The following query came up with my first two criteria:
SELECT p.ID, p.LNAME, p.FNAME,
CASE
WHEN COUNT(ct.indx) = 0 THEN 'None'
WHEN ct.ExpirationDate is NULL or ct.ExpirationDate = '' THEN 'No expiration date'
END AS Issue
FROM ParentTable AS p
LEFT JOIN ChildTable ct
ON p.ID = ct.ParentID
GROUP BY p.ID, p.LNAME, p.FNAME, ct.[INDEX], ct.ExpirationDate
HAVING (COUNT(ct.[INDEX]) = 0) OR (ct.ExpirationDate IS NULL OR ct.ExpirationDate = '')
ORDER BY p.LNAME
I don't know how to account for #3. Any help is appreciated. Thanks in advance.

You can also do this in the HAVING clause:
SELECT p.ID, p.LNAME, p.FNAME,
(CASE WHEN COUNT(ct.indx) = 0 THEN 'None'
WHEN ct.ExpirationDate is NULL or ct.ExpirationDate = '' THEN 'No expiration date'
WHEN sum(case when ActiveFlag = 1 then 1 else 0 end) = 0 then 'No active children'
END) AS Issue
FROM ParentTable p LEFT JOIN
ChildTable c
ON p.ID = ct.ParentID
GROUP BY p.ID, p.LNAME, p.FNAME
HAVING (COUNT(ct.[INDEX]) = 0) OR
(ct.ExpirationDate IS NULL OR ct.ExpirationDate = '') or
sum(case when ActiveFlag = 1 then 1 else 0 end) = 0
ORDER BY p.LNAME
The DISTINCT in the SELECT is redundant. You do not need it with an aggregation.
You can simplify the having to "sum(ActiveFlag)" if the activeFlag is indeed an integer. If not, then it should be "= '1'" rather than "= 1'.

you can use unions for this query.....
I am not sure about this query ....but it will help you in solving your prolem
select p.id,p.lname,p.fname from parents where (p.id not in (select pid from children))
union
select p.id,p.lname,p.fname from parents p,children c inner join on c.pid=p.id where c.active_flag=1 and c.expiration_date is null or c.expiration_date=''
union
select p.id,p.lname,p.fname from parents p inner join on c.pid=p.id where c.active_flag<>1;

SELECT *
FROM parenttable pt
-- condition 1: There should be no parents without children at all
WHERE NOT EXISTS (
SELECT * FROM childtable c1
WHERE c1.parent_id = pt.id
)
-- condition 2: There should be no children with a flag but without a date
OR EXISTS (
SELECT * FROM childtable c2
WHERE c2.parent_id = pt.id
AND c2.active_flag = 1 AND c2.expire_date IS NULL
)
-- condition 3: There should at least be a child with the active_flag
OR NOT EXISTS (
SELECT * FROM childtable c3
WHERE c3.parent_id = pt.id
AND c2.active_flag = 1
)
;
-- Active flags for children c1 and c2
-- c1 c2 (X= child doesn't exists)
-----+-----+---+
-- X X rule1+rule3 # no children at all
-- 0 X rule3 # only one child
-- 1 X # idem
-- 0 0 rule 3 # two children
-- 0 1
-- 1 0
-- 1 1
--
-- , which means that rule3 implies rule1, and rule1 is redundant
-------------------------
SELECT *
FROM parenttable pt
-- condition 1+3: There should at least be a child with the active_flag
WHERE NOT EXISTS (
SELECT * FROM childtable c1
WHERE c1.parent_id = pt.id
AND c1.active_flag = 1
)
-- condition 2: There should be no children with a flag but without a date
OR EXISTS (
SELECT * FROM childtable c2
WHERE c2.parent_id = pt.id
AND c2.active_flag = 1 AND c2.expire_date IS NULL
)
;

Related

SQL query where has children with conditions

Let's say I have:
a parents table with the following columns: (id, name)
a children attributes table with the following columns: (id, child_id, parent_id, attribute, attribute_value)
Now I want to filter any parent id's that has at least a child with both:
attribute => intelligence of 5
attribute => health of 4
Either one child with intelligence of 5 and health of 4, or one child has intelligence of 5 and another child has health of 4.
How would you query that, in PostgreSQL? Thank you
You can just do the intersection of
parents that have children with intelligence 5
parents that have children with health 4
(SELECT parent_id
FROM tab
WHERE attribute = 'intelligence'
AND attribute_value = 5 )
INTERSECT
(SELECT parent_id
FROM tab
WHERE attribute = 'health'
AND attribute_value = 4 )
If you only wants parents info:
SELECT
DISTINCT parents.id, parents.name
FROM
parents
LEFT JOIN attributes ON parents.id = attributes.parent_id
WHERE
(attribute = 'intelligence' AND attribute_value = 5)
OR (attribute = 'health' AND attribute_value = 4)
First we need to join the tables -- like this
select p.id as p_id, p.name as parent_name,
k.* -- we won't need this in later versions
from parent p
join kidatt k on p.id = k.parent_id
now we have two attributes we care about -- let make a query that shows those
select p.id as p_id, p.name as parent_name,
case when k.attribute = 'intelligence' and k.attribute_value = 5 then 1 else 0 end as has_a1,
case when k.attribute = 'health' and k.attribute_value = 4 then 1 else 0 end as has_a2
from parent p
join kidatt k on p.id = k.parent_id
we now have a query with a 1 in a row for those that have each of these
now we group by the parent.
select p.id as p_id, p.name as parent_name,
SUM(case when k.attribute = 'intelligence' and k.attribute_value = 5 then 1 else 0 end) as has_a1,
SUM(case when k.attribute = 'health' and k.attribute_value = 4 then 1 else 0 end) as has_a2
from parent p
join kidatt k on p.id = k.parent_id
group by p.id, p.name
now we have a query where a1 and a2 are greater than 0 if one or more child has it.
Now just select the results
select *
from (
select p.id as p_id, p.name as parent_name,
SUM(case when k.attribute = 'intelligence' and k.attribute_value = 5 then 1 else 0 end) as has_a1,
SUM(case when k.attribute = 'health' and k.attribute_value = 4 then 1 else 0 end) as has_a2
from parent p
join kidatt k on p.id = k.parent_id
group by p.id, p.name
)
where has_a1 > 0 and has_a2 > 0
note -- I did not write this query to be the best way to solve this problem -- instead I wrote it in a way to show you how to "think" in SQL and solve the problem with a series of steps.
I'd have to test to be sure, but I expect this would be the fastest way to do this query (depends on data and indexes etc.)
select distinct p.id as p_id, p.name as parent_name,
from parent p
join kidatt k on p.id = k.parent_id
where k.attribute = 'intelligence' and k.attribute_value = 5
intersect
select distinct p.id as p_id, p.name as parent_name,
from parent p
join kidatt k on p.id = k.parent_id
where k.attribute = 'health' and k.attribute_value = 4

SELECT first row in recursive part of table expression

I tried to get the first row for a WHERE statement in a recursive table expression. Sadly i'm getting this error:
The TOP or OFFSET operator is not allowed in the recursive part of a recursive common table expression 'cteTree'.
Here's my SQL Query:
WITH cteTree AS(
SELECT
cct.CategoryID AS bla,
ct.CategoryID,ct.ParentCategoryID,
ct.Name,
ct.Published,
CASE
WHEN EXISTS(SELECT ProductID FROM Product WHERE ProductID = (SELECT TOP(1) ProductID FROM ProductCategory WHERE ProductCategory.CategoryID = ct.CategoryID) AND Product.Published = 1) THEN 1
ELSE 0
END AS ProductExists,
1 As Cycle
FROM Category AS ct
LEFT JOIN Category AS cct ON cct.ParentCategoryID = ct.CategoryID
WHERE cct.CategoryID IS NULL
UNION ALL
SELECT
cct.CategoryID AS bla,
ct.CategoryID,
ct.ParentCategoryID,
ct.Name,
ct.Published,
CASE
WHEN cct.ProductExists = 1 THEN 1
WHEN EXISTS(SELECT ProductID FROM Product WHERE ProductID = (SELECT TOP(1) ProductID FROM ProductCategory WHERE ProductCategory.CategoryID = ct.CategoryID) AND Product.Published = 1) THEN 1
ELSE 0
END AS ProductExists,
Cycle + 1
FROM Category AS ct
JOIN cteTree AS cct ON ct.CategoryID = cct.ParentCategoryID
)
SELECT * FROM cteTree
The problem is in the second Case statement under UNION ALL.
SELECT ProductID FROM Product WHERE ProductID = (SELECT TOP(1) ProductID FROM ProductCategory WHERE ProductCategory.CategoryID = ct.CategoryID) AND Product.Published = 1
Does someone know if there's another expression for selecting the first row in a recursive Table expression that works?
There are some tricks to use TOP (1) in a CTE, such as using ROW_NUMBER instead, or putting it into a TVF. But in your case you can just use a normal join:
You should also use NOT EXISTS instead of the LEFT JOIN IS NULL construct.
WITH cteTree AS(
SELECT
cct.CategoryID AS bla,
ct.CategoryID,ct.ParentCategoryID,
ct.Name,
ct.Published,
CASE
WHEN EXISTS (SELECT 1
FROM Product p
JOIN ProductCategory pc ON pc.CategoryID = ct.CategoryID
WHERE p.Published = 1
) THEN 1
ELSE 0
END AS ProductExists,
1 As Cycle
FROM Category AS ct
WHERE NOT EXISTS (SELECT 1
FROM Category AS cct
WHERE cct.ParentCategoryID = ct.CategoryID
)
UNION ALL
SELECT
cct.CategoryID AS bla,
ct.CategoryID,
ct.ParentCategoryID,
ct.Name,
ct.Published,
CASE
WHEN cct.ProductExists = 1 THEN 1
WHEN EXISTS (SELECT 1
FROM Product p
JOIN ProductCategory pc ON pc.CategoryID = ct.CategoryID
WHERE p.Published = 1
) THEN 1
ELSE 0
END AS ProductExists,
Cycle + 1
FROM Category AS ct
JOIN cteTree AS cct ON ct.CategoryID = cct.ParentCategoryID
)
SELECT *
FROM cteTree

Replace ISNULL function in WHERE clause

I was asked to rewrite the below queries by eliminating the ISNULL Function in where clause due to performance issues. How can I rewrite the below queries by replacing the ISNULL Function in WHERE clause and in JOIN condition. Below are the sample queries.
Select *
from child c
inner join Parent P on c.id=p.id
where ISNULL(p.ParentId, 0) <> 0
Select *
from child c
inner join Parent P on isnull(c.Parentid,0)=p.id
select *
from parent
where isnull(Status, '') != 'Active'
Select *
from child c
inner join Parent P on c.id=p.id
where CAST(P.PostedDate AS DATE)
BETWEEN CAST(isnull(#FromDate,P.PostedDate) AS DATE)
AND CAST(#ToDate AS DATE)
SELECT *
FROM TEMP
WHERE EDATE=ISNULL(#EDATE,EDATE)
In most cases you can simply remove the isnull function, and it need not be replaced by anything. For example:
select *
from parent
where isnull(Status, '') != 'Active'
Can be rewritten as
select *
from parent
where Status != 'Active'
Since if Status is null, the comparison still returns FALSE. The rule is:
NULL = value: NULL (interpreted as FALSE, since it is not TRUE).
If NULL has to be included in your results, you can add Attribute/#Variable IS NULL.
Be carefull with this
Look at this example :
declare #table table(ID int, status varchar(50))
insert into #Table (ID, status)
values (1, 'active'), (2, ''), (3, null), (4, 'active')
select t.*
from #Table t
where t.status <> 'active'
This will return just one record (ID: 2)
while this
select t.*
from #Table t
where isnull(t.status, '') <> 'active'
will return 2 records (ID: 2 and 3)
So it depends on what you want in your result set, do you want records where the field Status is null also returned or not ?
In case you dont want them then the answer is simple
select t.*
from #Table t
where t.status <> 'active'
Otherwise you need something like this
Since the original query had IsNull in it, I suspect this is what you want.
select t.*
from #Table t
where (t.status is null or t.status <> 'active')
This does not use the IsNull() but I am not sure if much performance will be gained here.
Select * from child c inner join Parent P on c.id=p.id where ISNULL(p.ParentId, 0) <> 0
Here, neither you need ISNULL nor any alternative of this because in this case
NULL <> 0 will be FALSE similarly 0 <> 0 also FALSE
NULL = NULL will be FALSE if the setting SET ANSI_NULLS ON else TRUE
And here, the alternative of this is,
If you want to return NULL as TRUE then you can use
WHERE (p.ParentId IS NULL OR p.ParentId <> 0)
Otherwise,
(p.ParentId <> 0 OR 0 <> 0) --But it doesn't have any meaning and it's exactly same to simply use p.ParentId <> 0
You can handle ISNULL accordingly in your all the questions.
Please try like this.
Select * from child c inner join Parent P
on c.id=p.id OR where p.ParentId IS NULL
Select * from child c inner join Parent P on c.Parentid=p.id OR c.ParentId IS NULL
select * from parent
where [Status] != 'Active' AND [Status] IS NOT NULL
Select * from child c inner join Parent P on c.id=p.id
where
( (P.PostedDate) BETWEEN CAST(#FromDate) AS DATE) AND #FromDate IS NOT NULL AND CAST(#ToDate AS DATE) )
OR
( (P.PostedDate) BETWEEN P.PostedDate AND CAST(#ToDate AS DATE) )
SELECT * FROM TEMP
WHERE EDATE = CASE WHEN #EDATE IS NULL THEN EDATE ELSE #EDATE END
Substitute as below to make SARGABLE:
where ISNULL(p.ParentId, 0) <> 0 --- where p.ParentId = 0 or p.ParentID is null
Select * from child c inner join Parent P on isnull(c.Parentid,0)=p.id --- Select * from child c inner join Parent P on c.Parentid = p.id OR (c.Parentid is null and p.id = 0)
where isnull(Status, '') != 'Active' --- where Status <> 'Active'
where CAST(P.PostedDate AS DATE) BETWEEN CAST(isnull(#FromDate,P.PostedDate) AS DATE) AND CAST(#ToDate AS DATE) --- where P.PostedDate BETWEEN isnull(#FromDate,P.PostedDate) AND #ToDate
WHERE EDATE=ISNULL(#EDATE,EDATE) --- WHERE #EDATE IS NULL OR EDATE = #EDATE
When values can be null you have to use the Three-value logic.
Read this article.
So if you want to make your arguments SARGABLE avoiding to use ISNULL or COALESCE functions, you have to combine OR/AND/NOT/CASE operators, depending on logic of your query
In your examples:
Select *
from child c
inner join Parent P on c.id=p.id
-- where ISNULL(p.ParentId, 0) <> 0
where p.ParentId <> 0 -- when p.ParentId is null condition is evaluated as unknown and record are excluded.
Select *
from child c
-- inner join Parent P on isnull(c.Parentid,0)=p.id
inner join Parent P on (c.Parentid=p.id) OR (c.Parentid IS NULL AND p.id = 0)
select *
from parent
--where isnull(Status, '') != 'Active'
where Status IS NULL OR Status <> 'Active'
-- IN THIS CASE YOU HAVE NO BENEFIT BECAUSE CONVERSIONS MAKES YOUR ARGUMENTS NOT SARGABLE!!!
Select *
from child c
inner join Parent P on c.id=p.id
where CAST(P.PostedDate AS DATE)
BETWEEN CAST(CASE WHEN #FromDate IS NULL THEN P.PostedDate ELSE #FromDate END) AS DATE)
AND CAST(#ToDate AS DATE)
SELECT *
FROM TEMP
--WHERE EDATE=ISNULL(#EDATE,EDATE)
WHERE EDATE=#EDATE

Error using CTE in SQLITE3

I use these codes to get level(depth) of my parent/child categories table. and the isLeaf attribute:
with cteCat as (
select
id, parent,
[cteLevel] = 1
from Categories
where 1=1 and parent=0
union all
select
c.id, c.parent,
[cteLevel] = cc.cteLevel+1
from Categories c
join cteCat cc
on c.parent = cc.id
where 1=1
and c.parent <> 0
)
select
*
, CASE WHEN EXISTS (SELECT * FROM Categories c2 WHERE c2.parent = c1.id) THEN 0 ELSE 1 END AS IsLeaf
, (SELECT COUNT(*) FROM Categories c2 WHERE c2.parent = c1.id) AS Leafs
from cteCat c1
order by c1.id
result is:
id parent cteLevel IsLeaf Leafs
1 0 1 0 2
....
it's OK in SQLSERVER. but when i execute at sqlite, I get Error:
Error while executing SQL query on database 'ado':
no such column: cc.cteLevel
Any help? thanks.
SQLite does not support variable assignments like this.
However, you should be able to do the same with standard SQL:
WITH cteCat AS (
SELECT id,
parent,
1 AS cteLevel
FROM ...
WHERE ...
UNION ALL
SELECT c.id,
c.parent,
cc.cteLevel + 1
FROM ...
WHERE ...
)
SELECT ...

Join table on conditions, count on conditions

SELECT *, null AS score,
'0' AS SortOrder
FROM products
WHERE datelive = -1
AND hidden = 0
UNION
SELECT e.*, (SUM(r.a)/(COUNT(*)*1.0)+
SUM(r.b)/(COUNT(*)*1.0)+
SUM(r.c)/(COUNT(*)*1.0)+
SUM(r.d)/(COUNT(*)*1.0))/4 AS score,
'1' AS SortOrder
FROM products e
LEFT JOIN reviews r
ON r.productID = e.productID
WHERE e.hidden = 0
AND e.datelive != -1
GROUP BY e.productID
HAVING COUNT(*) >= 5
UNION
SELECT e.*, (SUM(r.a)/(COUNT(*)*1.0)+
SUM(r.b)/(COUNT(*)*1.0)+
SUM(r.c)/(COUNT(*)*1.0)+
SUM(r.d)/(COUNT(*)*1.0))/4 AS score,
'2' AS SortOrder
FROM products e
LEFT JOIN reviews r
ON r.productID = e.productID
WHERE e.hidden = 0
AND e.datelive != -1
GROUP BY e.productID
HAVING COUNT(*) < 5
ORDER BY SortOrder ASC, score DESC
This creates an SQL object for displaying products on a page. The first request grabs items of type datelive = -1, the second of type datelive != -1 but r.count(*) >= 5, and the third of type datelive != -1 and r.count(*) < 5. The reviews table is structured similar to the below:
reviewID | productID | a | b | c | d | approved
-------------------------------------------------
1 1 5 4 5 5 1
2 5 3 2 5 5 0
3 2 5 5 4 3 1
... ... ... ... ... ... ...
I'm trying to work it such that r.count(*) only cares for rows of type approved = 1, since tallying data based on unapproved reviews isn't ideal. How can I join these tables such that the summations of scores and the number of rows is dependent only on approved = 1?
I've tried adding in AND r.approved = 1 in the WHERE conditional for the joins and it doesn't do what I'd like. It does sort it properly, but then it no longer includes items with zero reviews.
You seem to be nearly there.
In your question you talked about adding the AND r.approved = 1 to the join criteria but by the sounds of it you are actually adding it to the WHERE clause.
If you instead properly add it to the join criteria like below then it should work fine:
SELECT *, null AS score,
'0' AS SortOrder
FROM products
WHERE datelive = -1
AND hidden = 0
UNION
SELECT e.*, (SUM(r.a)/(COUNT(*)*1.0)+
SUM(r.b)/(COUNT(*)*1.0)+
SUM(r.c)/(COUNT(*)*1.0)+
SUM(r.d)/(COUNT(*)*1.0))/4 AS score,
'1' AS SortOrder
FROM products e
LEFT JOIN reviews r ON r.productID = e.productID
WHERE e.hidden = 0
AND e.datelive != -1
GROUP BY e.productID
HAVING COUNT(*) >= 5
UNION
SELECT e.*, (SUM(r.a)/(COUNT(*)*1.0)+
SUM(r.b)/(COUNT(*)*1.0)+
SUM(r.c)/(COUNT(*)*1.0)+
SUM(r.d)/(COUNT(*)*1.0))/4 AS score,
'2' AS SortOrder
FROM products e
LEFT JOIN reviews r ON r.productID = e.productID AND r.approved = 1
WHERE e.hidden = 0
AND e.datelive != -1
GROUP BY e.productID
HAVING COUNT(*) < 5
ORDER BY SortOrder ASC, score DESC
SQL Fiddle here.
Notice again how I have simply put the AND r.approved = 1 directly after LEFT JOIN reviews r ON r.productID = e.productID which adds an extra criteria to the join.
As I mentioned in my comment, the WHERE clause will filter rows out of the combined record set after the join has been made. In some cases the RDBMS may optimise it out and put it into the join criteria but only where that would make no difference to the result set.
Calculating the non-zero sums and joining it to your result may solve it;
fiddle
SELECT a.productID,
NULL AS score,
'0' AS SortOrder
FROM products a
WHERE datelive = -1
AND hidden = 0
UNION
SELECT e.productID,
(min(x.a)/(min(x.cnt)*1.0)+ min(x.b)/(min(x.cnt)*1.0)+ min(x.c)/(min(x.cnt)*1.0)+ min(x.d)/(min(x.cnt)*1.0))/4 AS score,
'1' AS SortOrder
FROM products e
JOIN reviews r ON r.productID = e.productID
LEFT JOIN
(SELECT ee.productID,
sum(rr.a) AS a,
sum(rr.b) AS b,
sum(rr.c) AS c,
sum(rr.d) AS d,
count(*) AS cnt
FROM products ee
LEFT JOIN reviews rr ON ee.productID = rr.productID
GROUP BY ee.productID) x ON e.productID = x.productID
WHERE e.hidden = 0
AND e.datelive != -1
GROUP BY e.productID HAVING COUNT(*) >= 5
UNION
SELECT e.productID,
(min(x.a)/(min(x.cnt)*1.0)+ min(x.b)/(min(x.cnt)*1.0)+ min(x.c)/(min(x.cnt)*1.0)+ min(x.d)/(min(x.cnt)*1.0))/4 AS score,
'2' AS SortOrder
FROM products e
LEFT JOIN reviews r ON r.productID = e.productID
LEFT JOIN
(SELECT ee.productID,
sum(rr.a) AS a,
sum(rr.b) AS b,
sum(rr.c) AS c,
sum(rr.d) AS d,
count(*) AS cnt
FROM products ee
LEFT JOIN reviews rr ON ee.productID = rr.productID
GROUP BY ee.productID) x ON e.productID = x.productID
WHERE e.hidden = 0
AND e.datelive != -1
GROUP BY e.productID HAVING COUNT(*) < 5
ORDER BY SortOrder ASC,
score DESC
You could create a temp table that only contains rows where approved = 1, and then join on the temp table instead of reviews.
create table tt_reviews like reviews;
insert into tt_reviews
select * from reviews
where approved = 1;
alter table tt_reviews add index(productID);
Then replace reviews with tt_reviews in your above query.