'Can I group by in SQL query with window function?

I need to get employees with smallest salary in their departments I did it using anti join.

     select emp.employee_id,emp.last_name,emp.salary,emp.department_id
     from employees emp
     left join employees sml 
     on sml.department_id = emp.department_id and sml.salary < emp.salary
     where sml.employee_id is null and emp.department_id is not null

But I've been told that it's possible to do it using window function using one select. However I can't group it by department_id and use it at the same time. Is that a bug or me being stupid?

     SELECT  department_id,
     min(salary) OVER (partition by department_id)  as minsalary
     FROM employees;
     GROUP BY department_id

SQL Developer says 00979. 00000 - "not a GROUP BY expression"



Solution 1:[1]

First thing to remember is that windowed functions (like OVER() clause) work on the result of the query. That is: Server first executes the query and only then applies the windowed function as defined by you (of course, it's an oversimplification of what actually happens, but good enough to illustrate my point).

This means that you can actually use windowed function and group by clause in the same query, but you need to encapsulate group by aggregate with windowed function aggregate, like this:

SELECT department_id,
       min(min(salary)) OVER (partition by department_id) as minsalary
FROM employees
GROUP BY department_id;

However, I agree that this is not a good place to use windowed function. Matt's proposition - which I upvoted, full disclosure - is best here (ROW_NUMBER() in CTE or subquery, then selecting only the desired rows in main SELECT).

Solution 2:[2]

WITH cte AS (
    SELECT
       emp.*
       ,ROW_NUMBER() OVER (PARTITION BY emp.department_id ORDER BY emp.salary) as RowNumber
    FROM
       employees emp
)

SELECT c.*
FROM
    cte c
WHERE
    c.RowNumber = 1

You can use ROW_NUMBER() to get 1 row of lowest salary by department as above. If you want all rows in the case of ties switch it to RANK()

Otherwise you can do it with MIN() OVER but this will give you ties

WITH cte AS (
    SELECT
       emp.*
       ,MIN(emp.salary) OVER (PARTITION BY emp.department_id) as DeptMinSalary
    FROM
       employees emp
)

SELECT c.*
FROM
    cte c
WHERE
    c.salary = c.DeptMinSalary

As a derived table instead of a Common Table Expression:

SELECT t.*
FROM
    (SELECT
       emp.*
       ,ROW_NUMBER() OVER (PARTITION BY emp.department_id ORDER BY emp.salary) as RowNumber
    FROM
       employees emp) t
WHERE
    t.RowNumber = 1

One last thought on the subject because you ask "Can I group by in a SQL query with a window function?" Alex covers that the PARTITION BY is like a sub grouping within the Window Function. But to use a GROUP BY grouping with a Window function means that the GROUP BY result set would be evaluated PRIOR to the Window Function being evaluated.

Solution 3:[3]

you do not need window function in this case, cause a simple group by would work too.

And the error is correct, cause the window function isn't an aggregat function. And a window function can't be a Group by- member.

But you could use "distinct" instead.

SELECT DISTINCT department_id,
     min(salary) OVER (partition by department_id)  as minsalary
FROM employees;

In your Special case all this is oversized, of course. But I think understanding is the name of the game.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2
Solution 3 am2