0

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!

StevenC
  • 119
  • 14
  • What do you want the PL/SQL block to do? – William Robertson Feb 08 '18 at 14:38
  • Basically the same thing that the standard SQL query that I wrote does, but with added functionality to limit the results to the lowest numerical zip code in the event of a tie. I know the query above works fine, but I've been asked to accomplish this via PL/SQL and don't really know how. – StevenC Feb 08 '18 at 14:39
  • So you want it to return a ref cursor maybe? Also there won't be any tie because you are already grouping by zip code. You can limit the overall result set with a [`fetch first n`](https://oracle-base.com/articles/12c/row-limiting-clause-for-top-n-queries-12cr1) clause. – William Robertson Feb 08 '18 at 14:45
  • This is where I get lost, as I don't really know the syntax of PL/SQL too well yet. As it stands now, there is a tie w/the standard SQL statement (see SQL fiddle link for an example). Could you provide an example by chance of how you would accomplish this so I have an idea of what you're thoughts are? Sorry for my ignorance... – StevenC Feb 08 '18 at 15:00
  • 1
    Asked to accomplish via PL/SQL? Who asked you to do it via PL/SQL, and why? This is easy in plain SQL, and if you need the result in PL/SQL, it should still use the most efficient SQL query to get the value and pass it to PL/SQL; it is wrong, wrong, wrong to do in PS/SQL those parts of a task that properly belong in SQL. – mathguy Feb 08 '18 at 15:13
  • Your SQL fiddle returns two zip codes, 20636 and 20670, so I'm not sure what tie you mean. Also what tool/script/language will be calling the PL/SQL block? You don't use PL/SQL for reporting, it normally processes data, inserts, updates, deletes, solves sudokus or whatever, so I'm not sure what it means to convert a query into a PL/SQL block. – William Robertson Feb 08 '18 at 15:14
  • Yes - asked to accomplish via PL/SQL, specifically by my instructor. I know it is wrong and don't agree with doing it this way, but I guess its his best method of "teaching" how to use PL/SQL. @WilliamRobertson, yes - it returns two zip codes. By tie, I mean a tie for the sales count. It should only display the lowest numeric zip code. So for example, if zip code 11111 and 99999 had a total of 10 sales, only 11111 would be displayed. – StevenC Feb 08 '18 at 15:18

3 Answers3

1

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
mathguy
  • 37,873
  • 5
  • 22
  • 47
  • Thank you. I will work off of your feedback to accomplish what I'm after. I'm very thankful for the advice here - and very appreciative of your help in specific. I know you've been guiding many of my posts, providing thoughtful and helpful feedback...Thank goodness for Stack Overflow! – StevenC Feb 08 '18 at 15:31
1

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> 
Kris Rice
  • 2,995
  • 11
  • 31
0

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.

William Robertson
  • 12,552
  • 3
  • 33
  • 36
  • Thanks! I'll give it a shot. Very helpful - Hopefully I can get this PL/SQL down. It makes it 10x harder when I'm asked to do something that is not logical, but in this circumstance, unfortunately I have no choice. – StevenC Feb 08 '18 at 15:32
  • Thinking about it though, these coursework questions tend to love their loops and `dbms_output.put_line`s. Perhaps what they are looking for is `for r in (select...) loop dbms_output.put_line(r.customerzip); end loop;` – William Robertson Feb 09 '18 at 00:17