Oracle "NOT A GROUP BY EXPRESSION ERROR-00979" - sql

Below is my PL/SQL Oracle code. It prints the year in which max number of employees got hired and also prints the no of employees joined each month in that year.
If I execute the below code without the for-loop part it generates an error:
not a group by function error.
What can be the reason am I missing something obvious?
declare
v_year number(4);
v_c number(2);
begin
select to_char(hire_date,'yyyy') into v_year
from employees
group by to_char(hire_date,'yyyy')
having count(*) =
( select max( count(*))
from employees
group by to_char(hire_date,'yyyy')); // gets the year where max employees joined
dbms_output.put_line('Year : ' || v_year);
for month in 1 .. 12
loop
select count(*) into v_c
from employees
where to_char(hire_date,'mm') = month and to_char(hire_date,'yyyy') = v_year;
dbms_output.put_line('Month : ' || to_char(month) || ' Employees : ' || to_char(v_c));
end loop;
end;
Below is the code without for-loop:
declare
v_year number(4);
v_c number(2);
begin
select to_char(hire_date,'yyyy') into v_year
from employees
group by to_char(hire_date,'yyyy')
having count(*) =
( select max( count(*))
from employees
group by to_char(hire_date,'yyyy'));
dbms_output.put_line('Year : ' || v_year);
end;
It generates the following error:
Error report - ORA-00979: not a GROUP BY expression ORA-06512: at line 6
00979. 00000 - "not a GROUP BY expression"
*Cause:
*Action:

You don't need PL/SQL for this. Also, your SELECT ... INTO will fail if its query returns more than one row, which it would if there were two or more years with the same maximum number of employees hired.
select to_char(hire_date, 'YYYY'), to_char(hire_date, 'MM'), count(*)
from employees
group by rollup(to_char(hire_date, 'YYYY'), to_char(hire_date, 'MM'))
having to_char(hire_date, 'YYYY') =
(select to_char(hire_date, 'YYYY')
from employees
group by to_char(hire_date, 'YYYY')
having count(*) =
(select max(count(*))
from employees
group by to_char(hire_date, 'YYYY')))
order by to_char(hire_date, 'YYYY'), to_char(hire_date, 'MM')
P.S. I get the same error as you when I run your second example (no loop) in SQL Developer, but I do not get an error when I run it in SQL*Plus. SQL*Plus reports only "PL/SQL procedure successfully completed."

I think you'd better to rewrite the code using concurrent loops with year for the outer and month for the inner one through use of arrays such as
DECLARE
v_year OWA.nc_arr;
v_month OWA.nc_arr;
y INT := 0;
m INT := 0;
BEGIN
FOR yr IN
(
SELECT hire_year
FROM( SELECT TO_CHAR(hire_date, 'yyyy') AS hire_year,
DENSE_RANK() OVER (ORDER BY COUNT(*) DESC) AS dr
FROM employees
GROUP BY TO_CHAR(hire_date, 'yyyy'))
WHERE dr = 1
)
LOOP
y := y + 1;
v_year(y) := yr.hire_year;
DBMS_OUTPUT.PUT_LINE('Year'||y||' : ' || v_year(y));
FOR m IN 1 .. 12
LOOP
SELECT COUNT(*)
INTO v_month(m)
FROM employees
WHERE TO_NUMBER(TO_CHAR(hire_date, 'yyyy'))=v_year(y)
AND TO_NUMBER(TO_CHAR(hire_date, 'mm'))= m;
DBMS_OUTPUT.PUT_LINE('Month : ' || m || ' Employees : ' || to_char(v_month(m)));
END LOOP;
END LOOP;
END;
/
where analytic function is used for the year loop, since there might be more than one year, and multiple(12) variables will be needed for the month loop per each year.

Related

return the month in between sql

I want to ask is there any way in PL/SQL that I can return the months in between user input.
For example, the user put in
DateStart: 01/2019 and
DateEnd: 05/2019
The query should return
01/2019, 02/2019, 03/2019, 04/2019,05/2019.
You can do this in sql, no pl/sql needed for this.
Convert month/year to a date, 1st of the month.
Then count the months between the first and last argument.
Then select doing a connect by level + 2 (because you're including the boundary values). All put together this gives:
WITH test_data (monthyear_start, monthyear_end) AS
(
SELECT '01/2019', '05/2019' FROM DUAL
),
monthcount(months) AS
(
SELECT MONTHS_BETWEEN(TO_DATE('01/'||monthyear_end,'DD/MM/YYYY'),TO_DATE('01/'||monthyear_start,'DD/MM/YYYY')) FROM test_data
)
SELECT TO_CHAR(ADD_MONTHS(TO_DATE('01/'||monthyear_start,'DD/MM/YYYY'),LEVEL - 1),'MM/YYYY') FROM test_data, monthcount
CONNECT BY LEVEL < months + 2;
01/2019
02/2019
03/2019
04/2019
05/2019
You can firstly use connect by level <= syntax, and then apply listagg() function to concatenate the strings :
with t2 as
(
select distinct to_char( to_date(DateStart,'mm/yyyy') + level - 1 , 'mm/yyyy') as mnt
from t
connect by level <= to_date(DateEnd,'mm/yyyy') - to_date(DateStart,'mm/yyyy') + 1
)
select listagg(mnt,',') within group
(order by to_number(substr(mnt,-4)||substr(mnt,1,2))) as "Months"
from t2
Demo
Similar, but yet different, maybe even simpler than previous suggestions:
SQL> with test (dstart, dend) as
2 (select '01/2019', '05/2019' from dual)
3 select to_char(add_months(to_date(dstart, 'mm/yyyy'), level - 1), 'mm/yyyy') result
4 from test
5 connect by
6 level <= months_between(to_date(dend, 'mm/yyyy'), to_date(dstart, 'mm/yyyy')) + 1;
RESULT
-------
01/2019
02/2019
03/2019
04/2019
05/2019
SQL>
Here is a PL/SQL procedure and some test code.
DECLARE
PROCEDURE p_list_months(start_date IN DATE, end_date IN DATE) AS
v_count INTEGER := 0;
BEGIN
v_count := MONTHS_BETWEEN(end_date, start_date);
FOR i in 0..v_count LOOP
dbms_output.put_line(to_char(add_months(start_date, i),'mm/yyyy'));
END LOOP;
END;
BEGIN
p_list_months(SYSDATE, ADD_MONTHS(SYSDATE, 5));
END;
And a SQL query:
-- start_date 10/2019 end_date 10/2020
SELECT to_char(add_months(TO_DATE('10/2019','mm/yyyy'), LEVEL-1), 'mm/yyyy') dat
FROM dual
CONNECT BY LEVEL <= (MONTHS_BETWEEN( TO_DATE('10/2020','mm/yyyy'),
TO_DATE('10/2019','mm/yyyy'))) + 1;

ORA-00904: "DAYS": invalid identifier

Here is the code that I have:
SELECT (first_name || ' ' || last_name) AS Name, hire_date, days FROM EMPLOYEES WHERE days > 365*6;
This is what I am supposed to do:
Create a SQL query that shows the first_name, last_name, hire_date, and the number of days that the employee has worked from the EMPLOYEES table. Use the concatenation operator to put the first_name and the last_name together in the same column in the result set with a space between the two names. Use date arithmetic to show only employees that have worked longer than 6 years (365 * 6).
Can someone help me figure out what my error is?
To correct your query you need to perform date arithmetic.
SELECT
(first_name || ' ' || last_name) AS name,
hire_date,
ROUND(sysdate - hire_date) AS days
FROM hr.employees
WHERE sysdate - hire_date > 6*365;
But if you analyse -> hr schema, you find out, that there is job_history table relevant for this task. In my opinion correct solution should be like this:
SELECT a1.name,
a1.hire_date,
a1.days
FROM
(SELECT employees.employee_id,
(employees.first_name || ' ' || employees.last_name) AS name,
employees.hire_date,
SUM(NVL(job_history.end_date, TRUNC(sysdate)) - NVL(job_history.start_date, hire_date)) AS days
FROM hr.employees
LEFT JOIN hr.job_history
ON job_history.employee_id = employees.employee_id
GROUP BY employees.employee_id,
(employees.first_name || ' ' || employees.last_name),
employees.hire_date
) a1
WHERE days > 365*6;
You may try to put filter on the hire day instead
WHERE hire_date > sysdate - 365*6
this will work;
WHERE sysdate-hire_date >= 365*6

SQL order by with group by query

I have an EMPLOYEES table :
employee_id(1) hire_date(15-2-2001)
employee_id(2) hire_date(2-2-1999)
employee_id(3) hire_date(11-2-2003)
employee_id(4) hire_date(6-7-2001)
I want to display the YEAR with the highest number of employees hired in, with the number of employees hired each month.
I tried this :
select extract (year from hire_date)
from employees
where max(count(employee_id))=count(employee_id)
order by extract (year from hire_date);
and I keep getting an "ORA-00934: group function is not allowed here"
What am I doing wrong?
I'm using ORACLE 10g Express.
The idea is that you can use aggregation and window functions to get the totals by month and year. Then you can choose the largest using row_number() or dense_rank().
select ym.*
from (select ym.*, dense_rank() over (order by year_cnt, year) as seqnum
from (select extract(year from hire_date) as yyyy,
extract(month from hire_date) as mm,
count(*) as cnt,
sum(count(*)) over (partition by extract(year from hire_date)) as year_cnt
from employees
group by extract(year from hire_date),
extract(month from hire_date)
) ym
) ym
where seqnum = 1
order by yyyy, mm;
Hmmmm, you can do this without so many subqueries:
with ym as (
select extract(year from hire_date) as yyyy
extract(month from hire_date) as mm,
count(*) as cnt,
sum(count(*)) over (partition by extract(year from hire_date)) as yearcnt
from employees
group by extract(year from hire_date), extract(month from hire_date)
)
select *
from ym
where yearcnt = (select max(yearcnt) from ym)
order by yyyy, mm;
Of course, this returns multiple years if two years have the same maximum value.
With PL/SQL I found this :
declare
recuperation float;
CURSOR newRequest(recuperationDonnes float) is select count(employee_id) as nombreEmployes,
extract(month from hire_date) as mois from employees where
extract(year from hire_date) = (recuperationDonnes) group by extract(month from hire_date);
a float;
a1 float;
begin
select extract (year from hire_date) as annee into recuperation from employees having count
(employee_id) >= all (select count (employee_id) as emp from employees group by extract(year
from hire_date)) group by extract(year from hire_date);
OPEN newRequest(recuperation);
LOOP
FETCH newRequest into a,a1;
Exit when newRequest%NotFound;
dbms_output.put_Line('Year: '||recuperation||' mois: '||a1||' nombreEmployes: '||a);
END LOOP ;
CLOSE newRequest;
end;

Using max function on count

I would like to return 1 result, the year (datetime format) with the highest amount of orders and I'm trying to apply MAX function on my COUNT to get the value. Where have I gone wrong?
SELECT TO_CHAR(ODATE, 'YYYY') AS Year
, MAX(COUNT(*))
FROM ORDERS
GROUP BY TO_CHAR(ODATE, 'YYYY')
ORDER BY TO_CHAR(ODATE, 'YYYY');
Not sure if the MAX(COUNT(*)) is valid in this context
Instead do an ORDER on the COUNT(*) and use the ROWNUM
SELECT * FROM
(
SELECT TO_CHAR(ODATE, 'YYYY') AS Year, count(*) AS cnt
FROM ORDERS
GROUP BY TO_CHAR(ODATE, 'YYYY')
ORDER BY cnt DESC
)
WHERE ROWNUM = 1
This will ensure that you keep only the row having the highest count:
The nested query is there because ROWNUM is assigned by Oracle before the ORDER happens
Note that on Oracle 12c and above you can use the instruction FETCH FIRST x ROWS. Well described here.
This allows to do the same without a subquery because the FETCH is applied after the ORDER:
SELECT TO_CHAR(ODATE, 'YYYY') AS Year, count(*) AS cnt
FROM ORDERS
GROUP BY TO_CHAR(ODATE, 'YYYY')
ORDER BY cnt DESC
FETCH FIRST 1 ROWS ONLY;

Eliminate Multiple Output Oracle PL/SQL

I'm running this query to test my function
select first_name, last_name,get_balance_due(orders.order_id)
from customers, orders, order_lines
Where customers.customer_id=orders.customer_id and orders.order_id = order_lines.order_id and
order_date >= to_date( '30-Nov-1999', 'DD-Mon-YYYY' )
AND order_date< to_date( '01-Dec-1999', 'DD-Mon-YYYY' );
Group By First_name, Last_name, get_balance_due(orders.order_id)
The output is repeated 3 times.
FIRST_NAME LAST_NAME GET_BALANCE_DUE(ORDERS.ORDER_ID)
Jan Busse 602450.56
Jan Busse 602450.56
Jan Busse 602450.56
The function get_balance_due
CREATE OR REPLACE FUNCTION Get_balance_due
(order_id_f NUMBER)
RETURN NUMBER
AS
balance_due_f NUMBER;
BEGIN
SELECT SUM(total_amount) INTO balance_due_f
FROM order_lines
WHERE order_id_f = order_id
RETURN balance_due_f;
end;
How can I have 1 result instead of 3?
Remove the order_lines from your select statement:
select first_name, last_name,get_balance_due(orders.order_id)
from customers INNER JOIN orders
ON customers.customer_id=orders.customer_id
Where
order_date >= to_date( '30-Nov-1999', 'DD-Mon-YYYY' )
AND order_date< to_date( '01-Dec-1999', 'DD-Mon-YYYY' );
(and use INNER JOINs)