List Hours worked with multiple shifts and across dates - tsql

I have a few questions on how to handle a few employee timekeeping queries in SQL 2012… I had asked another quesiton last week about determining the time between shifts, which I as able to get a great response to.
We are fed the data from our Point Of Sale Software Provider, so we cannot change the format of the data.
Work days are based on DateOfBusiness which are from 5:00 am until 4:59am the next calendar day, so it crosses midnight. Locations End Of Day process is offset a little to control balance loads, some are 5:05am instead of 5:00am. When the End Of Day process runs, it clocks everyone out then clocks them back in when the process completes.
- Employees clock out for the 30 minute required meal breaks, so there are 2 or more records for one day for these employees. Not sure why, but several employees have more than 3 records in one day (EmployeeShiftNumber).
- Shifts occasionally cross DateOfBusiness. In at 11:00PM - Out at 7:00am
I need to report list of initial InTime and Final OutTime and the number of minutes worked. I have will have to compare these against the table which holds the employee schedules. This reporting is not for Payroll purposes, but for comparisons against scheduled shifts.
I have included some sample data grouped by employee and by day and the values I would expect to see commented to the right. I also need to see the number of minutes worked in the shift.
--drop table #Shift
CREATE TABLE #Shift(
FKEmployeeNumber int,
DateOfBusiness datetime,
FKStoreId int,
EmployeeShiftNumber int,
FKJobCodeId int,
InHour int,
InMinute int,
OutHour int,
OutMinute int)
insert into #Shift ( FKEmployeeNumber, DateOfBusiness, FKStoreId, EmployeeShiftNumber, FKJobCodeId, InHour, InMinute,OutHour,OutMinute)
values
(23761, '11/30/2017', 3013, 0, 1, 17, 39, 21, 30),
(23761, '11/30/2017', 3013, 1, 1, 21, 30, 2, 39), -- 5:39PM 2:39AM
(23770, '11/30/2017', 3013, 0, 200, 7, 19, 16, 25), -- 7:19AM 4:25PM
(23938, '11/30/2017', 3013, 0, 1, 16, 4, 1, 26), -- 4:04AM 1:26AM
(24006, '11/30/2017', 3013, 0, 1, 7, 30, 18, 36),
(24006, '11/30/2017', 3013, 1, 1, 18, 36, 18, 40), -- 7:30AM 6:40PM
(24018, '11/30/2017', 3013, 0, 2, 8, 52, 17, 0), -- 8:52M 4:00PM
(25176, '11/30/2017', 3013, 0, 200, 15, 59, 20, 1), -- 3:59PM 8:01PM
(25176, '11/30/2017', 3013, 1, 200, 20, 30, 0, 05), -- 8:30PM 12:05AM
(25180, '11/30/2017', 3013, 0, 1, 21, 0, 5, 0), -- 9:00PM 5:00AM
(25187, '11/30/2017', 3013, 0, 1, 10, 0, 16, 6), -- 10:00AM 4:06PM
(35189, '11/30/2017', 3013, 0, 1, 16, 58, 2, 4), -- 4:58PM 2:04AM
(25147, '12/04/2017', 3106, 0, 1, 6, 58, 15, 2),
(25147, '12/04/2017', 3106, 1, 1, 15, 3, 15, 3), -- 6:58AM 3:03PM
(26291, '12/01/2017', 3118, 1, 200, 23, 15, 5, 5),
(26291, '12/02/2017', 3118, 0, 200, 5, 6, 7, 22), -- 11:15PM 7:22AM
(26291, '12/03/2017', 3118, 0, 200, 7, 30, 15, 38), -- 7:30AM 3:38PM
(26291, '12/04/2017', 3118, 0, 200, 23, 15, 5, 5),
(26291, '12/05/2017', 3118, 0, 200, 5, 6, 7, 12), -- 11:15PM 7:12AM
(26291, '12/05/2017', 3118, 1, 200, 23, 15, 5, 5),
(26291, '12/06/2017', 3118, 0, 200, 15, 14, 7, 5) -- 11:15PM 7:05AM
--Select * from #Shift
SELECT fkstoreid, FKEmployeeNumber AS EmpID, DateOfBusiness AS Date,
RIGHT(CONVERT(varchar(30), DATEADD(MINUTE, InTime_Mins, 0), 100), 7) AS InTime,
RIGHT(CONVERT(varchar(30), DATEADD(MINUTE, OutTime_Mins, 0), 100), 7) AS OutTime,
MinsWorked
FROM (
select sh.FKStoreId, sh.FKEmployeeNumber, sh.DateOfBusiness,
MIN(sh.InHour*60+InMinute) AS InTime_Mins,
MAX(sh.OutHour*60+OutMinute) AS OutTime_Mins,
SUM(((sh.outhour+case when sh.OutHour < sh.InHour then 24 else 0 end)*60 + sh.outminute) -
(sh.inhour*60 + sh.inminute)) AS MinsWorked
from #shift sh
group by sh.FKStoreId,sh.FKEmployeeNumber, sh.DateOfBusiness
) AS derived
order by FKEmployeeNumber, DateOfBusiness

I've written a sample of how you could do it.
Briefly summarized I perform these steps:
Mark records that should be grouped with either the next or the previous record, because they are a part of the same shift.
Find the start and end of each shift
Join all records for an employee that fall within each shift
Group the resultset per employee per shift, and calculate the minutes worked by summing up the minutes between checkin and checkout for each record within a shift.
Sample query:
WITH ShiftParts AS (
SELECT
FKEmployeeNumber,
DATEADD(MINUTE, InMinute, DATEADD(HOUR, InHour, DateOfBusiness)) CheckIn, --Datetime of the checkin
DATEADD(DAY, CASE WHEN OutHour < InHour THEN 1 ELSE 0 END,
DATEADD(MINUTE, OutMinute, DATEADD(HOUR, OutHour, DateOfBusiness))) CheckOut --Datetime of the checkout (add one day if we crossed midnight).
FROM #Shift
),
GroupInfo AS (
SELECT
*,
CASE
WHEN DATEDIFF(MINUTE, LAG(CheckOut, 1, NULL) OVER (PARTITION BY FKEmployeeNumber ORDER BY CheckOut), CheckIn) <= 120
THEN 1 ELSE 0 END AS GroupWithPrevious, --Determine whether we want to group this record with the previous
CASE
WHEN DATEDIFF(MINUTE, CheckOut, LEAD(CheckIn, 1, NULL) OVER (PARTITION BY FKEmployeeNumber ORDER BY CheckOut)) <= 120
THEN 1 ELSE 0 END AS GroupWithNext --Determine whether we want to group this record with the next
FROM ShiftParts
), ShiftStartAndEnd AS (
SELECT
*,
CASE WHEN GroupWithNext = 1 THEN LEAD(CheckOut, 1, NULL) OVER (PARTITION BY FKEmployeeNumber ORDER BY CheckOut) ELSE CheckOut END AS FinalCheckOut
FROM GroupInfo
WHERE GroupWithPrevious = 0 OR GroupWithNext = 0 --Only pick beginning and end of a shift
)
SELECT
sse.FKEmployeeNumber,
sse.CheckIn,
sse.FinalCheckOut,
SUM(DATEDIFF(MINUTE,sp.CheckIn,sp.CheckOut)) AS MinutesWorked
FROM ShiftStartAndEnd sse
LEFT OUTER JOIN ShiftParts sp ON sp.FKEmployeeNumber = sse.FKEmployeeNumber AND sp.CheckIn >= sse.CheckIn AND sp.CheckOut <= sse.FinalCheckOut
WHERE sse.GroupWithPrevious = 0
GROUP BY sse.FKEmployeeNumber, sse.CheckIn, sse.FinalCheckOut
Note: I think the last two records in your sample data should not be grouped
(26291, '12/05/2017', 3118, 1, 200, 23, 15, 5, 5),
(26291, '12/06/2017', 3118, 0, 200, 15, 14, 7, 5) -- 11:15PM 7:05AM
should be:
(26291, '12/05/2017', 3118, 1, 200, 23, 15, 5, 5), -- 11:19PM 5:05AM
(26291, '12/06/2017', 3118, 0, 200, 15, 14, 7, 5) -- 3:14PM 7:05AM

Related

want to filter several data in 3 month periode mysql

So in this case I want filter buyer who only appear in (jan, feb, mar) in2019 and appear in (apr, may, june) on 2017, and here's my syntax
SELECT DISTINCT
d1.buyer_id,
d1.tgl
FROM data_2019 d1
INNER JOIN data_2017 d2
ON d1.buyer_id = d2.buyer_id
INNER JOIN data_2017 d3
ON d1.buyer_id = d3.buyer_id
INNER JOIN data_2018 d4
ON d1.buyer_id = d4.buyer_id
INNER JOIN data_2019 d5
ON d1.buyer_id = d5.buyer_id
WHERE
MONTH(d1.tgl) IN (7, 8, 9) AND
MONTH(d2.tgl) IN (4, 5, 6) AND
MONTH(d3.tgl) NOT IN (1, 2, 3, 7, 8, 9, 10, 11, 12) AND
MONTH(d4.tgl) NOT IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12) AND
MONTH(d5.tgl) NOT IN (1, 2, 3, 4, 5, 6, 10, 11, 12) group by buyer_id;
but that was wrong, do you know where it is wrong?

Finding consecutive records

I have followed code which is supposed to give me consecutive records using PlanId and costcentreid. for example in the image the record number 7 has costcentreid of 14 and the costcentreid before that is 5, so then it will ignore all the records before that and return me StartDate of 2017-07-12. if the previous record was of the same cost centre, then it would keep going back till the cost centre is different and then return me the lowest date but it's not doing that. i have provided my sql. Could you please help?
Sample Data:
Scenario 1: Correct Answer should be '2017-07-12 11:56:52.560'
DECLARE #T TABLE (StartDate, PlanId, CostCentreId, PositionId, CostCentreFlavourID, CustomerPositionID)
INSERT #T(StartDate, PLanId, CostCentreID, PositionId, CostCentreFlavourID, CustomerPOsitionID)
VALUES('1998-10-23 00:00:00.000', 19130, 14, 129, 3, 1, 766 ),
('2010-06-22 00:00:00.000', 19130, 207, 25, 3, 1,16247),
('2012-05-01 16:27:04.460', 19130, 42, 14, 3, 1,23946),
('2013-04-30 18:57:57.617', 19130, 295, 14, 3, 1,29453),
('2015-03-03 09:31:28.133', 19130, 275, 5, 3,1, 39286),
('2015-06-26 15:48:35.637', 19130, 195, 5, 3,1,41985),
('2017-07-12 11:56:52.560', 19130, 1445, 14, 3, 1,57699)
Scenario 2: Correct Answer should be : '2015-06-26 15:48:35.637'
DECLARE #T TABLE (StartDate, PlanId, CostCentreId, PositionId, CostCentreFlavourID, CustomerPositionID)
INSERT #T(StartDate, PLanId, CostCentreID, PositionId, CostCentreFlavourID, CustomerPOsitionID)
VALUES('1998-10-23 00:00:00.000', 19130, 14, 129, 3,1,766 ),
('2010-06-22 00:00:00.000', 19130, 207, 25, 3, 1,16247),
('2012-05-01 16:27:04.460', 19130, 42, 14, 3, 1,23946),
('2013-04-30 18:57:57.617', 19130, 295, 14, 3, 1,29453),
('2015-03-03 09:31:28.133', 19130, 275, 5, 3, 1,39286),
('2015-06-26 15:48:35.637', 19130, 195, 14, 3,1, 41985),
('2017-07-12 11:56:52.560', 19130, 1445, 14, 3, 1,57699)
Scenario 3: Correct Answer should be: '2012-05-01 16:27:04.460'
DECLARE #T TABLE (StartDate, PlanId, CostCentreId, PositionId, CostCentreFlavourID, CustomerPositionID)
INSERT #T(StartDate, PLanId, CostCentreID, PositionId, CostCentreFlavourID, CustomerPOsitionID)
VALUES('1998-10-23 00:00:00.000', 19130, 14, 129, 3,1,766 ),
('2010-06-22 00:00:00.000', 19130, 207, 25, 3,1, 16247),
('2012-05-01 16:27:04.460', 19130, 42, 14, 3,1, 23946),
('2013-04-30 18:57:57.617', 19130, 295, 14, 3,1, 29453),
('2015-03-03 09:31:28.133', 19130, 275, 14, 3, 1,39286),
('2015-06-26 15:48:35.637', 19130, 195, 14, 3,1, 41985),
('2017-07-12 11:56:52.560', 19130, 1445, 14, 3,1, 57699)
WITH cte
AS (SELECT cp1.StartDate,
fp.PlanId,
p.CostCentreID,
p.PositionID,
fp.CostCentreFlavourId,
fp.CustomerWithNDISNumberOfPlans,
cp1.CustomerPositionID,
ROW_NUMBER() OVER (PARTITION BY cp1.CustomerID ORDER BY cp1.StartDate) AS CustomerRow,
ROW_NUMBER() OVER (PARTITION BY cp1.CustomerID, p.CostCentreID ORDER BY cp1.StartDate) AS CustomerCostCentreRow ,
ROW_NUMBER() OVER (PARTITION BY cp1.CustomerID ORDER BY cp1.StartDate)
- ROW_NUMBER() OVER (PARTITION BY cp1.CustomerID, p.CostCentreID ORDER BY cp1.StartDate) rn3
FROM #FlavouredPlans fp
INNER JOIN dbo.tblCustomerPositions cp1
ON cp1.CustomerID = fp.LADSCustomerID
INNER JOIN dbo.tblPositions p
ON p.PositionID = cp1.PositionID
--AND fp.CostCentreId = p.CostCentreID
WHERE fp.CostCentreFlavourId = 3
AND fp.OrderOfPlans = 1
),
ctePositionStartDate
AS (SELECT *,
MIN(cte.StartDate) OVER (PARTITION BY PlanId, CostCentreID, startdate, rn3) MinStartDate,
ROW_NUMBER() OVER (PARTITION BY PlanId ORDER BY cte.StartDate ASC) [Order]
FROM cte )
SELECT *
FROM ctePositionStartDate
WHERE ctePositionStartDate.planid = 19130
You can get your result by using window functions. Check this query
DECLARE #T TABLE (StartDate datetime, PlanId int, CostCentreId int, PositionId int, CostCentreFlavourID int, CustomerPositionID int)
INSERT #T(StartDate, PLanId, PositionId, CostCentreID, CostCentreFlavourID, CustomerPOsitionID)
VALUES
('19981023 00:00:00.000', 19130, 14, 129, 3,1),
('20100622 00:00:00.000', 19130, 207, 25, 3,1),
('20120501 16:27:04.460', 19130, 42, 14, 3,1),
('20130430 18:57:57.617', 19130, 295, 12, 3,1),
('20150303 09:31:28.133', 19130, 275, 14, 3, 1),
('20150626 15:48:35.637', 19130, 195, 14, 3,1),
('20170712 11:56:52.560', 19130, 1445, 14, 3,1)
select
*
from (
select
*, row_number() over (partition by PlanId, CostCentreID, rn1 - rn2 order by StartDate) r3
, max(StartDate) over (partition by PlanId, CostCentreID, rn1 - rn2) mx1
, max(StartDate) over (partition by PlanId) mx2
from (
select
*, row_number()over(partition by PlanId order by StartDate) rn1
, row_number()over(partition by PlanId, CostCentreID order by StartDate) rn2
from
#T
) t
) t
where
mx1 = mx2
and r3 = 1

Summing arrays in conjunction with GROUP BY

I've got some periodic counter data (like once a second) from different objects that I wish to combine into an hourly total.
If I do it with separate column names, it's pretty straightforward:
CREATE TABLE ts1 (
id INTEGER,
ts TIMESTAMP,
count0 integer,
count1 integer,
count2 integer
);
INSERT INTO ts1 VALUES
(1, '2017-12-07 10:37:48', 10, 20, 50),
(2, '2017-12-07 10:37:48', 13, 7, 88),
(1, '2017-12-07 10:37:49', 12, 23, 34),
(2, '2017-12-07 10:37:49', 11, 13, 46),
(1, '2017-12-07 10:37:50', 8, 33, 80),
(2, '2017-12-07 10:37:50', 9, 3, 47),
(1, '2017-12-07 10:37:51', 17, 99, 7),
(2, '2017-12-07 10:37:51', 9, 23, 96);
SELECT id, date_trunc('hour', ts + '1 hour') nts,
sum(count0), sum(count1), sum(count2)
FROM ts1 GROUP BY id, nts;
id | nts | sum | sum | sum
----+---------------------+-----+-----+-----
1 | 2017-12-07 11:00:00 | 47 | 175 | 171
2 | 2017-12-07 11:00:00 | 42 | 46 | 277
(2 rows)
The problem is that different objects have different numbers of counts (though each particular object's rows -- ones sharing the same ID -- all have the same number of counts). Hence I want to use an array.
The corresponding table looks like this:
CREATE TABLE ts2 (
id INTEGER,
ts TIMESTAMP,
counts INTEGER[]
);
INSERT INTO ts2 VALUES
(1, '2017-12-07 10:37:48', ARRAY[10, 20, 50]),
(2, '2017-12-07 10:37:48', ARRAY[13, 7, 88]),
(1, '2017-12-07 10:37:49', ARRAY[12, 23, 34]),
(2, '2017-12-07 10:37:49', ARRAY[11, 13, 46]),
(1, '2017-12-07 10:37:50', ARRAY[8, 33, 80]),
(2, '2017-12-07 10:37:50', ARRAY[9, 3, 47]),
(1, '2017-12-07 10:37:51', ARRAY[17, 99, 7]),
(2, '2017-12-07 10:37:51', ARRAY[9, 23, 96]);
I have looked at this answer https://stackoverflow.com/a/24997565/1076479 and I get the general gist of it, but I cannot figure out how to get the correct rows summed together when I try to combine it with the grouping by id and timestamp.
For example, with this I get all the rows, not just the ones with matching id and timestamp:
SELECT id, date_trunc('hour', ts + '1 hour') nts, ARRAY(
SELECT sum(elem) FROM ts2 t, unnest(t.counts)
WITH ORDINALITY x(elem, rn) GROUP BY rn ORDER BY rn
) FROM ts2 GROUP BY id, nts;
id | nts | array
----+---------------------+--------------
1 | 2017-12-07 11:00:00 | {89,221,448}
2 | 2017-12-07 11:00:00 | {89,221,448}
(2 rows)
FWIW, I'm using postgresql 9.6
The problem with you original query is that you're summing all elements, because GROUP BY id, nts is executed in outer query. Combining a CTE with LATERAL JOIN would do the trick:
WITH tmp AS (
SELECT
id,
date_trunc('hour', ts + '1 hour') nts,
sum(elem) AS counts
FROM
ts2
LEFT JOIN LATERAL unnest(counts) WITH ORDINALITY x(elem, rn) ON TRUE
GROUP BY
id, nts, rn
)
SELECT id, nts, array_agg(counts) FROM tmp GROUP BY id, nts

PostgreSQL, mixing SUM horizontaly and vertically

I have a temporary table which is result of previously heavy combined data from which I have to create html document to show.
This table in short illustrates situation:
DROP TABLE IF EXISTS temp11;
CREATE TABLE temp11 (t_idx int PRIMARY KEY, mydate text, myclass int, mypercent double precision, valpercent double precision, valclass double precision);
INSERT INTO temp11
(t_idx, mydate, myclass, mypercent, valpercent, valclass) VALUES
(1, '01.01.2014', 1, 10, 10, 1),
(2, '01.01.2014', 2, 20, 20, 4),
(3, '01.01.2014', 2, 20, 50, 10),
(4, '01.01.2014', 1, 10, 17, 1.7),
(5, '02.01.2014', 2, 20, 40, 8),
(6, '02.01.2014', 1, 10, 18, 1.8),
(7, '02.01.2014', 2, 20, 50, 10),
(8, '03.01.2014', 1, 10, 10, 1),
(9, '03.01.2014', 2, 20, 40, 8),
(10, '03.01.2014', 1, 10, 20, 2),
(11, '03.01.2014', 2, 20, 30, 6);
Now I have a query for grouping and summing that into dates and valclasses:
SELECT mydate, myclass, mypercent,
SUM(valpercent) AS sumvalpercent,
SUM(valclass) AS sumvalclass,
SUM(valpercent+valclass) AS sum_row
FROM temp11
GROUP BY mydate, myclass, mypercent
ORDER BY mydate;
Result of this query is expectable:
"01.01.2014" 2 20 70 14.0 84.0
"01.01.2014" 1 10 27 2.7 29.7
"02.01.2014" 1 10 18 1.8 19.8
"02.01.2014" 2 20 90 18.0 108.0
"03.01.2014" 2 20 70 14.0 84.0
"03.01.2014" 1 10 30 3.0 33.0
But needs are a bit extended.
Is it possible to do with PostgreSQL that in same process after every date I get vertically SUM of data inside that date and after all, at the end, SUM of data from all dates so result will look like this:
"01.01.2014" 2 20 70 14.0 84.0
"01.01.2014" 1 10 27 2.7 29.7
97 16.7 113.7
"02.01.2014" 1 10 18 1.8 19.8
"02.01.2014" 2 20 90 18.0 108.0
108 19.8 127.8
"03.01.2014" 2 20 70 14.0 84.0
"03.01.2014" 1 10 30 3.0 33.0
100 17.0 117.0
305 53.5 358.5
If this is possible such (or similar), how that query should look like with showed data?
The simplest way I can think of is to use UNION ALL to get all the desired output at once.
If you leave out the fact that the dates are shown (needed for the order by clause) this query gives the requested output in the simplest way.
SELECT mydate, myclass, mypercent,
SUM(valpercent) AS sumvalpercent,
SUM(valclass) AS sumvalclass,
SUM(valpercent+valclass) AS sum_row
FROM temp11
GROUP BY mydate, myclass, mypercent
UNION ALL
SELECT mydate || ' total', null, null,
SUM(valpercent) AS sumvalpercent,
SUM(valclass) AS sumvalclass,
SUM(valpercent+valclass) AS sum_row
FROM temp11
GROUP BY mydate
UNION ALL
SELECT 'Total', null, null,
SUM(valpercent) AS sumvalpercent,
SUM(valclass) AS sumvalclass,
SUM(valpercent+valclass) AS sum_row
FROM temp11
ORDER BY mydate;
Here's a fiddle
Perhaps it can be rewritten more elegantly using WITH
EDIT:
This will be more efficient because it only traverses through temp11 table just once. Then it only uses the temporary table temp100 which has much fewer rows for the additional totals (no more than one row per day). The UNIONs still remain and the logic is still the same.
WITH temp100 (mydate,myclass,mypercent, sumvalpercent,sumvalclass,sum_row) as (
SELECT mydate, myclass, mypercent,
SUM(valpercent) AS sumvalpercent,
SUM(valclass) AS sumvalclass,
SUM(valpercent+valclass) AS sum_row
FROM temp11
GROUP BY mydate, myclass, mypercent
)
SELECT mydate,myclass,mypercent, sumvalpercent,sumvalclass,sum_row
FROM temp100
UNION ALL
SELECT mydate || ' total' as mydate, null, null, SUM(sumvalpercent), SUM(sumvalclass), SUM(sum_row)
FROM temp100
GROUP BY mydate
UNION ALL
SELECT 'Total' as mydate, null, null, SUM(sumvalpercent), SUM(sumvalclass), SUM(sum_row)
FROM temp100
ORDER BY mydate;
This is the fiddle

How to get the data which are not soled from the given tables? (PostgreSQL)

i have products table as
product_id int type with primary key constraint
product name varchar with not null constraint
sample data like as below
1. 100, 'Nokia'
2. 200, 'IPhone'
3. 300, 'Samsung'
4. 400, 'LG'
i have the sales table as
SALE_ID with int type,
PRODUCT_ID with int type referece of product table (foregin key)
YEAR with int type
Quantity with int type
PRICE with int type check price > 0
sample data like as below
1, 100, 2010, 25, 5000
2, 100, 2011, 16, 5000
3, 100, 2012, 8, 5000
4, 200, 2010, 10, 9000
5, 200, 2011, 15, 9000
6, 200, 2012, 20, 9000
7, 300, 2010, 20, 7000
8, 300, 2011, 18, 7000
9, 300, 2012, 20, 7000
Query
How to find the products which are not soled?
#Praveen
Your solution is subtly incorrect, because there's no NOT NULL constraint on product_id in sales. It builds a list then filters on the list, but the list could contain NULL, and 2 NOT IN (1, NULL) is NULL, which in WHERE is treated as false.
It is much better to re-phrase this as
WHERE NOT EXISTS (SELECT 1 FROM sales s WHERE s.product_id = products.product_id)
The Query ::
select *
from products
WHERE NOT EXISTS (SELECT 1
FROM sales s WHERE s.product_id = products.product_id);
#igor-romanchenko
select *
from products
where product_id not in (select PRODUCT_ID
from sales
where PRODUCT_ID is not null);