sql ORDER BY multiple values in specific order? - sql

Ok I have a table with a indexed key and a non indexed field.
I need to find all records with a certain value and return the row.
I would like to know if I can order by multiple values.
Example:
id x_field
-- -----
123 a
124 a
125 a
126 b
127 f
128 b
129 a
130 x
131 x
132 b
133 p
134 p
135 i
pseudo: would like the results to be ordered like this, where ORDER BY x_field = 'f', 'p', 'i', 'a'
SELECT *
FROM table
WHERE id NOT IN (126)
ORDER BY x_field 'f', 'p', 'i', 'a'
So the results would be:
id x_field
-- -----
127 f
133 p
134 p
135 i
123 a
124 a
125 a
129 a
The syntax is valid but when I execute the query it never returns any results, even if I limit it to 1 record. Is there another way to go about this?
Think of the x_field as test results and I need to validate all the records that fall in the condition. I wanted to order the test results by failed values, passed values. So I could validate the failed values first and then the passed values using the ORDER BY.
What I can't do:
GROUP BY, as I need to return the specific record values
WHERE x_field IN('f', 'p', 'i', 'a'), I need all the values as I'm trying to use one query for several validation tests. And x_field values are not in DESC/ASC order
After writing this question I'm starting to think that I need to rethink this, LOL!

...
WHERE
x_field IN ('f', 'p', 'i', 'a') ...
ORDER BY
CASE x_field
WHEN 'f' THEN 1
WHEN 'p' THEN 2
WHEN 'i' THEN 3
WHEN 'a' THEN 4
ELSE 5 --needed only is no IN clause above. eg when = 'b'
END, id

Try:
ORDER BY x_field='f', x_field='p', x_field='i', x_field='a'
You were on the right track, but by putting x_field only on the 'f' value, the other three were treated as constants and not compared against anything in the dataset.

You can use a LEFT JOIN with a "VALUES ('f',1),('p',2),('a',3),('i',4)" and use the second column in your order-by expression. Postgres will use a Hash Join which will be much faster than a huge CASE if you have a lot of values. And it is easier to autogenerate.
If this ordering information is fixed, then it should have its own table.

I found a much cleaner solution for this:
ORDER BY array_position(ARRAY['f', 'p', 'i', 'a']::varchar[], x_field)
Note: array_position needs Postgres v9.5 or higher.

Use a case switch to translate the codes into numbers that can be sorted:
ORDER BY
case x_field
when 'f' then 1
when 'p' then 2
when 'i' then 3
when 'a' then 4
else 5
end

The CASE and ORDER BY suggestions should all work, but I'm going to suggest a horse of a different color. Assuming that there are only a reasonable number of values for x_field and you already know what they are, create an enumerated type with F, P, A, and I as the values (plus whatever other possible values apply). Enums will sort in the order implied by their CREATE statement. Also, you can use meaninful value names—your real application probably does and you have just masked them for confidentiality—without wasted space, since only the ordinal position is stored.

For someone who is new to ORDER BY with CASE this may be useful
ORDER BY
CASE WHEN GRADE = 'A' THEN 0
WHEN GRADE = 'B' THEN 1
ELSE 2 END

#bobflux's answer is great. I would like to extend it by adding a complete query that uses proposed approach.
select tt.id, tt.x_field
from target_table as tt
-- Here we join our target_table with order_table to specify custom ordering.
left join
(values ('f', 1), ('p', 2), ('i', 3), ('a', 4)) as order_table (x_field, order_num)
on order_table.x_field = tt.x_field
order by
order_table.order_num, -- Here we order values by our custom order.
tt.x_field; -- Other values can be ordered alphabetically, for example.
Here is complete demo.

Since i don't have enough reputation to write as a comment, added this as a new answer.
You can add asc or desc to order by clause.
ORDER BY x_field='A' ASC, x_field='I' DESC, x_field='P' DESC, x_field='F' ASC
which makes I first, P second and A as last one and F before the last.

You can order by a selected column or other expressions.
Here an example, how to order by the result of a case-statement:
SELECT col1
, col2
FROM tbl_Bill
WHERE col1 = 0
ORDER BY -- order by case-statement
CASE WHEN tbl_Bill.IsGen = 0 THEN 0
WHEN tbl_Bill.IsGen = 1 THEN 1
ELSE 2 END
The result will be a List starting with "IsGen = 0" rows, followed by "IsGen = 1" rows and all other rows a the end.
You could add more order-parameters at the end:
SELECT col1
, col2
FROM tbl_Bill
WHERE col1 = 0
ORDER BY -- order by case-statement
CASE WHEN tbl_Bill.IsGen = 0 THEN 0
WHEN tbl_Bill.IsGen = 1 THEN 1
ELSE 2 END,
col1,
col2

if you are using MySQL 4.0 afterwards, consider using FIELD() . It returns the index position of the first argument through the next arguments and it is case-sensitive.
ORDER BY FIELD(x_field, 'f', 'p', 'i', 'a')

you can use position(text in text) in order by for ordering the sequence

Related

SQL: How to get max over column but exclude certain values?

I have a numeric column and all I want is the maximum value in that column that does NOT exceed a certain number. I am doing this along with a group by statement, using the MAX function.
So basically if for each group, the column is 1, 2, 3, 4, 5, and I want the maximum that does not exceed 4, then in this case, the maximum for this group would be 4.
However, if the column equals 5, 6, 7, 8, then since all values exceed 4, I … actually don't care, this won't end up being displayed, so just return anything.
How do I do this? Using SQL/Oracle.
You can use conditional aggregation as follows:
Select case when count(case when col > 4 then 1 end) = count(*)
then max(col)
else max(case when col <= 4 then col end)
end as res_
From your_table t
select max(case when col>4 then 0 else col end) from table1
If that is all that the query needs to do, it is best to filter the rows with col > 4 before aggregation, in a where clause. This will reduce the amount of work done by the aggregation itself, which is the most expensive operation in the whole query.
As a side effect, "groups" where all values in the column are > 4 will not be included at all in the output. For some reporting tasks this would be a problem, but you said in your case you wouldn't show anything in the output for those groups anyway.
So, you could do something like this:
select agg_col1, agg_col2, ..., max(col) as max_col_up_to_4
from your_table
WHERE col <= 4 -- DO THE FILTERING HERE!
group by agg_col1, agg_col2, ...
;
(Here agg_col1, agg_col2, ... are, obviously, the columns by which you group for your aggregation.)

SQL - Tags searching query

I have following table in my database:
ID name
1 x
2 x
3 y
1 y
1 z
Now I want to select only this objects (ID's) which has both 'x' and 'y' value s tag name. In this case this will be only record with ID = 1 because sought values set ('x' and 'y') is subset of this record possible names set ('x', 'y' and 'z').
How to write a SQL query?
Thanks for help :)
One method uses aggregation:
select id
from t
where name in ('x', 'y')
group by id
having count(*) = 2;
If you care about performance you might want to compare this to:
select id
from t tx join
t ty
on tx.id = ty.id and tx.name = 'x' and ty.name = 'y';
The first version is easier to generalize to more tags. Under some circumstances, the second might have better performance.

What SQL query can answer "Do these rows exist?"

Here is the code to create the database:
CREATE TABLE foo (
id TEXT PRIMARY KEY,
value TEXT
);
INSERT INTO foo VALUES(1, 10), (2, 20), (3, 30), (5, 50);
Now I have a set of rows and I want back 0 if the row doesnt exist, 1 if the row exists but is not the same, and 2 if the row exists exactly.
So the result of the query on (1, 11), (2, 20), (4, 40) should be 1, 2, 0.
The reason I want this is to know what query to use to insert the data into the database. If it is a 0, I do a normal insert, if it is a 1 I do an update, and if it is a 2 I skip the row. I know that INSERT OR REPLACE will result in nearly the same rows, but the problem is that it doesnt trigger the correct triggers (it will always trigger an on insert trigger instead of an update trigger or no trigger if the row exists exactly).
Also, I want to do one query with all of the rows, not one query per row.
The idea is to use an aggregation query. Count the number of times that the id matches. If there are none, then return 0. Then check the value to distinguish between 1 and 2:
select (case when max(id = 1) = 0 then 0
when max(id = 1 and value = 11) = 0 then 1
else 2
end) as flag
from table t;
You need to plug the values into the query.
EDIT:
If you want to match a bunch of rows, do something like this:
select testvalue.id,
(case when max(t.id = testvalue.id) = 0 then 0
when max(t.id = testvalue.id and t.value = testvalue.value) = 0 then 1
else 2
end) as flag
from table t cross join
(select 1 as id 10 as value union all
select 2, 20 union all
select 4, 40
) as testvalues
group by testvalues.id;
You can use the EXISTS argument in Transact-SQL. MSDN Documentation.
This returns true if a row exists. You can then use an If statement within that to check if the row is the same or different, and if true, use the RETURN argument with your specified values. MSDN Documentation.
This is based off of Gordon Linoff's answer so upvote him. I just wanted to share what I actually went with:
select testvalues.id,
(case when t.id != testvalues.id then 0
when t.value != testvalues.value then 1
else 2
end) as flag
from (select 1 as id, 11 as entity union all
select 2, 20 union all
select 4, 40
) as testvalues
LEFT OUTER JOIN foo t on testvalues.id=t.id
This prevents the full memory usage of a cross join and group by clauses.

oracle adding, then avg with null

I have sql like:
select avg(decode(type, 'A', value, null) + decode(type, 'B', value, null)) from table;
The problem with this is some of these types can be null, so the addition part will result in null because adding anything to null makes it null. So you might think I could change the decode from null to 0, but that seems to make the avg() count it as part of it's averaging, but it shouldn't/I don't want it counted as part of the average.
Ideally the addition would just ignore the nulls and just not try to add them to the rest of the values.
So let's say my numbers are:
5 + 6 + 5
3 + 2 + 1
4 + null + 2
They total 28 and I'd want to divide by 8 (ignore the null), but if I change the null to 0 in the decode, the avg will then divide by 9 which isn't what I want.
As written, your code should always return null, since if the first decode returns value, then the second decode must always return null. I'm going to assume that you made an error in genericizing your code and that what you really meant was this:
avg(decode(type1, 'A', value1, null) + decode(type2, 'B', value2, null))
(Or, instead of type1, it could be a.type. The point is that the fields in the two decodes are meant to be separate fields)
In this case, I think the easisest thing to do is check for nulls first:
avg(case when type1 is null and type2 is null then null
else case type1 when 'A' then value1 else 0 end
+ case type2 when 'B' then value2 else 0 end
end)
(I replaced decode with case because I find it easier to read, but, in this case decode would work just as well.)
This is overcomplicated to do a sum here. Juste output the values with a CASE, and you are done.
SELECT AVG(
CASE WHEN type = 'A' OR type = 'B'
THEN value
ELSE null
END
)
FROM table
A simple workaround would be to calculate the average yourself:
select
-- The sum of all values with type 'A' or 'B'
sum(decode(type, 'A', value, 'B', value, 0)) /
-- ... divided by the "count" of all values with type 'A' or 'B'
sum(decode(type, 'A', 1, 'B', 1, 0))
from table;
A SQLFiddle example
But the way AVG() works, it would probably be sufficient, if you just removed the addition and put everything in a single DECODE()
select avg(decode(type, 'A', value, 'B', value, null)) from table
The logic here is a bit complicated:
select avg((case when type = 'A' then value else 0 end) + (case when type = 'B' then value else 0 end))
from table
where type in ('A', 'B')
The where clause guarantees that you have at least one "A" or "B". The problem is arising when you have no examples of "A" or "B".

Ordering Select clause result in specific way

I need help with writing a select clause query.
For example, lets say I have a query like that:
select value from some_table order by value asc;
as a result I get this:
1
2
3
4
5
6
7
8
9
10
but a special query I want to write, is the one which still will give me sorted values, but will put 5 after 8.
this means I need one value to be out of regular order.
it can be described in other way. lets say I have two groups of numbers (example):
A={ a | 1<=a<=118, a!=78 } B={ b | b>118 }
I have a group C=A U B U {78}
and I need all these values sorted like "A,78,B"
Assuming value is integer, you could do this:
SELECT *
FROM tbl
ORDER BY
CASE
WHEN value = 5 THEN 8.5
ELSE value
END
Or to expand upon DCP's answer...
SELECT *
FROM tbl
ORDER BY
CASE
WHEN (Condition for first grouping) THEN 1
WHEN (Condition for second grouping) THEN 2
WHEN (Condition for third grouping) THEN 3
ELSE 4
END
You can use multiple conditions in your order by:
ORDER BY (value BETWEEN 1 AND 118) AND value != 78 DESC,
value > 118 DESC,
value
This will ensure that values which match the first predicate come first, then values matching the second predicate, and finally values matching none of the predicates. If there is a tie (two numbers matching the same predicate) then these numbers are sorted in ascending order.
Note that I haven't tested this in Oracle. It might be necessary to wrap the predicate in a CASE expression (CASE WHEN predicate THEN 1 ELSE 0 END) to get the sorting to work in Oracle.
ORDER BY
(CASE WHEN ((value BETWEEN 1 AND 118) AND value <> 78) THEN 1 ELSE 0 END) DESC,
(CASE WHEN (value > 118) THEN 1 ELSE 0 END) DESC,
value
Order by some CASE-expression to remap your values.