oracle sql union order by - sql

This is a certification question case A and case B, why it works in case B, but doesn't work in case A
case A (not working)
select col_a, col_b, 'b' from table_a
union
select col_a, col_b, 'a' from table_a
order by 'b';
case B (working)
select col_a, col_b, 'b' from table_a order by 'b';

You're sorting data by a string literal, 'a' which is as good as any other string literal, such as 'Littlefoot': both are useless, but are allowed:
SQL> select dname, 'a' from dept order by 'a';
DNAME '
-------------- -
ACCOUNTING a
RESEARCH a
SALES a
OPERATIONS a
SQL> select dname, 'a' from dept order by 'Littlefoot';
DNAME '
-------------- -
ACCOUNTING a
RESEARCH a
SALES a
OPERATIONS a
SQL>
order by clause - applied to unioned queries - works for the whole data set returned by that union:
SQL> select dname, 'a' from dept where deptno <= 20
2 union
3 select dname, 'b' from dept where deptno > 20
4 order by dname;
DNAME '
-------------- -
ACCOUNTING a
OPERATIONS b
RESEARCH a
SALES b
SQL>
As you already know, sorting by string literal won't work in this case:
SQL> select dname, 'a' from dept where deptno <= 20
2 union
3 select dname, 'b' from dept where deptno > 20
4 order by 'Littlefoot';
order by 'Littlefoot'
*
ERROR at line 4:
ORA-01785: ORDER BY item must be the number of a SELECT-list expression
SQL>
That error literally means:
You tried to execute a SELECT statement that included a ORDER BY clause that referenced a column number that did not correspond to a valid column in your SELECT list.
As you were already told, you could use an alias or column's position, or use that union as a subquery and then apply sorting by a string literal - in that case, it'll (again) work:
SQL> select *
2 from (select dname, 'a' from dept where deptno <= 20
3 union
4 select dname, 'b' from dept where deptno > 20
5 )
6 order by 'Littlefoot';
DNAME '
-------------- -
ACCOUNTING a
OPERATIONS b
RESEARCH a
SALES b
SQL>
That's useless (as it was before), but it is allowed.
One could hope that something like this would work ("hope" by means of implicit data conversion, where Oracle would convert '2' (a string) to 2 (a number) and sort by position), but - nope:
SQL> select dname, 'a' from dept where deptno <= 20
2 union
3 select dname, 'b' from dept where deptno > 20
4 order by '2';
order by '2'
*
ERROR at line 4:
ORA-01785: ORDER BY item must be the number of a SELECT-list expression
Positional sorting works, of course:
SQL> select dname, 'a' from dept where deptno <= 20
2 union
3 select dname, 'b' from dept where deptno > 20
4 order by 2;
DNAME '
-------------- -
RESEARCH a
ACCOUNTING a
SALES b
OPERATIONS b
SQL>
Therefore, I suspect that it is the union that causes problems. Maybe it is about the fact that UNION returns DISTINCT data set, and - with it - ORDER BY clause can't refer columns that aren't part of the SELECT column list, literally.

'b' is a string literal and not an identifier for the column (as the identifier for the column is the quoted identifier "'B'").
You can either use the column number:
select col_a, col_b, 'b' from table_a union
select col_a, col_b, 'a' from table_a
order by 3;
or use a quoted identifier and convert the literal to upper-case:
select col_a, col_b, 'b' from table_a union
select col_a, col_b, 'a' from table_a
order by "'B'";
or give the column an alias and use that:
select col_a, col_b, 'b' AS b from table_a union
select col_a, col_b, 'a' from table_a
order by b;
See the SELECT documentation
Set Operators: UNION, UNION ALL, INTERSECT, MINUS
[...]
order_by_clause
Use the ORDER BY clause to order rows returned by the statement. Without an order_by_clause, no guarantee exists that the same query executed more than once will retrieve rows in the same order.
[...]
expr
expr orders rows based on their value for expr. The expression is based on columns in the select list or columns in the tables, views, or materialized views in the FROM clause.
[...]
Restrictions on the ORDER BY Clause
The following restrictions apply to the ORDER BY clause:
If you have specified the DISTINCT operator in this statement, then this clause cannot refer to columns unless they appear in the select list.
You are using UNION (and not UNION ALL) which is implicitly applying the DISTINCT operator so the restriction applies that you "refer to columns unless they appear in the select list." The identifier for the column in the select list is the quoted identifier "'B'" and not the string literal 'b' (and also not the string literal 'B') and you need to refer to it as that (or by the position in the select list or by an alias).
Note: Even without applying the additional restriction from the implicit DISTINCT, the base restriction for UNION queries is "The expression is based on columns in the select list or columns in the tables, views, or materialized views in the FROM clause" and the literal 'b' is not a column in the select list or from an object of the FROM clause so it would be valid if you used UNION ALL instead of UNION.

Related

oracle count query with union

I have a query with union all functionality each giving me count(*) return from respective queries and another count query like below. I want an outer query that gives the total.
1st query
select count(*) from a
union all
select count(*) from b;
Sample result for 1st query:
COUNT
10
40
2nd query
select count(*) from xy;
Sample result for 2nd query:
COUNT
20
I want output like this in 2 rows:
TABLES
COUNT
xy
20
ab
50
something like above. How can I achieve this in oracle? please suggest the best way to do this.
I wrote a select and union all but not sure how to proceed further.
One option is to sum counts returned by the 1st query and then union it with the 2nd; also, add constants which show the source:
select 'ab' what, (select count(*) from a) + (select count(*) from b) cnt from dual
union all
select 'xy', count(*) from xy;
You can use:
SELECT 'ab' AS type,
COUNT(*) AS total
FROM ( SELECT 1 FROM a UNION ALL
SELECT 1 from b );
UNION ALL
SELECT 'xy', COUNT(*)
FROM xy;
You can sum counts from your three unioned Select statements and group the result by combination of sources:
WITH
a AS
( Select LEVEL "A_ID", 'some column a' "COL_A" From Dual Connect By LEVEL <= 30 ),
b AS
( Select LEVEL "B_ID", 'some column b' "COL_B" From Dual Connect By LEVEL <= 20 ),
xy AS
( Select LEVEL "XY_ID", 'some column xy' "COL_XY" From Dual Connect By LEVEL <= 20 )
with above sample data it is like here:
SELECT
CASE WHEN SOURCE IN('a', 'b') THEN 'ab' ELSE SOURCE END "SOURCE",
Sum(CNT) "CNT"
FROM
( Select 'a' "SOURCE", Count(*) "CNT" From a Union All
Select 'b', Count(*) From b Union All
Select 'xy', Count(*) From xy
)
GROUP BY
CASE WHEN SOURCE IN('a', 'b') THEN 'ab' ELSE SOURCE END
--
-- R e s u l t :
-- SOURCE CNT
-- ------ ----------
-- ab 50
-- xy 20
Assuming that your real queries can be a lot more complex, I take it as a given that we shall not try to change them and somehow merge or split them.
Your first query returns two rows. You want to get their sum, so you must aggregate the result and use SUM.
Below query uses CTEs (subqueries in the WITH clause) for your two queries, and then a query that gets this sum. It then uses these CTEs for the final UNION ALL query.
with query1 (cnt) as (select count(*) from a union all select count(*) from b)
, query2 (cnt) as (select count(*) from xy)
, sumquery1 (total) as (select sum(cnt) from query1)
select 'ab' as tables, total from sumquery1
union all
select 'xy' as tables, cnt from query2
order by tables desc;

Change result set order based on another column being null

I have a query that sorts the results based on the second column.
select decode(name, null, 'n/a', name) name, value1
from tableA
group by name
order by 2
How can I change it so a result where the first column is null is always the last row in the result set, without changing the ordering for the rest of the results?
One solution I have in my mind is to use union two queries, one which excludes nulls and one which only has nulls, something like:
select decode(name, null, 'n/a', name) name, value1
from tableA
where name is not null
group by name
union
select decode(name, null, 'n/a', name) name, value1
from tableA
where name is null
group by name
order by 2
Is there a better way to do this?
Your union approach won't get the result you want; the order by is applied after the two queries are unioned, so they will be sorted the same as if you had a single query. You could add a flag column in each brach of the union and include that in the ordering, but the you need to exclude it from the final select list.
You can handle this in the order by clause, using a case statement (or decode if you prefer) against column 1 which treats all not-null values as one priority regardless of the actual value, and all null values as a different priority; and then further orders by column 2:
select decode(a.name, null, 'n/a', a.name) name, value1
from tableA a
order by case when a.name is null then 1 else 0 end, value1 desc
I've used a table alias and included that in the case to avoid confusion between the original table value and the column alias of the same name. This will put all the results with null ('n/a') names after all of those which are not null; and within each category all the results will still be sorted by the second column.
With some sample data:
with tableA (name, value1) as (
select 'Joe', 3 from dual
union all select 'Anne', 10 from dual
union all select null, 4 from dual
union all select 'Sarah', 2 from dual
union all select 'Bill', 5 from dual
union all select 'Mary', 7 from dual
)
... ordering just by the second column gets:
select decode(a.name, null, 'n/a', a.name) name, value1
from tableA a
order by value1 desc;
NAME VALUE1
----- ----------
Anne 10
Mary 7
Bill 5
n/a 4
Joe 3
Sarah 2
Adding this case clause puts your 'n/a' row last in the result set:
select decode(a.name, null, 'n/a', a.name) name, value1
from tableA a
order by case when a.name is null then 1 else 0 end, value1 desc;
NAME VALUE1
----- ----------
Anne 10
Mary 7
Bill 5
Joe 3
Sarah 2
n/a 4
(I've ignored the group by clause in your example because you don't have any aggregates, and neither of your queries is valid; but you can still group and do this in your real query if you need to.)

Oracle Pivot query gives columns with quotes around the column names. What?

I'm trying to use PIVOT in Oracle and I'm getting a weird result. It's probably just an option I need to set but what I know about Oracle/SQL I could fit into this comment box.
Here's an example of my query:
with testdata as
(
select 'Fred' First_Name, 10 Items from dual
union
select 'John' First_Name, 5 Items from dual
union
select 'Jane' First_Name, 12 Items from dual
union
select 'Fred' First_Name, 15 Items from dual
)
select * from testdata
pivot (
sum(Items)
for First_Name
in ('Fred','John','Jane')
The results come out as I expected except the Column names have single quotes around them (picture from Toad - if I export to Excel the quotes get carried to Excel):
How do I get rid of the single quotes around the column names? I tried taking them out in the "in" clause and I get an error:
in (Fred,John,Jane)
I also tried replacing the single quotes with double quotes and got the same error. I don't know if this is an Oracle option I need to set/unset before running my query or a Toad thing.
you can provide aliases to the new columns in the pivot statement's IN clause.
(NB: This is different from the standard where clause IN() which does not allow aliases.)
with testdata as
(
select 'Fred' First_Name, 10 Items from dual
union
select 'John' First_Name, 5 Items from dual
union
select 'Jane' First_Name, 12 Items from dual
union
select 'Fred' First_Name, 15 Items from dual
)
select * from testdata
pivot (
sum(Items)
for First_Name
in ('Fred' as fred,'John' as john,'Jane' as jane)
)
and also for your aggregate clause which is necessary if you have multiple clauses..
with testdata as
(
select 'Fred' First_Name, 10 Items from dual
union
select 'John' First_Name, 5 Items from dual
union
select 'Jane' First_Name, 12 Items from dual
union
select 'Fred' First_Name, 15 Items from dual
)
select * from testdata
pivot (
sum(Items) itmsum,
count(Items) itmcnt
for First_Name
in ('Fred' as fred,'John' as john,'Jane' as jane)
)
returns
FRED_ITMSUM FRED_ITMCNT JOHN_ITMSUM JOHN_ITMCNT JANE_ITMSUM JANE_ITMCNT
----------- ----------- ----------- ----------- ----------- -----------
25 2 5 1 12 1
Of course you can then go full circle and use standard oracle aliasing and rename them to whatever you like including putting quotes back in again..
with testdata as
(
select 'Fred' First_Name, 10 Items from dual
union
select 'John' First_Name, 5 Items from dual
union
select 'Jane' First_Name, 12 Items from dual
union
select 'Fred' First_Name, 15 Items from dual
)
select FRED_ITMSUM "Fred's Sum", FRED_ITMCNT "Fred's Count"
, JOHN_ITMSUM "John's Sum", JOHN_ITMCNT "John's Count"
, JANE_ITMSUM "Janes's Sum", JANE_ITMCNT "Janes's Count"
from testdata
pivot (
sum(Items) itmsum,
count(Items) itmcnt
for First_Name
in ('Fred' as fred,'John' as john,'Jane' as jane)
)
gives
Fred's Sum Fred's Count John's Sum John's Count Janes's Sum Janes's Count
---------- ------------ ---------- ------------ ----------- -------------
25 2 5 1 12 1

GROUP BY without aggregate function

I am trying to understand GROUP BY (new to oracle dbms) without aggregate function.
How does it operate?
Here is what i have tried.
EMP table on which i will run my SQL.
SELECT ename , sal
FROM emp
GROUP BY ename , sal
SELECT ename , sal
FROM emp
GROUP BY ename;
Result
ORA-00979: not a GROUP BY expression
00979. 00000 - "not a GROUP BY expression"
*Cause:
*Action:
Error at Line: 397 Column: 16
SELECT ename , sal
FROM emp
GROUP BY sal;
Result
ORA-00979: not a GROUP BY expression
00979. 00000 - "not a GROUP BY expression"
*Cause:
*Action: Error at Line: 411 Column: 8
SELECT empno , ename , sal
FROM emp
GROUP BY sal , ename;
Result
ORA-00979: not a GROUP BY expression
00979. 00000 - "not a GROUP BY expression"
*Cause:
*Action: Error at Line: 425 Column: 8
SELECT empno , ename , sal
FROM emp
GROUP BY empno , ename , sal;
So, basically the number of columns have to be equal to the number of columns in the GROUP BY clause, but i still do not understand why or what is going on.
That's how GROUP BY works. It takes several rows and turns them into one row. Because of this, it has to know what to do with all the combined rows where there have different values for some columns (fields). This is why you have two options for every field you want to SELECT : Either include it in the GROUP BY clause, or use it in an aggregate function so the system knows how you want to combine the field.
For example, let's say you have this table:
Name | OrderNumber
------------------
John | 1
John | 2
If you say GROUP BY Name, how will it know which OrderNumber to show in the result? So you either include OrderNumber in group by, which will result in these two rows. Or, you use an aggregate function to show how to handle the OrderNumbers. For example, MAX(OrderNumber), which means the result is John | 2 or SUM(OrderNumber) which means the result is John | 3.
Given this data:
Col1 Col2 Col3
A X 1
A Y 2
A Y 3
B X 0
B Y 3
B Z 1
This query:
SELECT Col1, Col2, Col3 FROM data GROUP BY Col1, Col2, Col3
Would result in exactly the same table.
However, this query:
SELECT Col1, Col2 FROM data GROUP BY Col1, Col2
Would result in:
Col1 Col2
A X
A Y
B X
B Y
B Z
Now, a query:
SELECT Col1, Col2, Col3 FROM data GROUP BY Col1, Col2
Would create a problem: the line with A, Y is the result of grouping the two lines
A Y 2
A Y 3
So, which value should be in Col3, '2' or '3'?
Normally you would use a GROUP BY to calculate e.g. a sum:
SELECT Col1, Col2, SUM(Col3) FROM data GROUP BY Col1, Col2
So in the line, we had a problem with we now get (2+3) = 5.
Grouping by all your columns in your select is effectively the same as using DISTINCT, and it is preferable to use the DISTINCT keyword word readability in this case.
So instead of
SELECT Col1, Col2, Col3 FROM data GROUP BY Col1, Col2, Col3
use
SELECT DISTINCT Col1, Col2, Col3 FROM data
You're experiencing a strict requirement of the GROUP BY clause. Every column not in the group-by clause must have a function applied to reduce all records for the matching "group" to a single record (sum, max, min, etc).
If you list all queried (selected) columns in the GROUP BY clause, you are essentially requesting that duplicate records be excluded from the result set. That gives the same effect as SELECT DISTINCT which also eliminates duplicate rows from the result set.
The only real use case for GROUP BY without aggregation is when you GROUP BY more columns than are selected, in which case the selected columns might be repeated. Otherwise you might as well use a DISTINCT.
It's worth noting that other RDBMS's do not require that all non-aggregated columns be included in the GROUP BY. For example in PostgreSQL if the primary key columns of a table are included in the GROUP BY then other columns of that table need not be as they are guaranteed to be distinct for every distinct primary key column. I've wished in the past that Oracle did the same as it would have made for more compact SQL in many cases.
Let me give some examples.
Consider this data.
CREATE TABLE DATASET ( VAL1 CHAR ( 1 CHAR ),
VAL2 VARCHAR2 ( 10 CHAR ),
VAL3 NUMBER );
INSERT INTO
DATASET ( VAL1, VAL2, VAL3 )
VALUES
( 'b', 'b-details', 2 );
INSERT INTO
DATASET ( VAL1, VAL2, VAL3 )
VALUES
( 'a', 'a-details', 1 );
INSERT INTO
DATASET ( VAL1, VAL2, VAL3 )
VALUES
( 'c', 'c-details', 3 );
INSERT INTO
DATASET ( VAL1, VAL2, VAL3 )
VALUES
( 'a', 'dup', 4 );
INSERT INTO
DATASET ( VAL1, VAL2, VAL3 )
VALUES
( 'c', 'c-details', 5 );
COMMIT;
Whats there in table now
SELECT * FROM DATASET;
VAL1 VAL2 VAL3
---- ---------- ----------
b b-details 2
a a-details 1
c c-details 3
a dup 4
c c-details 5
5 rows selected.
--aggregate with group by
SELECT
VAL1,
COUNT ( * )
FROM
DATASET A
GROUP BY
VAL1;
VAL1 COUNT(*)
---- ----------
b 1
a 2
c 2
3 rows selected.
--aggregate with group by multiple columns but select partial column
SELECT
VAL1,
COUNT ( * )
FROM
DATASET A
GROUP BY
VAL1,
VAL2;
VAL1
----
b
c
a
a
4 rows selected.
--No aggregate with group by multiple columns
SELECT
VAL1,
VAL2
FROM
DATASET A
GROUP BY
VAL1,
VAL2;
VAL1
----
b b-details
c c-details
a dup
a a-details
4 rows selected.
--No aggregate with group by multiple columns
SELECT
VAL1
FROM
DATASET A
GROUP BY
VAL1,
VAL2;
VAL1
----
b
c
a
a
4 rows selected.
You have N columns in select (excluding aggregations), then you should have N or N+x columns
Use sub query e.g:
SELECT field1,field2,(SELECT distinct field3 FROM tbl2 WHERE criteria) AS field3
FROM tbl1 GROUP BY field1,field2
OR
SELECT DISTINCT field1,field2,(SELECT distinct field3 FROM tbl2 WHERE criteria) AS field3
FROM tbl1
If you have some column in SELECT clause , how will it select it if there is several rows ? so yes , every column in SELECT clause should be in GROUP BY clause also , you can use aggregate functions in SELECT ...
you can have column in GROUP BY clause which is not in SELECT clause , but not otherwise
As an addition
basically the number of columns have to be equal to the number of columns in the GROUP BY clause
is not a correct statement.
Any attribute which is not a part of GROUP BY clause can not be used for selection
Any attribute which is a part of GROUP BY clause can be used for selection but not mandatory.
For anyone trying to group data (from foreign tables as an example) like a json object with nested arrays of data you can achieve this in sql with array_agg (you can also use this in conjunction with json_build_object to create a json object with key-value pairs).
As a refference, I found helpful this video on yt: https://www.youtube.com/watch?v=A6N1h9mcJf4
-- Edit
If you want to have a nested array inside a nested array, you could do it by using array.
In the following example, 'variation_images' (subquery 2 - in relation to the variation table) are nested under the 'variation' query (subquery 1 - in relation to product table) which is nested under the product query (main query):
SELECT product.title, product.slug, product.description,
ARRAY(SELECT jsonb_build_object(
'var_id', variation.id, 'var_name', variation.name, 'images',
ARRAY(SELECT json_build_object('img_url', variation_images.images)
FROM variation_images WHERE variation_images.variation_id = variation.id)
)
FROM variation WHERE variation.product_id = product.id)
FROM product
I know you said you want to understand group by if you have data like this:
COL-A COL-B COL-C COL-D
1 Ac C1 D1
2 Bd C2 D2
3 Ba C1 D3
4 Ab C1 D4
5 C C2 D5
And you want to make the data appear like:
COL-A COL-B COL-C COL-D
4 Ab C1 D4
1 Ac C1 D1
3 Ba C1 D3
2 Bd C2 D2
5 C C2 D5
You use:
select * from table_name
order by col-c,colb
Because I think this is what you intend to do.

how to sort by case insensitive alphabetical order using COLLATE NOCASE

I am trying to sort alphabetically case insensitive using COLLATE NOCASE
but getting error
ORA - 00933 SQL command not properly ended.
below is the query I am firing:
SELECT LPN.LPN_ID,
LPN.TC_ORDER_ID,
ORDERS.D_NAME,
ORDERS.PURCHASE_ORDER_NUMBER AS ORDER_PURCHASE_ORDER_NUMBER,
ORDERS.D_NAME AS D_NAME_2, LPN.LPN_NBR_X_OF_Y
FROM ORDERS ORDERS,
LPN LPN
WHERE ORDERS.ORDER_ID=LPN.ORDER_ID
ORDER BY ORDERS.D_NAME COLLATE NOCASE DESC
I checked here to try this but still getting error
How to use SQL Order By statement to sort results case insensitive?
Any suggestions please ?
Oracle does not support COLLATE NOCASE option of the order by clause. To be able to perform case-insensitive ordering you have two options:
Set NLS_COMP='ANSI' and 'NLS_SORT=BINARY_CI', CI suffix means case-insensitive, session or system wide by using alter session or alter system statement:
alter session set nls_comp='ANSI';
alter session set nls_sort='BINARY_CI';
with t1(col) as(
select 'A' from dual union all
select 'a' from dual union all
select 'b' from dual union all
select 'B' from dual
)
select *
from t1
order by col
Result:
COL
---
A
a
b
B
Change case of the character literal by using either upper() or lower() function.
with t1(col) as(
select 'A' from dual union all
select 'a' from dual union all
select 'b' from dual union all
select 'B' from dual
)
select *
from t1
order by upper(col)
result:
COL
---
A
a
b
B
Edit
but i need the UpperCase to preceed any LowerCase eg. Alan, alan, Brian, brian, Cris
This is not the case-insensitive ordering, rather quite contrary in some sense. As one of the options you could do the following to produce desired result:
with t1(col) as(
select 'alan' from dual union all
select 'Alan' from dual union all
select 'brian' from dual union all
select 'Brian' from dual union all
select 'Cris' from dual
)
select col
from ( select col
, case
when row_number() over(partition by lower(col)
order by col) = 1
then 1
else 0
end as rn_grp
from t1
)
order by sum(rn_grp) over(order by lower(col))
Result:
COL
-----
Alan
alan
Brian
brian
Cris
COLLATE NOCASE does not work with Oracle, Try this:
SELECT LPN.LPN_ID,
LPN.TC_ORDER_ID,
ORDERS.D_NAME,
ORDERS.PURCHASE_ORDER_NUMBER AS ORDER_PURCHASE_ORDER_NUMBER,
ORDERS.D_NAME AS D_NAME_2,
LPN.LPN_NBR_X_OF_Y
FROM orders orders,
lpn lpn
where orders.order_id=lpn.order_id
ORDER BY lower(orders.d_name) DESC;
Since 10g there is a function NLSSORT which does pretty much what Nicholas Krasnov described but doesn't require altering the system or session.
so you can try something like this:
SELECT LPN.LPN_ID, LPN.TC_ORDER_ID, ORDERS.D_NAME, ORDERS.PURCHASE_ORDER_NUMBER
AS ORDER_PURCHASE_ORDER_NUMBER, ORDERS.D_NAME AS D_NAME_2, LPN.LPN_NBR_X_OF_Y
FROM ORDERS ORDERS, LPN LPN
WHERE ORDERS.ORDER_ID=LPN.ORDER_ID
ORDER BY nlssort(ORDERS.D_NAME, 'NLS_SORT = binary_ci') desc
Note you can't use this directly in a UNION or you'll get the following error:
ORA-01785: ORDER BY item must be the number of a SELECT-list
expression.
Instead, you need to wrap it:
SELECT * FROM (SELECT a, b FROM x, y UNION SELECT c, d FROM m, n)
ORDER BY nlssort(a, 'nls_sort=binary_ci') DESC