Ok this is a tricky one (for me at least) lets say I have the following tables--
TABLES
ORDERS
create table orders (
ono number(5) not null primary key,
cno number(5) references customers,
eno number(4) references employees,
received date,
shipped date);
ODETAILS
create table odetails (
ono number(5) not null references orders,
pno number(5) not null references parts,
qty integer check(qty > 0),
primary key (ono,pno));
PARTS
create table parts(
pno number(5) not null primary key,
pname varchar2(30),
qoh integer check(qoh >= 0),
price number(6,2) check(price >= 0.0),
olevel integer);
TABLES DATA
insert into orders values
(1020,1111,1000,'10-DEC-11','12-DEC-11');
insert into orders values
(1021,1111,1000,'12-JAN-12','15-JAN-12');
insert into orders values
(1022,2222,1001,'13-FEB-12','20-FEB-12');
insert into orders values
(1023,3333,1000,'12-MAR-12',null);
insert into odetails values
(1020,10506,1);
insert into odetails values
(1020,10507,1);
insert into odetails values
(1020,10508,2);
insert into odetails values
(1020,10509,3);
insert into odetails values
(1021,10601,4);
insert into odetails values
(1022,10601,1);
insert into odetails values
(1022,10701,1);
insert into odetails values
(1023,10800,1);
insert into odetails values
(1023,10900,1);
insert into parts values
(10506,'Land Before Time I',200,19.99,20);
insert into parts values
(10507,'Land Before Time II',156,19.99,20);
insert into parts values
(10508,'Land Before Time III',190,19.99,20);
insert into parts values
(10509,'Land Before Time IV',60,19.99,20);
insert into parts values
(10601,'Sleeping Beauty',300,24.99,20);
insert into parts values
(10701,'When Harry Met Sally',120,19.99,30);
insert into parts values
(10800,'Dirty Harry',140,14.99,30);
insert into parts values
(10900,'Dr. Zhivago',100,24.99,30);
Now I'm required to create a procedure which takes in a value as month and generates a report which contains the following--
a. Number of sales
b. Sales value
c. Most popular item
d. Least popular item
My approach
CREATE OR REPLACE PROCEDURE TEST_REPORT
(MONTH_NUMBER IN NUMBER )
AS
PARTS_NUMBER VARCHAR2(10);
SHIPPING_STATUS VARCHAR2(10);
V_ENO VARCHAR2(5);
V_PNO VARCHAR2(5);
SALES NUMBER(30);
V_MONTH VARCHAR2(10);
BEGIN
SELECT RECEIVED INTO V_MONTH FROM ORDERS WHERE
WHERE EXTRACT(MONTH FROM ORDERS.RECEIVED) = MONTH_NUMBER;
SELECT SUM(PRICE*QTY)
INTO SALES
FROM ORDERS,ODETAILS,PARTS
WHERE
EXTRACT(MONTH FROM ORDERS.RECEIVED) = MONTH_NUMBER
END TEST_REPORT;
Then bang my head explodes. I was thinking of using cursors.. but then I thought a SELECT query with different column alias would be simpler. But as it seems I cant get the syntax right and currently If I execute this procedure it gives me a an error
ORA-01422: exact fetch returns more than requested number of rows
As there is more than one record in the table. What approach would be right and logical? and also did I get the syntax right for the procedures? I would appreciate the help and guidance.
Many thanks
(P.S. I condemn the person's naming convention in the script which I had to run to create the tables)
Well, let's see:
Number of sales (I'm assuming "of each part"):
SELECT od.PNO, COUNT(*) AS SALE_COUNT
FROM ODETAILS od
INNER JOIN ORDERS o
ON od.ONO = o.ONO
WHERE EXTRACT(MONTH FROM o.RECEIVED) = &MONTH_NUMBER
GROUP BY od.PNO
Sales value:
SELECT od.PNO, p.PRICE, SUM(od.QTY) AS SALES_QTY, SUM(od.QTY * p.PRICE) AS SALES_VALUE
FROM ODETAILS od
INNER JOIN PARTS p
ON p.PNO = od.PNO
GROUP BY od.PNO, p.PRICE
Most popular item (just take the first row):
SELECT od.PNO, SUM(od.QTY) AS TOTAL_QTY
FROM ODETAILS od
GROUP BY od.PNO
ORDER BY SUM(od.QTY) DESC
Least popular item (which has any sales) (just take the first row):
SELECT od.PNO, SUM(od.QTY) AS TOTAL_QTY
FROM ODETAILS od
GROUP BY od.PNO
ORDER BY SUM(od.QTY) ASC
Number of order cancellations and value, number of returns and values, item that has highest return: there does not appear to be data in the question (at this point) which would supply this information. I'll add a comment to the question to this effect. If further information or explanations are forthcoming I'll include this in my answer.
Best of luck.
Both these queries have issues.
Below query will return more than one record from ORDERS table. INTO clause will work only when the query returns exactly one record at a time.
SELECT RECEIVED INTO V_MONTH FROM ORDERS WHERE
WHERE EXTRACT(MONTH FROM ORDERS.RECEIVED) = MONTH_NUMBER;
This query have 2 issues, no join between ORDERS, ODETAILS and PARTS as well as INTO clause. Here also it will return more than one record.
SELECT SUM(PRICE*QTY)
INTO SALES
FROM ORDERS,ODETAILS,PARTS
WHERE
EXTRACT(MONTH FROM ORDERS.RECEIVED) = MONTH_NUMBER
END TEST_REPORT;
You need to define your requirement to suggest approach for your application.
Related
I am working on my Project 'Supermarket Billing Management System' since I am a beginner I m facing a lot of issues while making project. Here I've already created a trigger and a procedure but I don't know how I can execute it, I've created a trigger for a total price of a single Product i.e ProdTotal = ProdPrice * ProdQuantity;.
That means whenever a user enters some data in the Products table, then this trigger must get executed, but I don't know how to execute it, similarly, I've created a procedure to calculate the total price of all the products purchased by a single customer. Just like when you go to the supermarket or any store then after purchasing the items, you get a bill and there is the final total amount. I m not even exactly sure whether my procedure code is right or wrong, though it was created successfully, I m not sure whether it will give me the exact output which I want, So if you can help me then, please let me know, I've researched a lot from different websites and also from many youtube videos but seriously I am not getting how to solve it, so please help me!
Code:
Products table
create table Products
( ProdId number primary key,
ProdNum number not null unique,
ProdName varchar2(15),
ProdPrice int,
ProdQuantity int,
ProdCustId int references Customers,
ProdOrdId int references Orders,
ProdStoreId int references Stores
);
Payments table
create table Payments
( PayId int primary key,
PayDate date,
ProdTotal int,
FinalTotal int,
PayOrdId int references orders,
PayProdId int references Products,
PayCustId int references Customers
);
Trigger code
create trigger PROD_TOTAL
AFTER INSERT ON Products
BEGIN
UPDATE Payments
SET ProdTotal = (SELECT Products.ProdPrice * Products.ProdQuantity FROM Products);
END;
/
Procedure code
create procedure FINAL_TOTAL(C IN NUMBER, T OUT NUMBER)
IS
BEGIN
UPDATE Payments
SET FinalTotal = FinalTotal + ProdTotal
WHERE PayCustId = C;
Commit;
SELECT FinalTotal into T FROM Payments WHERE PayCustId = C;
END;
/
Insert statement in Product table:
insert into Products values(1,1001,'Syrup',30,4,1,1,1);
Insert statements in Payments table:
insert into Payments(PayId, PayDate, PayOrdID, PayProdId, PayCustId)
values(1,date'2020-10-07',1,1,1);
Output:
select * from products;
PRODID PRODNUM PRODNAME PRODPRICE PRODQUANTITY PRODCUSTID
---------- ---------- --------------- ---------- ------------ ----------
PRODORDID PRODSTOREID
---------- -----------
1 1001 Syrup 30 4 1
1 1
select * from Payments;
PAYID PAYDATE PRODTOTAL FINALTOTAL PAYORDID PAYPRODID PAYCUSTID
---------- --------- ---------- ---------- ---------- ---------- ----------
1 07-OCT-20 1 1 1
Now here, as you can see PRODTOTAL and FINALTOTAL column is blank, I know why it is blank because I didn't enter any value. And the reason why I didn't enter any value in these two columns is that I want, the system should automatically calculate that calculation with the help of trigger and procedure and, I can't even remove trigger and procedure because it's mandatory in our project to use both of these concepts. So please help me!!!
As already proposed, first try to get the design right with respect to your requirements. You can implement many constraints just by designing correctly your database schema.
Stay away from triggers and PL/SQL for as long as possible. It will force you to better design in the end and will pay off.
Before using triggers for business logic, try to use views for things that can be selected. That's what the database is for.
When you are "done", test for the performance and if it's suboptimal, improve your schema. If nothing helps, start using triggers for business logic.
I've put together a sample with views I am talking about. I hope it can get you started.
create table Products (
ProdId number generated always as identity primary key
, ProdName varchar2(20) not null
);
create table Stores (
StoreId number generated always as identity primary key
, StoreName varchar2(20) not null
);
create table Customers (
CustomerId number generated always as identity primary key
, CustomerName varchar2(20) not null
);
create table Prices (
PriceId number generated always as identity primary key
, ProdId number not null
, Price number
, ValidFrom date default on null sysdate
, constraint fk_Prices_Product foreign key (ProdId) references Products (ProdId)
);
create unique index uniq_prices_product_price on Prices (ProdId, ValidFrom);
create table Orders (
OrderId number generated always as identity primary key
, CustomerId number not null
, StoreId number not null
, OrderedAt date default on null sysdate
, constraint fk_Orders_Customer foreign key (CustomerId) references Customers (CustomerId)
, constraint fk_Orders_Store foreign key (StoreId) references Stores (StoreId)
);
create table OrderLines (
OrderLineId number generated always as identity primary key
, OrderId number not null
, ProdId number not null
, ProdQuantity number not null
, constraint fk_OrderLines_Order foreign key (OrderId) references Orders (OrderId)
, constraint fk_OrderLines_Prod foreign key (ProdId) references Products (ProdId)
);
create table Payments (
PaymentId number generated always as identity primary key
, OrderId number not null
, PaidAt date default on null sysdate
, PaidAmount number not null
, constraint fk_Payments_Order foreign key (OrderId) references Orders (OrderId)
);
create view Prices_V as
select
p.*
, coalesce(
lead(p.ValidFrom) over (partition by p.ProdId order by p.ValidFrom)
, to_date('9999', 'YYYY')
) ValidTo
from Prices p;
create view Orders_V as
select
o.*
, (
select sum(ol.ProdQuantity * p.Price)
from OrderLines ol
join Prices_V p on (p.ProdId = ol.ProdId and o.OrderedAt between p.ValidFrom and p.ValidTo)
where o.OrderId = ol.OrderId
) Total
, (
select sum(PaidAmount)
from Payments p
where p.OrderId = o.OrderId
) TotalPaid
from Orders o;
insert into Products(ProdName)
select 'Prod A' from dual union all
select 'Prod B' from dual;
insert into Stores(StoreName) values ('Store A');
insert into Customers(CustomerName)
select 'Customer A' from dual union all
select 'Customer B' from dual;
insert into Prices(ProdId, Price, ValidFrom)
select 1, 10, sysdate - 10 from dual union all
select 1, 12, sysdate - 2 from dual union all
select 1, 14, sysdate + 3 from dual union all
select 2, 100, sysdate - 10 from dual union all
select 2, 90, sysdate - 2 from dual union all
select 2, null, sysdate + 5 from dual;
insert into Orders(CustomerId, StoreId, OrderedAt)
select 1 cid, 1 stoid, sysdate - 5 from dual union all
select 2, 1, sysdate - 5 from dual union all
select 2, 1, sysdate - 1 from dual;
insert into OrderLines(OrderId, ProdId, ProdQuantity)
select 1 ordid, 1 prodid, 3 prodquant from dual union all
select 1, 2, 2 from dual union all
select 2, 2, 10 from dual union all
select 3, 2, 10 from dual;
insert into Payments(OrderId, PaidAmount) values (2, 500);
select * from Prices_V order by ProdId, ValidFrom;
select * from OrderLines order by OrderId, ProdId;
select * from Orders_v order by OrderId;
Some of the ideas in there:
Prices are stored in separate table, reference the product and have validity so that product price can change over time. Price view have ValidTo column added so it's easier to work with
There is a unique index on Prices so that we cannot have 2 prices for the same product at the same time
You can have many items in order, so that's why there is Orders and OrderLines tables in 1-to-many relationship
In Order_V the total paid is shown (using a subquery on Payments) and the total order values is shown (using a subquery on OrderLines and Prices, date of order is used to get prices form the correct period)
Based on the schema you will se what things you can represent and which you cannot. It's your job to make it match your requirements :)
And now I've come to the point you say triggers and procedures are mandatory in your project. Hence I have a proposal:
Create a procedure that will allow users to create new price for a product. It should definitely check that the validity does not start in the past. Then implement another one that allows for changing the valid to date (also cannot end in the past). You can than revoke any insert/update privileges on Products table and force users to use your procedures that will contain this business logic.
Create a table PricesLog and trigger on Prices that will insert the PriceId, old.Price, new.Price, sysdate and User to the log on any inserts/updates to the prices table.
I am working on oracle database.
We load customer data in source table which eventually migrates to target table.
Every time customer data is loaded in source table it is having a unique batch_id.
If we want to update some field in customer table, then we again load the same customer in source table but this time with different batch_id.
Now I want to know batch_id of the customer just before the latest batch_id.
Batch_id we take is usually the current date.
Use ROW_NUMBER analytic function
your sample data
select * from tab
order by 1,2
CUSTOMER_ID BATCH_ID
----------- -------------------
1 09.12.2019 00:00:00
1 10.12.2019 00:00:00
2 10.12.2019 00:00:00
Row_number assihns sequence number starting from 1 for each customer order descending on BATCH_ID - you are interested on one before the latest, i.e. the rows with the number 2.
with cust as (
select
customer_id, batch_id,
row_number() over (partition by customer_id order by batch_id desc) rn
from tab)
select CUSTOMER_ID, BATCH_ID
from cust
where rn = 2;
CUSTOMER_ID BATCH_ID
----------- -------------------
1 09.12.2019 00:00:00
It seems that you're basically looking for the second biggest value in the SOURCE table.
In this example code the SOURCE_TABLE represents the table containing same CUSTOMER_NO with different BATCH_NO:
create table source_table (customer_no integer, batch_no date);
insert into source_table values ('1', SYSDATE-2);
insert into source_table values ('1', SYSDATE-1);
insert into source_table values ('1', SYSDATE);
SELECT batch_no
FROM (
SELECT batch_no, row_number() over (order by batch_no desc) as row_num
FROM source_table
) t
WHERE row_num = 2
Where row_num = 2 represents the second biggest value in the table.
The query returns SYSDATE-1.
I have written a standard SQL Select Query to select the zip code in which the largest number of sales were. I now need to convert it to an anonymous PL/SQL block, however I'm still very "green" with PL/SQL and really don't have much of an idea as to how to accomplish this. Also, I need to incorporate a LIMIT into the PL/SQL anonymous block that will only display the lowest numeric zip code in the event of a tie.
Here are the tables w/some data:
CREATE TABLE CUSTOMERS
(customerID INT PRIMARY KEY,
customerZip VARCHAR(15) NOT NULL);
CREATE TABLE SALES
(saleID INT PRIMARY KEY,
customerID INT,
CONSTRAINT SALES_FK1 FOREIGN KEY (customerID) REFERENCES CUSTOMERS(customerID));
INSERT INTO CUSTOMERS (customerID, customerZIP) VALUES (1, '20636');
INSERT INTO CUSTOMERS (customerID, customerZIP) VALUES (2, '20619');
INSERT INTO CUSTOMERS (customerID, customerZIP) VALUES (3, '20670');
INSERT INTO CUSTOMERS (customerID, customerZIP) VALUES (4, '20670');
INSERT INTO CUSTOMERS (customerID, customerZIP) VALUES (5, '20636');
INSERT INTO SALES (saleID, customerID) VALUES (1, 1);
INSERT INTO SALES (saleID, customerID) VALUES (2, 2);
INSERT INTO SALES (saleID, customerID) VALUES (3, 3);
INSERT INTO SALES (saleID, customerID) VALUES (4, 4);
INSERT INTO SALES (saleID, customerID) VALUES (5, 5);
And here's the SQL query I wrote:
SELECT C.customerZip, COUNT (*) AS "MOST_SALES_byZIP"
FROM SALES S
INNER JOIN CUSTOMERS C
ON S.customerID = C.customerID
GROUP BY C.customerZip
HAVING COUNT (*) >= ALL
(SELECT COUNT(*)
FROM SALES S
INNER JOIN CUSTOMERS C
ON S.customerID = C.customerID
GROUP BY C.customerZip)
ORDER BY C.customerZip;
Basically, I first need to know how to "convert" this into a PL/SQL anonymous block. Then, I need to know how I can limit the results to only show the lowest numeric zip code if there is a tie between two or more.
I have an SQL fiddle Schema built here, if it helps: http://sqlfiddle.com/#!4/ca18bf/2
Thank you!
80% of good PL/SQL programming is good SQL coding.
In your problem: first, in SQL, to select the lowest numeric zip code from among those tied for most sales, you can do a join followed by aggregation by zip code, as you did already - and then use the aggregate LAST function. Like so:
select min(customerzip) keep (dense_rank last order by count(*)) as selected_zip
from sales inner join customers using (customerid)
group by customerzip
;
SELECTED_ZIP
---------------
20636
Now it is easy to use this in an anonymous block (if you have to - for whatever reason). SET SERVEROUTPUT ON is not part of the PL/SQL code; it is a command to the interface program, to instruct it to display the content of the output buffer on screen.
set serveroutput on
declare
selected_zip integer;
begin
select min(customerzip) keep (dense_rank last order by count(*))
INTO selected_zip -- this is the PL/SQL part!
from sales inner join customers using (customerid)
group by customerzip
;
dbms_output.put_line('Selected zip is: ' || selected_zip);
end;
/
PL/SQL procedure successfully completed.
Selected zip is: 20636
Here's an option. Create a function since an anonymous block can only print to STDOUT, it can't return something into a variable
The having clause is remove and simply order by count,zip so that top count wins then top count + top zip based on the order. Added in fetch first 1 rows ONLY to only get the 1 row then returned it from the function.
SQL> CREATE OR REPLACE FUNCTION getlowest RETURN NUMBER AS
l_ret NUMBER;
BEGIN
FOR r IN (
SELECT
c.customerzip,
COUNT(*) AS "MOST_SALES_byZIP"
FROM
sales s
INNER JOIN customers c ON s.customerid = c.customerid
GROUP BY
c.customerzip
order by
COUNT(*), c.customerzip
fetch first 1 rows ONLY
) LOOP
l_ret := r.customerzip;
END LOOP;
RETURN l_ret;
END;
/
SQL> show errors;
SQL>
SQL> select getlowest from dual
2 /
20619
SQL>
If the aim is to return a result set, then the PL/SQL block to do that would be
-- [Declare the host ref cursor according to the calling tool/language]
-- e.g. in SQL*Plus
var resultset refcursor
begin
open :resultset for
select c.customerzip, count(*) as most_sales_byzip
from sales s
join customers c on s.customerid = c.customerid
group by c.customerzip
having count(*) >= all
( select count(*) from sales s
join customers c on s.customerid = c.customerid
group by c.customerzip )
order by c.customerzip;
end;
From Oracle 12.1 onwards you can use implicit result sets:
declare
rc sys_refcursor;
begin
open rc for
open :resultset for
select c.customerzip, count(*) as most_sales_byzip
from sales s
join customers c on s.customerid = c.customerid
group by c.customerzip
having count(*) >= all
( select count(*) from sales s
join customers c on s.customerid = c.customerid
group by c.customerzip )
order by c.customerzip;
dbms_sql.return_result(rc);
end;
However we already have SQL to do this so it seems a bit pointless.
I'm looking for a good SQL approach (Oracle database) to fulfill the next requirements:
Delete rows from Table A that are not present in Table B.
Both tables have identical structure
Some fields are nullable
Amount of columns and rows is huge (more 100k rows and 20-30 columns to compare)
Every single field of every single row needs to be compared from Table A against table B.
Such requirement is owing to a process that must run every day as changes will come from Table B.
In other words: Table A Minus Table B => Delete the records from the Table A
delete from Table A
where (field1, field2, field3) in
(select field1, field2, field3
from Table A
minus
select field1, field2, field3
from Table B);
It's very important to mention that a normal MINUS within DELETE clause fails as does not take the nulls on nullable fields into consideration (unknown result for oracle, then no match).
I also tried EXISTS with success, but I have to use NVL function to replace the nulls with dummy values, which I don't want it as I cannot guarantee that the value replaced in NVL will not come as a valid value in the field.
Does anybody know a way to accomplish such thing? Please remember performance and nullable fields as "a must".
Thanks ever
decode finds sameness (even if both values are null):
decode( field1, field2, 1, 0 ) = 1
To delete rows in table1 not found in table2:
delete table1 t
where t.rowid in (select t1.rowid
from table1 t1
left outer join table2 t2
on decode(t1.field1, t2.field1, 1, 0) = 1
and decode(t1.field2, t2.field2, 1, 0) = 1
and decode(t1.field3, t2.field3, 1, 0) = 1
/* ... */
where t2.rowid is null /* no matching row found */
)
to use existing indexes
...
left outer join table2 t2
on (t1.index_field1=t2.index_field1 or
t1.index_field1 is null and t2.index_field1 is null)
and ...
Use a left outer join and test for null in your where clause
delete a
from a
left outer join b on a.x = b.x
where b.x is null
Have you considered ORALCE SQL MERGE statement?
Use Bulk operation for huge number of records. Performance wise it will be faster.
And use join between two table to get rows to be delete. Nullable columns can be compared with some default value.
Also, if you want Table A to be similar as Table B, why don't you truncate table A and then insert data from table b
Assuming you the same PK field available on each table...(Having a PK or some other unique key is critical for this.)
create table table_a (id number, name varchar2(25), dob date);
insert into table_a values (1, 'bob', to_date('01-01-1978','MM-DD-YYYY'));
insert into table_a values (2, 'steve', null);
insert into table_a values (3, 'joe', to_date('05-22-1989','MM-DD-YYYY'));
insert into table_a values (4, null, null);
insert into table_a values (5, 'susan', to_date('08-08-2005','MM-DD-YYYY'));
insert into table_a values (6, 'juan', to_date('11-17-2001', 'MM-DD-YYYY'));
create table table_b (id number, name varchar2(25), dob date);
insert into table_b values (1, 'bob', to_date('01-01-1978','MM-DD-YYYY'));
insert into table_b values (2, 'steve',to_date('10-14-1992','MM-DD-YYYY'));
insert into table_b values (3, null, to_date('05-22-1989','MM-DD-YYYY'));
insert into table_b values (4, 'mary', to_date('12-08-2012','MM-DD-YYYY'));
insert into table_b values (5, null, null);
commit;
-- confirm minus is working
select id, name, dob
from table_a
minus
select id, name, dob
from table_b;
-- from the minus, re-query to just get the key, then delete by key
delete table_a where id in (
select id from (
select id, name, dob
from table_a
minus
select id, name, dob
from table_b)
);
commit;
select * from table_a;
But, if at some point in time, tableA is to be reset to the same as tableB, why not, as another answer suggested, truncate tableA and select all from tableB.
100K is not huge. I can do ~100K truncate and insert on my laptop instance in less than 1 second.
> DELETE FROM purchase WHERE clientcode NOT IN (
> SELECT clientcode FROM client );
This deletes the rows from the purchase table whose clientcode are not in the client table. The clientcode of purchase table references the clientcode of client table.
DELETE FROM TABLE1 WHERE FIELD1 NOT IN (SELECT CLIENT1 FROM TABLE2);
I have the following query and I need it to return all the null values between those two dates.
select cust_first_name
from customers
join orders using(customer_id)
where order_date between (to_date('01-01-2007','DD-MM-YYYY'))
and (to_date('31-12-2008','DD-MM-YYYY'));
Sounds like what you want is customers with no orders within the given date range. The join you are using finds the opposite of that.
You could do this with an outer join, in which case you need to apply the date filter prior to the join. It's probably easier and more readable to use a NOT IN or NOT EXISTS subquery:
select cust_first_name
from customers
WHERE customers.customer_id NOT IN (
SELECT orders.customer_id from orders
where order_date between (to_date('01-01-2007','DD-MM-YYYY'))
and (to_date('31-12-2008','DD-MM-YYYY'))
)
Here is an example of how to do what you want.
The key part is doing a left join on your orders table, and then simply doing a not between date1 and date2
declare #customers table (
id int identity(1,1),
first_name nvarchar(50),
last_name nvarchar(50)
)
declare #orders table (
id int identity(1,1),
customer_id int,
order_date datetime
)
insert into #customers(first_name, last_name) values ('bob', 'gates')
insert into #customers(first_name, last_name) values ('cyril', 'smith')
insert into #customers(first_name, last_name) values ('harry', 'potter')
insert into #orders(customer_id, order_date) values (1, '2007-02-01')
insert into #orders(customer_id, order_date) values (2, '2015-02-15')
insert into #orders(customer_id, order_date) values (3, '2008-02-15')
select
customers.id
,customers.first_name
,customers.last_name
from #customers customers
left join #orders orders on orders.customer_id = customers.id
where orders.id is null
or orders.order_date not between ('2007-01-01') and ('2008-12-31')
group by
customers.id
,customers.first_name
,customers.last_name;