SQL where nested select not null - sql

I have a Customers table with CustomerID and CustomerName.
I then have a Orders table with CustomerID, datetime OrderPlaced and datetime OrderDelivered.
Bearing in mind that not all customers have placed orders, I would like to get a list of CustomerName, OrderPlaced and OrderDelivered but only for customers that have placed orders and whose orders have already been delivered, and only the most recent OrderPlaced per customer.
I started by doing (fully aware that this does not implement the OrderDelivered limitation to it yet, but already not doing what I want):
SELECT CustomerID,
(SELECT TOP 1 OrderDelivered
FROM Orders ORDER BY OrderDelivered DESC) AS OrderDelivered
FROM Customer
WHERE OrderDelivered IS NOT NULL
But already MS SQL doesn't like this, it says that it doesn't know what OrderDelivered is on the WHERE clause.
How can I accomplish this?

Personally, I would move your subquery into the FROM and use CROSS APPLY. Then you can far more easily reference the column:
SELECT C.CustomerID,
O.OrderDelivered
FROM Customer C
CROSS APPLY (SELECT TOP 1 OrderDelivered
FROM Orders oa
WHERE oa.CustomerID = C.CustomerID --Guess column name for orders
AND O.OrderDelivered IS NOT NULL
ORDER BY O.OrderDelivered DESC) O;
As, however, this is a CROSS APPLY, then the results will already be filtered; so no need for the WHERE.

If you want the most recent delivered order, then one method uses apply:
select c.*, o.OrderPlaced, o.OrderDelivered
from customer c cross apply
(select top (1) o.*
from orders o
where o.CustomerID = c.CustomerID and
o.OrderDelivered is not null
order by o.OrderPlaced desc
) o;

You can achieve this by using the OVER clause (https://learn.microsoft.com/en-us/sql/t-sql/queries/select-over-clause-transact-sql).
DECLARE #customers TABLE (CustomerId INT, CustomerName NVARCHAR(20))
DECLARE #orders TABLE (CustomerId INT, OrderPlaced DATETIME, OrderDelivered DATETIME)
INSERT INTO #customers VALUES
(1, 'a'),
(2, 'b')
INSERT INTO #orders VALUES
(1, '2019-01-01', null),
(2, '2019-01-03', '2019-02-01'),
(2, '2019-01-05', null)
SELECT
c.CustomerName,
-- Latest OrderPlaced
FIRST_VALUE(o.OrderPlaced)
OVER(PARTITION BY c.CustomerId ORDER BY o.OrderPlaced DESC) AS OrderPlaced,
-- The matching OrderDelivered
FIRST_VALUE(o.OrderDelivered)
OVER(PARTITION BY c.CustomerId ORDER BY o.OrderPlaced DESC) AS OrderDelivered
FROM #customers c
INNER JOIN #orders o ON o.CustomerId = c.CustomerId
WHERE o.OrderDelivered IS NOT NULL

Related

How to count the number of times each value of an attribute from one table, appears in another table? And if there is no appearance return zero

If I have a CUSTOMER table with the attribute customer_id and an ORDER table
with the attributes order_id and customer_id.
How do I find the total number of orders submitted by each customer and if a customer has none, return zero.
I have tried the following:
SELECT c.customer_id, COUNT(*)
FROM Customer c, Orders o
WHERE c.customer_id= o.customer_id
GROUP BY c.customer_id;
With the above, I am able to display the number of orders made by each customer, only if they made an order.
How do I also display count 0 for those customers who did not make any order?
Use an outer join and count the rows in the "outer" table:
SELECT c.customer_id, COUNT(o.customer_id)
FROM Customer c
LEFT JOIN Orders o ON c.customer_id= o.customer_id
GROUP BY c.customer_id;
You can use LEFT JOIN and in the COUNT() place the o.customer_id
SELECT c.customer_id, COUNT(o.customer_id) AS OrderCount
FROM Customer c
LEFT JOIN Orders o ON c.customer_id = o.customer_id
GROUP BY c.customer_id;
Demo with sample data. Here Customer Id 2 and 4 doesn't have any data in the Orders table and it result zero in the ouput.
DECLARE #Customer TABLE (CustomerId INT);
INSERT INTO #Customer (CustomerId) VALUES (1), (2), (3), (4), (5);
DECLARE #Orders TABLE (CustomerId INT, OrderId INT);
INSERT INTO #Orders (CustomerId, OrderId) VALUES (1, 1), (1, 2), (3, 2), (3, 4), (5, 1);
SELECT c.CustomerId, COUNT(o.CustomerId) AS OrderCount
FROM #Customer c
LEFT JOIN #Orders o ON c.CustomerId = o.CustomerId
GROUP BY c.CustomerId;
Output:
CustomerId OrderCount
----------------------
1 2
2 0
3 2
4 0
5 1
First aggregate the orders by customer ID and calculate the total count within an inner select. Make sure to left join this to your customers table so that you don't lose any of the customers that have not placed an order. Finally use a case statement to determine whether or not the return value for the number of orders for a customer is null meaning they have made no orders and in that case set the value to zero.
SELECT
c.customer_id,
CASE
WHEN o.num_orders IS NULL THEN 0
ELSE o.num_orders
END
FROM Customer c
LEFT JOIN (
SELECT customer_id, COUNT(*) AS num_orders
FROM Orders
GROUP BY customer_id
) AS o ON c.customer_id= o.customer_id;
Try the IFNULL function :
https://www.w3schools.com/sql/sql_isnull.asp
hope it'll help!

SQL query with JOIN and WHERE IN clause

UPDATE: Initially, I had the order date at line item table and realized that was a mistake and moved it to the Order table. Have updated my example query as well. Sorry
I am trying to write a query to load all orders whose line item order date is after a certain date along with loading all other orders which are out there for the same product returned by the first part of the query. Maybe an example could help
CREATE TABLE DemandOrder
(OrderId INT, OrderDate date, Customer VARCHAR(25))
CREATE TABLE LineItem
(OrderId INT, LineItemId INT, ProductId VARCHAR(10))
INSERT INTO DemandOrder VALUES(1, '01/23/2014', 'ABC');
INSERT INTO DemandOrder VALUES(2, '01/24/2014', 'DEF');
INSERT INTO DemandOrder VALUES(3, '01/24/2014', 'XYZ');
INSERT INTO DemandOrder VALUES(4, '01/23/2014', 'ABC');
INSERT INTO LineItem VALUES(1, 1, 'A');
INSERT INTO LineItem VALUES(1, 2, 'C');
INSERT INTO LineItem VALUES(2, 1, 'B');
INSERT INTO LineItem VALUES(3, 1, 'A');
INSERT INTO LineItem VALUES(4, 1, 'C');
In the above example, I need to query for all orders where the order date is on or after 01/24 along with all other orders which may have the returned by the first part of the query. The result should have orders 1, 2 & 3
Here is the updated sql code (using ErikE's suggestions from a post below)
SELECT
DISTINCT O.*
FROM
dbo.[DemandOrder] O
INNER JOIN dbo.LineItem LI
ON O.OrderID = LI.OrderID
WHERE
EXISTS (
SELECT *
FROM
dbo.DemandOrder O2 INNER JOIN
dbo.LineItem L2 ON O2.OrderId = L2.OrderId
WHERE
O2.OrderDate >= '01/24/2014'
AND LI.ProductID = L2.ProductID -- not clear if correct
);
Thanks for your help and suggestions
You can also do this with window functions:
select o.*
from (Select o.*,
max(li.OrderDate) over (partition by li.product) as maxOrderDate
from Order o INNER JOIN
LineItem li
ON o.OrderId = li.OrderId
) o
where o.maxOrderDate >= '2014-01-24';
You might actually want select distinct in the outer query, to prevent duplicates if one order has multiple products shipped after the given date.
As for your query, you can simplify it. The order table is not needed:
SELECT o.*
FROM Order o INNER JOIN
LineItem li
ON o.OrderId = li.OrderId
WHERE li.Product IN (SELECT li.Product
FROM LineItem li and li.OrderDate >= '2014-01-24'
);
You can also do this with window functions:
select o.*
from (Select o.*,
max(li.OrderDate) over (partition by li.product) as maxProductOrderDate
from Order o INNER JOIN
LineItem li
ON o.OrderId = li.OrderId
) o
where o.maxProductOrderDate >= '2014-01-24';
You might actually want select distinct in the outer query, to prevent duplicates if one order has multiple products shipped after the given date.
As for your query, you can simplify it because you do not need the order table in the subquery, unless you need it for filtering purposes:
SELECT o.*
FROM Order o INNER JOIN
LineItem li
ON o.OrderId = li.OrderId
WHERE li.Product IN (SELECT li.Product
FROM LineItem li
WHERE li.OrderDate >= '2014-01-24'
);
You probably want select distinct o.* in the outer query, to avoid duplicates when an order has two or more products that match the condition.
To get a result set with 1 row per order (meaning you're not interesting in line item data, just the order summary), something like this should do:
select o.*
from ( select distinct OrderId
from dbo.LineItem t1
where exists ( select *
from dbo.LineItem t2
where t2.Product = t1.Product
and t2.OrderDate >= #SomeLowerBoundDateTimeValue
)
) t
join dbo.Order o on o.OrderId = t.OrderId
The first item in the from clause is a derived table consisting of the set of order ids associated with a product that was part of an order dated on or after the specified date. Having done that, the rest is trival: just join against the order table.
Generally, for performance, you want to use correlated subqueries with [not] exists (...) in preference to uncorrelated subqueries with [not] in (...).
exists short circuits as soon as possible; in does not as it must construct the entire result set of the subquery.
I believe this is going to be close to what you're looking for.
All orders that have at least one productID that matches any product ID in an order 1/24/2014 or later.
SELECT
O.*
FROM
dbo.[Order] O
INNER JOIN dbo.LineItem LI
ON O.OrderID = LI.OrderID
WHERE
EXISTS (
SELECT *
FROM
dbo.LineItem L2
INNER JOIN dbo.LineItem L3
ON L2.ProductID = L3.ProductID
INNER JOIN dbo.[Order] O2
ON L3.OrderID = O2.OrderID
WHERE
O2.OrderDate >= '20140124'
AND O.OrderID = L2.OrderID
)
;
first i guess that your result should be OrderId: 2 and 3 because OrderDate is 01/24...
If you want to get that result you could try to do this.
Select o1.OrderId,o1.CustomerName,l1.OrderDate,l1.ProductId
from Order o1 INNER JOIN
LineItem l1
ON o1.OrderId = l1.OrderId
where l1.OrderDate >= '01/242014'
Hope this works and solve your question.
Regards!!!
This is what you're looking for, I believe.
Here's what's happening:
JOIN LineItem liBase: grab the initial records from LineItem based on the MinDate specification
JOIN LineItem liMatches: Self JOIN to to the LineItem table using the ProductIDs collected in the initial JOIN
JOIN LineItem projection: Using the OrderIDs collected from in the previous JOIN, grab the records from the LineItem table (in an additional self JOIN)
SELECT projection.*: projection is the set of results that we are after. SELECT them
Here's the query:
;WITH parms (
MinDate
) AS (
SELECT CONVERT(DATETIME, '01/24/2014')
)
SELECT projection.*
FROM parms p
JOIN LineItem liBase
ON liBase.OrderDate >= p.MinDate
JOIN LineItem liMatches
ON liMatches.ProductId = liBase.ProductId
JOIN LineItem projection
ON projection.OrderId = liMatches.OrderId
ORDER BY projection.OrderId
;
Same query, but with data generation (generates the LineItem and Order data sets that you presented in your question).
;WITH parms (
MinDate
) AS (
SELECT CONVERT(DATETIME, '01/24/2014')
)
, LineItem (
OrderId
, LineItemID
, OrderDate
, ProductId
) AS (
SELECT 1, 1, CONVERT(DATETIME, '01/23/2014'), 'B' UNION
SELECT 4, 1, CONVERT(DATETIME, '01/23/2014'), 'C' UNION
SELECT 2, 1, CONVERT(DATETIME, '01/24/2014'), 'A' UNION
SELECT 3, 1, CONVERT(DATETIME, '01/24/2014'), 'B'
)
, [Order] (
OrderId
, CustomerName
) AS (
SELECT 1, 'ABC' UNION
SELECT 2, 'XYZ' UNION
SELECT 3, 'DEF'
)
SELECT projection.*
FROM parms p
JOIN LineItem liBase
ON liBase.OrderDate >= p.MinDate
JOIN LineItem liMatches
ON liMatches.ProductId = liBase.ProductId
JOIN LineItem projection
ON projection.OrderId = liMatches.OrderId
ORDER BY projection.OrderId
;

How to group count and join in sequel?

I've looked through all the documentation and I'm having an issue putting together this query in Sequel.
select a.*, IFNULL(b.cnt, 0) as cnt FROM a LEFT OUTER JOIN (select a_id, count(*) as cnt from b group by a_id) as b ON b.a_id = a.id ORDER BY cnt
Think of table A as products and table B is a record indicated A was purchased.
So far I have:
A.left_outer_join(B.group_and_count(:a_id), a_id: :id).order(:count)
Essentially I just want to group and count table B, join it with A, but since B does not necessarily have any records for A and I'm ordering it by the number in B, I need to default a value.
DB[:a].
left_outer_join(DB[:b].group_and_count(:a_id).as(:b), :a_id=>:id).
order(:cnt).
select_all(:a).
select_more{IFNULL(:b__cnt, 0).as(:cnt)}
I can help you in MS SQL syntax.
Let's say your tables are Product and Order.
CREATE TABLE Product (
Id INT NOT NULL,
NAME VARCHAR(100) NOT NULL)
CREATE TABLE [Order] (
Id INT NOT NULL,
ProductId INT)
INSERT INTO Product (Id, Name) VALUES
(1, 'Tea'), (2, 'Coffee'), (3, 'Hot Chocolate')
INSERT INTO [Order] (Id, ProductId) VALUES
(1, 1), (2, 1), (3, 1), (4, 2)
This query will give the number of orders each product has, including ones without any orders.
SELECT p.Id AS ProductId,
p.Name AS ProductName,
COUNT(o.Id) AS Orders
FROM Product p
LEFT OUTER JOIN [Order] o
ON p.Id = o.ProductId
GROUP BY
p.Id,
p.Name
ORDER BY
COUNT(o.Id) DESC

How to get last children records with parent record from database

I have database with two tables:
Customers (Id PK, LastName)
and
Orders (Id PK, CustomerId FK, ProductName, Price, etc.)
I want to retrieve only customer' last orders details together with customer name.
I use .NET L2SQL but I think it's SQL question more than LINQ question so I post here SQL query I tried:
SELECT [t0].[LastName], (
SELECT [t2].[ProductName]
FROM (
SELECT TOP (1) [t1].[ProductName]
FROM [Orders] AS [t1]
WHERE [t1].[CustomerId] = [t0].[Id]
ORDER BY [t1].[Id] DESC
) AS [t2]
) AS [ProductName], (
SELECT [t4].[Price]
FROM (
SELECT TOP (1) [t3].[Price]
FROM [Orders] AS [t3]
WHERE [t3].[CustomerId] = [t0].[Id]
ORDER BY [t3].[Id] DESC
) AS [t4]
) AS [Price]
FROM [Customers] AS [t0]
Problem is that Orders has more columns (30) and with each column the query gets bigger and slower because I need to add next subqueries.
Is there any better way?
In SQL Server 2005 and above:
SELECT *
FROM (
SELECT o.*,
ROW_NUMBER() OVER (PARTITION BY c.id ORDER BY o.id DESC) rn
FROM customers c
LEFT JOIN
orders o
ON o.customerId = c.id
) q
WHERE rn = 1
or this:
SELECT *
FROM customers c
OUTER APPLY
(
SELECT TOP 1 *
FROM orders o
WHERE o.customerId = c.id
ORDER BY
o.id DESC
) o
In SQL Server 2000:
SELECT *
FROM customers с
LEFT JOIN
orders o
ON o.id =
(
SELECT TOP 1 id
FROM orders oi
WHERE oi.customerId = c.id
ORDER BY
oi.id DESC
)

SQL - identifying rows for a value in one table, where all joined rows only has a specific value

IN SQL Server, I have a result set from a joined many:many relationship.
Considering Products linked to Orders via a link table ,
Table - Products
ID
ProductName
Table - Orders
ID
OrderCountry
LinkTable OrderLines (columns not shown)
I'd like to be able to filter these results to show only the results where for an entity from one table, all the values in the other table only have a given value in a particular column. In terms of my example, for each product, I want to return only the joined rows when all the orders they're linked to are for country 'uk'
So if my linked result set is
productid, product, orderid, ordercountry
1, Chocolate, 1, uk
2, Banana, 2, uk
2, Banana, 3, usa
3, Strawberry, 4, usa
I want to filter so that only those products that have only been ordered in the UK are shown (i.e. Chocolate). I'm sure this should be straight-forward, but its Friday afternoon and the SQL part of my brain has given up for the day...
You could do something like this, where first you get all products only sold in one country, then you proceed to get all orders for those products
with distinctProducts as
(
select LinkTable.ProductID
from Orders
inner join LinkTable on LinkTable.OrderID = Orders.ID
group by LinkTable.ProductID
having count(distinct Orders.OrderCountry) = 1
)
select pr.ID as ProductID
,pr.ProductName
,o.ID as OrderID
,o.OrderCountry
from Products pr
inner join LinkTable lt on lt.ProductID = pr.ID
inner join Orders o on o.ID = lt.OrderID
inner join distinctProducts dp on dp.ProductID = pr.ID
where o.OrderCountry = 'UK'
In the hope that some of this may be generally reusable:
;with startingRS (productid, product, orderid, ordercountry) as (
select 1, 'Chocolate', 1, 'uk' union all
select 2, 'Banana', 2, 'uk' union all
select 2, 'Banana', 3, 'usa' union all
select 3, 'Strawberry', 4, 'usa'
), countryRankings as (
select productid,product,orderid,ordercountry,
RANK() over (PARTITION by productid ORDER by ordercountry) as FirstCountry,
RANK() over (PARTITION by productid ORDER by ordercountry desc) as LastCountry
from
startingRS
), singleCountry as (
select productid,product,orderid,ordercountry
from countryRankings
where FirstCountry = 1 and LastCountry = 1
)
select * from singleCountry where ordercountry='uk'
In the startingRS, you put whatever query you currently have to generate the intermediate results you've shown. The countryRankings CTE adds two new columns, that ranks the countries within each productid.
The singleCountry CTE reduces the result set back down to those results where country ranks as both the first and last country within the productid (i.e. there's only a single country for this productid). Finally, we query for those results which are just from the uk.
If you want, for example, all productid rows with a single country of origin, you just skip this last where clause (and you'd get 3,strawberry,4,usa in your results also)
So is you've got a current query that looks like:
select p.productid,p.product,o.orderid,o.ordercountry
from product p inner join order o on p.productid = o.productid --(or however these joins work for your tables)
Then you'd rewrite the first CTE as:
;with startingRS (productid, product, orderid, ordercountry) as (
select p.productid,p.product,o.orderid,o.ordercountry
from product p inner join order o on p.productid = o.productid
), /* rest of query */
Hmm. Based on Philip's earlier approach, try adding something like this to exclude rows where there's been the same product ordered in another country:
SELECT pr.Id, pr.ProductName, od.Id, od.OrderCountry
from Products pr
inner join LinkTable lt
on lt.ProductId = pr.ID
inner join Orders od
on od.ID = lt.OrderId
where
od.OrderCountry = 'UK'
AND NOT EXISTS
(
SELECT
*
FROM
Products MatchingProducts
inner join LinkTable lt
on lt.ProductId = MatchingProducts.ID
inner join Orders OrdersFromOtherCountries
on OrdersFromOtherCountries.ID = lt.OrderId
WHERE
MatchingProducts.ID = Pr.ID AND
OrdersFromOtherCountries.OrderCountry != od.OrderCountry
)
;WITH mytable (productid,ordercountry)
AS
(SELECT productid, ordercountry
FROM Orders od INNER JOIN LinkTable lt ON od.orderid = lt.OrderId)
SELECT * FROM mytable
INNER JOIN dbo.Products pr ON pr.productid = mytable.productid
WHERE pr.productid NOT IN (SELECT productid FROM mytable
GROUP BY productid
HAVING COUNT(ordercountry) > 1)
AND ordercountry = 'uk'
SELECT pr.Id, pr.ProductName, od.Id, od.OrderCountry
from Products pr
inner join LinkTable lt
on lt.ProductId = pr.ID
inner join Orders od
on od.ID = lt.OrderId
where od.OrderCountry = 'UK'
This probably isn't the most efficient way to do this, but ...
SELECT p.ProductName
FROM Product p
WHERE p.ProductId IN
(
SELECT DISTINCT ol.ProductId
FROM OrderLines ol
INNER JOIN [Order] o
ON ol.OrderId = o.OrderId
WHERE o.OrderCountry = 'uk'
)
AND p.ProductId NOT IN
(
SELECT DISTINCT ol.ProductId
FROM OrderLines ol
INNER JOIN [Order] o
ON ol.OrderId = o.OrderId
WHERE o.OrderCountry != 'uk'
)
TestData
create table product
(
ProductId int,
ProductName nvarchar(50)
)
go
create table [order]
(
OrderId int,
OrderCountry nvarchar(50)
)
go
create table OrderLines
(
OrderId int,
ProductId int
)
go
insert into Product VALUES (1, 'Chocolate')
insert into Product VALUES (2, 'Banana')
insert into Product VALUES (3, 'Strawberry')
insert into [order] values (1, 'uk')
insert into [order] values (2, 'uk')
insert into [order] values (3, 'usa')
insert into [order] values (4, 'usa')
insert into [orderlines] values (1, 1)
insert into [orderlines] values (2, 2)
insert into [orderlines] values (3, 2)
insert into [orderlines] values (4, 3)
insert into [orderlines] values (3, 2)
insert into [orderlines] values (3, 3)