SQL Query to identify "Top Performers" [?] - sql

I'm still learning Oracle SQL and would like your guidance.
Let say, we have MONTHLY_SALES_TOTALS table that has 3 fields: name, region, amount. We need to determine the best sales people per region. Best means that their amount is equal to the maximum for the region.
CREATE TABLE montly_sales_totals
(
name varchar(20),
amount numeric(9),
region varchar(30)
);
INSERT ALL
INTO montly_sales_totals (name, amount, region) VALUES ('Peter', 55555, 'east')
INTO montly_sales_totals (name, amount, region) VALUES ('Susan', 55555, 'east')
INTO montly_sales_totals (name, amount, region) VALUES ('Mark', 1000000, 'south')
INTO montly_sales_totals (name, amount, region) VALUES ('Glenn', 50000, 'east')
INTO montly_sales_totals (name, amount, region) VALUES ('Paul', 500000, 'south')
SELECT * from dual;
Possible solution:
SELECT m1.name, m1.region, m1.amount
FROM montly_sales_totals m1
JOIN
(SELECT MAX(amount) max_amount, region FROM montly_sales_totals GROUP BY region) m2
ON (m1.region = m2.region)
WHERE m1.amount = m2.max_amount
ORDER by 2,1;
SQL Fiddle: http://sqlfiddle.com/#!4/6a2d8/6
Now my questions:
How efficient is such query?
How can/should it be simplified and/or improved?
I could not use Top since the number of "max" rows vary by region. Is it another direct functionality I could've used instead?

I would use RANK():
SELECT *
FROM (
SELECT name, amount, region,
RANK() OVER (PARTITION BY region ORDER BY amount DESC) rnk
FROM montly_sales_totals
) t
WHERE t.rnk = 1
Here's a modified version of the SQL Fiddle

There are a number of ways one can go about this. Here's another:
select S.region, S.name, V.regionmax
from sales as S
inner join
(
select region, max(amount) as regionmax
from sales group by region
) as V
on S.region = V.region and S.amount = regionmax
As to efficiency, the main factor is the use of the proper index(es). Inline views can perform very well.

I like CTE syntax, but using that website the time taken is the same 2ms, so I can't beat yours :)
with Maximums as (
SELECT region,
MAX(amount) max_amount
FROM montly_sales_totals GROUP BY region
)
SELECT m1.name, m1.region, m1.amount
FROM montly_sales_totals m1, Maximums
WHERE (m1.amount = Maximums.max_amount)
and (m1.region = Maximums.region)
ORDER by 2,1;

you can do this by using the function too...
select * from (select m1.*, row_number( ) over (partition by m1.region order by m1.amount desc,m1.name desc ) max_sal from montly_sales_totals m1 ) where max_sal =1 ;
this query can do one extra thing if both employee sal are same!

Related

Having clause with subquery

I have below scenario
CREATE TABLE plch_sales
(
region VARCHAR2 (100),
product VARCHAR2 (100),
amount NUMBER
)
/
INSERT INTO plch_sales VALUES ('North', 'Magic Wand', 1000);
INSERT INTO plch_sales VALUES ('North', 'Skele-Gro', 1000);
INSERT INTO plch_sales VALUES ('North', 'Timeturner ', 1000);
INSERT INTO plch_sales VALUES ('South', 'Portkey', 1000);
INSERT INTO plch_sales VALUES ('South', 'Quaffle', 1000);
INSERT INTO plch_sales VALUES ('West', 'Imperius', 1000);
INSERT INTO plch_sales VALUES ('West', 'Gringotts', 1000);
COMMIT;
why the below query is producing no rows? the database is oracle.
select region, sum(amount) sm_amount from PLCH_SALES group by region having sum(amount) > (select sum(amount)/3 from PLCH_SALES);
You need to first do the sum() and then divide it by 3 like the below -
SELECT region, SUM(amount) sm_amount
FROM PLCH_SALES
GROUP BY region
HAVING SUM(amount) > (SELECT SUM(amount)FROM PLCH_SALES)/3;
OR you can alternatively try using window() aggregate function
select distinct region,sm_amount from
(
select region,sum(amount) over(partition by region) as sm_amount, sum(amount) over()/3 as total
from plch_sales
)a where sm_amount>total
The way you have written will not work with having clause .
There are multiple ways to do it like with CTE , Parent_table or like below :
select region, sum(amount) sm_amount
from PLCH_SALES
group by region
having sum(amount) >(sum(amount)/3 )
;
Your query should work. The subquery returns:
2333.333333333333333333333333333333333333
The query without the having returns:
REGION SM_AMOUNT
West 2000
North 3000
South 2000
So clearly, 'North' should be in the result set.
However, it does not in Oracle. Here is a db<>fiddle. It does work with a subquery:
select x.*
from (select region, sum(amount) as sm_amount, (select sum(amount)/3 from PLCH_SALES) as thirds
from PLCH_SALES
group by region
) x
where sm_amount > thirds ;
And it does work with a CTE:
with thirds as (
select sum(s2.amount)/3 as val from PLCH_SALES s2
)
select region, sum(amount) sm_amount
from PLCH_SALES
group by region
having sum(amount) > (select val from thirds);
This sounds like a bug.
The query works in other databases:
Postgres
SQL Server
MySQL
And no doubt any other database where one would try it.

Count returning incorrect result

I've written a count query which isn't working, as expected.
The query is below and it returns three columns - DeparmtmentName, CategoryName and NumberOfCats.
The NumberOfCats column should show the number of Categories in a given Department.
So with the table below, the NumberOfCats column should have the number 4 in each row, instead of 1, because the Bakey Department has 4 Categories, in this case.
Does anyone know how I can amend the code, so it returns the right result, please?
SELECT
DepartmentName,
CategoryName,
COUNT(DISTINCT CategoryName) as NumberOfCats
FROM v_EnterpriseStructure
GROUP BY
DepartmentName,
CategoryName
ORDER BY DepartmentName;
Seems like a window function will take care of these pretty easily:
SELECT
DepartmentName,
CategoryName,
COUNT( CategoryName) OVER(Partition by DepartmentName) as NumberOfCats
FROM v_EnterpriseStructure
group by DepartmentName,
CategoryName
One way is to seperate the count from the rest:
SELECT
T1.DepartmentName,
T1.CategoryName,
AG.NumberOfCats
FROM v_EnterpriseStructure T1
INNER JOIN (
SELECT DepartmentName,
COUNT(DISTINCT CategoryName) as NumberOfCats
FROM v_EnterpriseStructure
GROUP BY DepartmentName
) AS AG
ON AG.DepartmentName = T1.DepartmentName
ORDER BY DepartmentName;
You may need to add a DISTINCT
One option, using COUNT() as an analytic function:
WITH cte AS (
SELECT *, COUNT(*) OVER (PARTITION BY DepartmentName) cnt
FROM v_EnterpriseStructure
)
SELECT DISTINCT
DepartmentName,
CategoryName,
cnt AS NumberOfCats
FROM cte
ORDER BY DepartmentName;
By adding a subquery to your query you can get the data you need. If your table is < 100.000 this should run without issues. if it's larger you might wish to consider altering your data structures.
http://sqlfiddle.com/#!18/d30e1/1
The create statement.
create table v_EnterpriseStructure (DepartmentName nvarchar(50), CategoryName nvarchar(50))
CREATE INDEX IX_DepartmentName
ON v_EnterpriseStructure(DepartmentName);
insert into v_EnterpriseStructure VALUES
('BAKERY', 'Bread'),('BAKERY', 'Cakes'),('BAKERY', 'Scones'),('BAKERY', 'Croissants'),
('BAR', 'Beer'),('BAR', 'Cola'),
('Storage', 'MrProper'),('Storage', 'MrProper');
insert into v_EnterpriseStructure select * from v_EnterpriseStructure;
insert into v_EnterpriseStructure select * from v_EnterpriseStructure;
insert into v_EnterpriseStructure select * from v_EnterpriseStructure;
insert into v_EnterpriseStructure select * from v_EnterpriseStructure;
insert into v_EnterpriseStructure select * from v_EnterpriseStructure;
The query:
SELECT
DepartmentName,
CategoryName,
(select
COUNT(distinct CategoryName) from v_EnterpriseStructure as v2
where v1.DepartmentName = v2.DepartmentName
) as NumberOfCats
FROM v_EnterpriseStructure as v1
group by DepartmentName,
CategoryName
ORDER BY DepartmentName;

How to select multiple max values from a sql table

I am trying to get the top performers from a table, grouped by the company but can't seem to get the grouping right.
I have tried to use subqueries but this goes beyond my knowledge
I am trying to make a query that selects the rows in green. In other words I want to include the name, the company, and what they paid but only the top performers of each company.
Here is the raw data
create table test (person varchar(50),company varchar(50),paid numeric);
insert into
test
values
('bob','a',200),
('jane','a',100),
('mark','a',350),
('susan','b',650),
('thabo','b',100),
('thembi','b',210),
('lucas','b',110),
('oscar','c',10),
('janet','c',20),
('nancy','c',30)
You can use MAX() in a subquery as
CREATE TABLE T(
Person VARCHAR(45),
Company CHAR(1),
Paid INT
);
INSERT INTO T
VALUES ('Person1', 'A', 10),
('Person2', 'A', 20),
('Person3', 'B', 10);
SELECT T.*
FROM T INNER JOIN
(
SELECT Company, MAX(Paid) Paid
FROM T
GROUP BY Company
) TT ON T.Company = TT.Company AND T.Paid = TT.Paid;
Demo
Or using a window function as
SELECT Person,
Company,
Paid
FROM
(
SELECT *, ROW_NUMBER() OVER(PARTITION BY Company ORDER BY Paid DESC) RN
FROM T
) TT
WHERE RN = 1;
Demo
Here's your query.
select a.person, a.company, a.paid from tableA a
inner join
(select person, company, row_number() over (partition by company order by paid desc) as rn from tableA) as t1
on t1.person = a.person and t1.company = a.company
where t1.rn = 1
Maybe something like
WITH ranked AS (SELECT person, company, paid
, rank() OVER (PARTITION BY company ORDER BY paid DESC) AS rnk
FROM yourtable)
SELECT person, company, paid
FROM ranked
WHERE rnk = 1
ORDER BY company;
You can use rank() function with partition by clause.
DENSE_RANK gives you the ranking within your ordered partition, but the ranks are consecutive. No ranks are skipped if there are ranks with multiple items.
WITH cte AS (
SELECT person, company, paid
rank() OVER (PARTITION BY company ORDER BY paid desc) rn
FROM yourtable
)
SELECT
*
FROM cte

Multiple Aggregates Pivot Table

I have a few columns of data I am wanting to put into a Pivot table. This is what the data currently looks like once the following query is ran:
SELECT
t.clinic,
t.fiscal_year,
t.total,
n.new_pats,
(t.total - n.new_pats) AS active
FROM (SELECT DISTINCT
clinic,
fiscal_year,
COUNT(DISTINCT patient_id) AS total
FROM transactions t
JOIN period p
ON (t.date_entered BETWEEN p.period_start AND p.period_end)
GROUP BY clinic, fiscal_year) t
JOIN (SELECT DISTINCT
clinic,
per.fiscal_year,
COUNT(DISTINCT patient_id) AS new_pats
FROM patient pat
JOIN period per
ON (pat.first_visit_date BETWEEN per.period_start AND per.period_end)
GROUP BY clinic, fiscal_year) n
ON (t.clinic = n.clinic AND t.fiscal_year = n.fiscal_year)
And I would like the data to be broken up by the three aggregates in their own rows and the years being the columns where it looks something like this:
This could also be broken into 3 separate columns per aggregate per year, but this is what I'm ideally aiming for. I haven't put together a Pivot table within SQL before and am at a loss. Is there a better way to go about formatting this data in the desired way?
You could try this
SELECT clinic,
aggregate_field,
ISNULL([2009],0) AS [2009],
ISNULL([2010],0) AS [2010],
ISNULL([2016],0) AS [2016],
ISNULL([2017],0) AS [2017]
FROM (SELECT clinic,
fiscal_year,
aggregate_field,
value
FROM (
------- This part is your original query -----------------
SELECT t.clinic,
t.fiscal_year,
t.total,
n.new_pats,
( t.total - n.new_pats ) AS active
FROM (SELECT DISTINCT clinic,
fiscal_year,
Count(DISTINCT patient_id) AS total
FROM transactions t
JOIN period p
ON ( t.date_entered BETWEEN
p.period_start AND p.period_end )
GROUP BY clinic, fiscal_year) t
JOIN (SELECT DISTINCT clinic,
per.fiscal_year,
Count(DISTINCT patient_id) AS new_pats
FROM patient pat
JOIN period per
ON ( pat.first_visit_date BETWEEN
per.period_start AND per.period_end )
GROUP BY clinic, fiscal_year) n
ON ( t.clinic = n.clinic
AND t.fiscal_year = n.fiscal_year )
-------------------------------------------------------------
)
unpivot_source
UNPIVOT ( value
FOR aggregate_field IN (total,
new_pats,
active) ) unpivot_result) AS
pivot_source
PIVOT ( Max(value)
FOR fiscal_year IN ([2009],
[2010],
[2016],
[2017]) ) AS pivot_result
ORDER BY clinic, aggregate_field DESC
I've create a demo here http://rextester.com/MFHWV68715.
Try this:
CREATE TABLE #ActualData
(
Clinic varchar(50),
fiscal_year int,
total int,
new_pats int,
active int
)
INSERT INTO #ActualData VALUES ('A', 2016, 3538,1787,1751), ('A', 2017, 1218,373,845), ('B', 2009, 1,2,3), ('B', 2010, 1,2,3)
SELECT * FROM #ActualData ad
;WITH temps AS
(
SELECT Clinic, fiscal_year, aggregateF, Number FROM
(
SELECT ad.Clinic, ad.fiscal_year, ad.total,ad.new_pats,ad.active FROM #ActualData ad
) src
UNPIVOT
(
Number FOR aggregateF IN (total,new_pats ,active)
) unpvt
)
SELECT Clinic, aggregateF, [2009],[2010],[2016],[2017]
FROM
(
SELECT t.Clinic, t.aggregateF, t.fiscal_year, t.Number FROM temps t
) src
PIVOT
(
MIN (Number) FOR fiscal_year IN ([2009],[2010],[2016],[2017])
) pvt
ORDER BY pvt.Clinic, pvt.aggregateF DESC
DROP TABLE #ActualData

Sorting with sql

I have a question on how to sort data using sql. For that I made up a simple example to illustrate my problem.
if object_id('MyTable', 'U') is not null drop table dbo.MyTable;
create table MyTable
(
country varchar(10) not null
, town varchar( 10 ) not null
, amount int not null
)
insert into MyTable values
( 'US', 'NYC', 100 )
, ( 'US', 'BAL', 150 )
, ( 'US', 'WAS', 200 )
, ( 'CA', 'MON', 100 )
, ( 'CA', 'TOR', 150 )
, ( 'CA', 'VAN', 200 )
How can I sort the data in a sense the all "countries are sorted by the amount in descending order AND that the towns in alphabetical order for each country.
Thanks,
Christian
To sort in SQL, use Order By: http://msdn.microsoft.com/en-us/library/ms188385.aspx
So if you wanted sorted by Country, then Amount, then Town, you'd add an Order By clause after your Where class like:
ORDER BY Country, Amount DESC, Town
I think that this should do it:
SELECT
country,
SUM(amount) OVER(PARTITION BY country) as CountryTotal,
town,
amount
FROM MyTable
ORDER BY CountryTotal, country, town
I am not sure whether you want to order by total amount of a country or just amount .
for total amount of country use below.
select
mt.country,
ctotal.countrycotal,
mt.town,
mt.amount
from
(
SELECT
country,
SUM(amount) as countrycotal,
FROM MyTable
group BY country
) ctotal
inner join Mytable mt on ctotal.country = mt.country
order by ctotal.countrytotal,ctotal.country,mt.town
for just amount use below
select * from Mytable
order by mt.amount,mt.country,mt.town