How do I select rows from table that have one or more than one specific value in a column? - sql

I have a table containing data such as:
BP_NUMBER,CONTRACT_TYPE
0000123, 1
0000123, 2
0000123, 3
0000123, 4
0000124, 4
0000124, 4
0000124, 4
0000125, 4
0000126, 1
0000126, 5
I want to select rows containing one or more occurrences of CONTRACT_TYPE = 4. In other words, I want to know who are the clients with one or more contracts of the same type and type 4.
I tried this query:
SELECT * FROM (
SELECT BP_NUMBER, CONTRACT_TYPE, COUNT(*) OVER (PARTITION BY BP_NUMBER) CT FROM CONTRACTS
WHERE (1=1)
AND DATE = '18/10/2022'
AND CONTRACT_TYPE = 4)
WHERE CT= 1;
But it returns rows with only one occurrence of CONTRACT_TYPE = 4.
Also tried something like:
SELECT BP_NUMBER FROM CONTRACTS
WHERE (1=1)
AND CONTRACT_TYPE = 4
AND CONTRACT_TYPE NOT IN (SELECT CONTRACT_TYPE FROM CONTRACTS WHERE CONTRACT_TYPE != 4 GROUP BY CONTRACT_TYPE);
Trying to avoid any other contract types than 4. I really don't understand why it doesn't work.
The expected result would be:
0000124 --(4 occurrences of type 4)
0000125 --(1 occurrence of type 4)
Any help? Thanks

You can try something like this:
SELECT
BP_NUMBER
FROM CONTRACTS c1
WHERE CONTRACT_TYPE = 4
AND NOT EXISTS
(SELECT 1 FROM CONTRACTS c2 WHERE c2.BP_NUMBER = c1.BP_NUMBER
AND c2.CONTRACT_TYPE <> c1.CONTRACT_TYPE)
Depending on how you actually want to see it (and what other values you might want to include), you could either do a DISTINCT on the BP_NUMBER, or group on that column (and potentially others)
A similar result could also be achieved using an outer join between two instances of the CONTRACTS table. Essentially, you need the second instance of the same table so that you can exclude output rows when there are records with the "unwanted" contract types

You can just do the aggregation like here:
WITH
tbl AS
(
Select '0000123' "BP_NUMBER", '1' "CONTRACT_TYPE" From Dual Union All
Select '0000123', '2' From Dual Union All
Select '0000123', '3' From Dual Union All
Select '0000123', '4' From Dual Union All
Select '0000124', '4' From Dual Union All
Select '0000124', '4' From Dual Union All
Select '0000124', '4' From Dual Union All
Select '0000125', '4' From Dual Union All
Select '0000126', '1' From Dual Union All
Select '0000126', '5' From Dual
)
Select
BP_NUMBER "BP_NUMBER",
Count(*) "OCCURENCIES"
From
tbl
WHERE CONTRACT_TYPE = '4'
GROUP BY BP_NUMBER
ORDER BY BP_NUMBER
--
-- R e s u l t :
--
-- BP_NUMBER OCCURENCIES
-- --------- -----------
-- 0000123 1
-- 0000124 3
-- 0000125 1

Related

Rank function with complex scenario

I have a TAble TABLE1 and having columns like ID, Status and Code
BAsed on the code priority i want the output , the priority is
SS -> RR - > TT - > AA ( these priority is not stored in any tables)
Query should first look for Approved status then we need to check for Code column
Example1:
ID: 2345 - This record having Approved status for all the codes like SS , AA and RR
and based on the code priority SS should be pulled in the output as 2345, SS
Example2:
ID: 3333- This record having Approved status for all the codes like RR and TT
and based on the code priority RR should be pulled in the output as 3333, RR
ID: 4444- Eventhough this record is having Codes like SS and RR but it is status column is having value as TERMED so we need to populate the next priority in the list and output should display as 4444 TT
ID: 5555- None of the status for this ID is having Approved status all are having status as Termed so based on the priority in the output 5555,SS should be picked as this one is the priority
so output for 2345 and 5555 is same only the difference is if none of the record having approved status then only we should go for Termed - if the record is only having termed then based on priority record should be pulled
Attached the picture for reference
You may use RANK along with a CASE expression for ordering:
WITH cte AS (
SELECT t.*,
RANK() OVER (PARTITION BY ID
ORDER BY CASE Status WHEN 'Approved' THEN 1
WHEN 'Termed' THEN 2
ELSE 3 END,
CASE Code WHEN 'SS' THEN 1
WHEN 'RR' THEN 2
WHEN 'TT' THEN 3
WHEN 'AA' THEN 4 END) rnk
FROM yourTable t
WHERE Status = 'Approved'
)
SELECT ID, Code
FROM cte
WHERE rnk = 1;
Demo
create table table1 (id, status, code) as
select 2345, 'Approved', 'SS' from dual union all
select 2345, 'Approved', 'AA' from dual union all
select 2345, 'Approved', 'RR' from dual union all
select 3333, 'Approved', 'RR' from dual union all
select 3333, 'Approved', 'TT' from dual union all
select 4444, 'TERMED', 'SS' from dual union all
select 4444, 'TERMED', 'RR' from dual union all
select 4444, 'Approved', 'TT' from dual
;
select ID, CODE
from (
select ID, STATUS, CODE
, row_number()over(
partition by ID
order by status
, decode(code, 'SS', 1, 'RR', 2, 'TT', 3, 'AA', 4) ) rank
from table1
)
where rank = 1
;

Generate random value from a list of values with even distribution in Oracle

I am trying to generate a random value from a list of valid values [1,2,3,4,5,9]. If I run my function 10 times, there are 2 values that never appear. I need every value in the list to be present in my sample. How can I ensure this ? An equal distribution would be good, but at least few rows of every value.
select random_code
from (
with temp_code_table as (
select '1' as random_code from dual union
select '2' as random_code from dual union
select '3' as random_code from dual union
select '4' as random_code from dual union
select '5' as random_code from dual union
select '9' as random_code from dual)
select random_code from temp_code_table order by dbms_random.value)
where rownum = 1;
I am running the above SQL inside a function, which checks that the random code generated is not the same as the original value.
Edit: Not sure the answer in the below post will help me achieve what I want.
Generating Random Value using CASE
Any advice on how to achieve this ?
"A random number" and "at least one of each" cannot be combined. The definition of random is that you down't know what you'll get... Roll a dice 10 times. Are you sure you'll have thrown all sides at least once ? No.
So to solve your problem, you can select one of each first - that way you have those already and then fill up the rest with randomly selected numbers from the set.
with temp_code_table (nr) as
(
SELECT '1' FROM dual UNION
SELECT '2' FROM dual UNION
SELECT '3' FROM dual UNION
SELECT '4' FROM dual UNION
SELECT '5' FROM dual UNION
SELECT '9' FROM dual
)
-- now select one of each. order doesn't matter, we're doing that at the end
, one_of_each AS
(
SELECT nr FROM temp_code_table
)
-- ok we got at least one of each. Now fill up the rest with randoms
-- generate a list of hundred random values 1,2,3,4,5,9 (100 is chosen randomly)
, hundred_random AS
(
SELECT CASE round(dbms_random.value(1,6))
WHEN 1 THEN '1'
WHEN 2 THEN '2'
WHEN 3 THEN '3'
WHEN 4 THEN '4'
WHEN 5 THEN '5'
WHEN 6 THEN '9'
END AS nr
FROM DUAL connect by LEVEL < 101
)
-- select 4 out of those.
, four_more (nr) AS
(
SELECT nr FROM hundred_random WHERE rownum < 5
)
-- put it all together
, ten_rows AS
(
SELECT nr FROM one_of_each
UNION ALL
SELECT nr FROM four_more
)
-- and shuffle...
SELECT nr FROM ten_rows order by dbms_random.value
;
The "temp_code_table" cte generates exactly 6 rows. That way you have at least one of each number in the set.
The "hundred_random" cte uses the case statement on the random number which will generate a single random value of the set. Then that is run 100 times using CONNECT BY LEVEL. Out of those 100, 4 are picked. Those 4 could all be 1 - this is random, there is no guarantee that you have distinct numbers.
At the end we union the 6 rows of the temp_code_table cte and the 4 rows of the four_more and order them randomly.

How to union a hardcoded row after each grouped result

After every group / row i want to insert a hardcoded dummy row with a bunch of 'xxxx' to act a separator.
I would like to use oracle sql to do this query. i can execute it using a loop but i don't want to use plsql.
As the others suggest, it is best to do it on the front end.
However, if you have a burning need to be done as a query, here is how.
Here I did not use the rownum function as you have already done. I assume, your data is returned by a query, and you can replace my table with your query.
I made few more assumptions, as you have data with row numbers in it.
[I am not sure what do you mean by not PL/SQL]
Select Case When MOD(rownm, 2) = 0 then ' '
Else to_char((rownm + 1) / 2) End as rownm,
name, total, column1
From
(
select (rownm * 2 - 1) rownm,name, to_char(total) total ,column1 from t
union
SELECT (rownm * 2) rownm,'XXX' name, 'XXX' total, 'The row act .... ' column1 FROM t
) Q
Order by Q.rownm;
and here is the fiddle
Since you're already grouping the data, it might be easier to use GROUPING SETS instead of a UNION.
Grouping sets let you group by multiple sets of columns, including the same set twice to duplicate rows. Then the GROUP_ID function can be used to determine when the fake values should be used. This code will be a bit smaller than a UNION approach, and should be faster since it doesn't need to reference the table multiple times.
select
case when group_id() = 0 then name else '' end name,
case when group_id() = 0 then sum(some_value) else null end total,
case when group_id() = 1 then 'this rows...' else '' end column1
from
(
select 'jack' name, 22 some_value from dual union all
select 'jack' name, 1 some_value from dual union all
select 'john' name, 44 some_value from dual union all
select 'john' name, 1 some_value from dual union all
select 'harry' name, 1 some_value from dual union all
select 'harry' name, 1 some_value from dual
) raw_data
group by grouping sets (name, name)
order by raw_data.name, group_id();
You can use row generator technique (using CONNECT BY) and then use CASE..WHEN as follows:
SQL> SELECT CASE WHEN L.LVL = 1 THEN T.ROWNM END AS ROWNM,
2 CASE WHEN L.LVL = 1 THEN T.NAME
3 ELSE 'XXX' END AS NAME,
4 CASE WHEN L.LVL = 1 THEN TO_CHAR(T.TOTAL)
5 ELSE 'XXX' END AS TOTAL,
6 CASE WHEN L.LVL = 1 THEN T.COLUMN1
7 ELSE 'This row act as separator..' END AS COLUMN1
8 FROM T CROSS JOIN (
9 SELECT LEVEL AS LVL FROM DUAL CONNECT BY LEVEL <= 2
10 ) L ORDER BY T.ROWNM, L.LVL;
ROWNM NAME TOTAL COLUMN1
---------- ---------- ----- ---------------------------
1 Jack 23
XXX XXX This row act as separator..
2 John 45
XXX XXX This row act as separator..
3 harry 2
XXX XXX This row act as separator..
4 roy 45
XXX XXX This row act as separator..
5 Jacob 26
XXX XXX This row act as separator..
10 rows selected.
SQL>

Excluding records from table based on rules from another table

I'm using Oracle SQL and I have a product table with diffrent attributes and sales volume for each product and another table with certain exclusion rules for different level of aggregation. Let's look at the example:
Here is our main table with sales data on which we want to perform some calculations:
And the other table contains diffrent rules which are supposed to exclude certain rows from table above:
When there is an "x", this column shouldn't be considered so our rules are:
1. exclude all rows with ATTR_3 = 'no'
2. exlcude all rows with ATTR_1 = 'Europe' and ATTR_2 = 'snacks' and ATTR_3 = 'no'
3. exlcude all rows with ATTR_1 = 'Africa'
And based on that our final output should be like that:
How this could be achived in SQL? I was thinking about join but I have no idea how to handle different levels of aggregation for exclusions.
I think your expected output is wrong. None of the rules excludes the 2nd row (Europe - snacks - yes).
SQL> with
2 -- sample data
3 test (product_id, attr_1, attr_2, attr_3) as
4 (select 81928 , 'Europe', 'beverages', 'yes' from dual union all
5 select 16534 , 'Europe', 'snacks' , 'yes' from dual union all
6 select 56468 , 'USA' , 'snacks' , 'no' from dual union all
7 select 129921, 'Africa', 'drinks' , 'yes' from dual union all
8 select 123021, 'Africa', 'snacks' , 'yes' from dual union all
9 select 165132, 'USA' , 'drinks' , 'yes' from dual
10 ),
11 rules (attr_1, attr_2, attr_3) as
12 (select 'x' , 'x' , 'no' from dual union all
13 select 'Europe', 'snacks', 'no' from dual union all
14 select 'Africa', 'x' , 'x' from dual
15 )
16 -- query you need
17 select t.*
18 from test t
19 where (t.attr_1, t.attr_2, t.attr_3) not in
20 (select
21 decode(r.attr_1, 'x', t.attr_1, r.attr_1),
22 decode(r.attr_2, 'x', t.attr_2, r.attr_2),
23 decode(r.attr_3, 'x', t.attr_3, r.attr_3)
24 from rules r
25 );
PRODUCT_ID ATTR_1 ATTR_2 ATT
---------- ------ --------- ---
81928 Europe beverages yes
16534 Europe snacks yes
165132 USA drinks yes
SQL>
You can use the join using CASE .. WHEN statement as follows:
SELECT P.*
FROM PRODUCT P
JOIN RULESS R ON
(R.ATTR_1 ='X' OR P.ATTR_1 <> R.ATTR_1)
AND (R.ATTR_2 ='X' OR P.ATTR_2 <> R.ATTR_2)
AND (R.ATTR_3 ='X' OR P.ATTR_3 <> R.ATTR_3)
You can use NOT EXISTS
SELECT *
FROM sales s
WHERE NOT EXISTS (
SELECT 0
FROM attributes a
WHERE ( ( a.attr_1 = s.attr_1 AND a.attr_1 IS NOT NULL )
OR a.attr_1 IS NULL )
AND ( ( a.attr_2 = s.attr_2 AND a.attr_2 IS NOT NULL )
OR a.attr_2 IS NULL )
AND ( ( a.attr_3 = s.attr_3 AND a.attr_3 IS NOT NULL )
OR a.attr_3 IS NULL )
)
where I considered the x values within the attributes table as NULL. If you really have x characters, then you can use :
SELECT *
FROM sales s
WHERE NOT EXISTS (
SELECT 0
FROM attributes a
WHERE ( ( NVL(a.attr_1,'x') = s.attr_1 AND NVL(a.attr_1,'x')!='x' )
OR NVL(a.attr_1,'x')='x' )
AND ( ( NVL(a.attr_2,'x') = s.attr_2 AND NVL(a.attr_2,'x')!='x' )
OR NVL(a.attr_2,'x')='x' )
AND ( ( NVL(a.attr_3,'x') = s.attr_3 AND NVL(a.attr_3,'x')!='x' )
OR NVL(a.attr_3,'x')='x' )
)
instead.
Demo
I would do this with three different not exists:
select p.*
from product p
where not exists (select 1
from rules r
where r.attr_1 = p.attr_1 and r.attr_1 <> 'x'
) and
not exists (select 1
from rules r
where r.attr_2 = p.attr_2 and r.attr_2 <> 'x'
) and
not exists (select 1
from rules r
where r.attr_3 = p.attr_3 and r.attr_3 <> 'x'
) ;
In particular, this can take advantage of indexes on (attr_1), (attri_2) and (attr_3) -- something that is quite handy if you have a moderate number of rules.

Keep order from 'IN' clause

Is it possible to keep order from a 'IN' conditional clause?
I found this question on SO but in his example the OP have already a sorted 'IN' clause.
My case is different, 'IN' clause is in random order
Something like this :
SELECT SomeField,OtherField
FROM TestResult
WHERE TestResult.SomeField IN (45,2,445,12,789)
I would like to retrieve results in (45,2,445,12,789) order. I'm using an Oracle database. Maybe there is an attribute in SQL I can use with the conditional clause to specify to keep order of the clause.
There will be no reliable ordering unless you use an ORDER BY clause ..
SELECT SomeField,OtherField
FROM TestResult
WHERE TestResult.SomeField IN (45,2,445,12,789)
order by case TestResult.SomeField
when 45 then 1
when 2 then 2
when 445 then 3
...
end
You could split the query into 5 queries union all'd together though ...
SELECT SomeField,OtherField
FROM TestResult
WHERE TestResult.SomeField = 4
union all
SELECT SomeField,OtherField
FROM TestResult
WHERE TestResult.SomeField = 2
union all
...
I'd trust the former method more, and it would probably perform much better.
Decode function comes handy in this case instead of case expressions:
SELECT SomeField,OtherField
FROM TestResult
WHERE TestResult.SomeField IN (45,2,445,12,789)
ORDER BY DECODE(SomeField, 45,1, 2,2, 445,3, 12,4, 789,5)
Note that value,position pairs (e.g. 445,3) are kept together for readability reasons.
Try this:
SELECT T.SomeField,T.OtherField
FROM TestResult T
JOIN
(
SELECT 1 as Id, 45 as Val FROM dual UNION ALL
SELECT 2, 2 FROM dual UNION ALL
SELECT 3, 445 FROM dual UNION ALL
SELECT 4, 12 FROM dual UNION ALL
SELECT 5, 789 FROM dual
) I
ON T.SomeField = I.Val
ORDER BY I.Id
There is an alternative that uses string functions:
with const as (select ',45,2,445,12,789,' as vals)
select tr.*
from TestResult tr cross join const
where instr(const.vals, ','||cast(tr.somefield as varchar(255))||',') > 0
order by instr(const.vals, ','||cast(tr.somefield as varchar(255))||',')
I offer this because you might find it easier to maintain a string of values rather than an intermediate table.
I was able to do this in my application using (using SQL Server 2016)
select ItemID, iName
from Items
where ItemID in (13,11,12,1)
order by CHARINDEX(' ' + Convert("varchar",ItemID) + ' ',' 13 , 11 , 12 , 1 ')
I used a code-side regex to replace \b (word boundary) with a space. Something like...
var mylist = "13,11,12,1";
var spacedlist = replace(mylist,/\b/," ");
Importantly, because I can in my scenario, I cache the result until the next time the related items are updated, so that the query is only run at item creation/modification, rather than with each item viewing, helping to minimize any performance hit.
Pass the values in via a collection (SYS.ODCINUMBERLIST is an example of a built-in collection) and then order the rows by the collection's order:
SELECT t.SomeField,
t.OtherField
FROM TestResult t
INNER JOIN (
SELECT ROWNUM AS rn,
COLUMN_VALUE AS value
FROM TABLE(SYS.ODCINUMBERLIST(45,2,445,12,789))
) i
ON t.somefield = i.value
ORDER BY rn
Then, for the sample data:
CREATE TABLE TestResult ( somefield, otherfield ) AS
SELECT 2, 'A' FROM DUAL UNION ALL
SELECT 5, 'B' FROM DUAL UNION ALL
SELECT 12, 'C' FROM DUAL UNION ALL
SELECT 37, 'D' FROM DUAL UNION ALL
SELECT 45, 'E' FROM DUAL UNION ALL
SELECT 100, 'F' FROM DUAL UNION ALL
SELECT 445, 'G' FROM DUAL UNION ALL
SELECT 789, 'H' FROM DUAL UNION ALL
SELECT 999, 'I' FROM DUAL;
The output is:
SOMEFIELD
OTHERFIELD
45
E
2
A
445
G
12
C
789
H
fiddle