Using CASE WHEN in the where condition - sql

I was wondering if anyone can help me write some code for the following logic.
We have a table
----------------
id, lang, letter
----------------
1 1 E
1 1 E
1 1 E
1 1 E
2 2 F
Problem:
I need to select ALL the rows for which the following condition fails:
id = lang (ie its either 1 or 2)
lang = 1 when letter = 'e' OR lang=2 when letter=2
I know I can hard code it. Also i would like to do this in ONE query only.
Please help

WHERE NOT
(
id = lang
AND
(
(lang = 1 AND letter = 'e')
OR (lang = 2 AND letter = '2')
)
)

select * from table
where id <> lang and
(lang<>1 and letter <> 'e' or
lang<>2 and letter <> '2')
assuming you mean you want all data where both of those conditions are false.

I think this is what you want to exclude the records meeting that criteria:
create table #t
(
id int,
lang int,
letter varchar(1)
)
insert into #t values (1, 1, 'E')
insert into #t values (1, 1, 'E')
insert into #t values (1, 1, 'E')
insert into #t values (1, 1, 'E')
insert into #t values (2, 2, 'F')
insert into #t values (1, 1, 'G')
insert into #t values (1, 1, 'H')
insert into #t values (1, 1, 'I')
insert into #t values (1, 1, 'J')
insert into #t values (2, 2, '2')
SELECT *
FROM #t
WHERE NOT
(
id = lang
AND
(
(
lang = 1
AND letter = 'E'
)
OR
(
lang = 2
AND letter = '2'
)
)
)
drop table #t
to get the records with that, just remove the NOT it:
SELECT *
FROM #t
WHERE
(
id = lang
AND
(
(
lang = 1
AND letter = 'E'
)
OR
(
lang = 2
AND letter = '2'
)
)
)

The idea here is that there are three business rules that may be implemented as three distinct tuple constraints (i.e. not false for every row in the table):
id and lang must be equal (begging the question, why not make one a computed column?).
If letter is 'E' then lang must be 1 (I assume there is a typo in your question where you said 'e' instead of 'E').
If letter is 'F' then lang must be 2 (I assume there is a typo in your question where you said 2 instead of 'F').
The constraints 'don't have anything to say' about any other data (e.g. when letter is 'X') and will allow this to pass.
All three tuple constraints can be written in conjunctive normal form as a constraint validation query:
SELECT * FROM T
WHERE id = lang
AND ( letter <> 'E' OR lang = 1 )
AND ( letter <> 'F' OR lang = 2 )
The data that violates the constraints can be simply shown (in pseudo relational algebra) as:
T MINUS (constraint validation query)
In SQL:
SELECT * FROM T
EXCEPT
SELECT * FROM T
WHERE id = lang
AND ( letter <> 'E' OR lang = 1 )
AND ( letter <> 'F' OR lang = 2 )
It is good to be able to rewrite predicates in case one's query of choice runs like glue on one's DBMS of choice! The above may be rewritten as e.g.
SELECT * FROM T
WHERE NOT ( id = lang
AND ( letter <> 'E' OR lang = 1 )
AND ( letter <> 'F' OR lang = 2 ) )
Applying rewrite laws (De Morgan's and double-negative) e.g.
SELECT * FROM T
WHERE id <> lang
OR ( letter = 'E' AND lang <> 1 )
OR ( letter = 'F' AND lang <> 2 )
Logically speaking, this should be better for the optimizer because for the above to be a contradiction every disjunct member must be false (put another way, it only takes one OR'ed clause to be true for the data to be deemed 'bad'). In practice (in theory?), the optimizer should be able to perform such rewrites anyhow!
p.s. nulls are bad for logic -- avoid them!
Here's my test code with sample data:
WITH Nums AS ( SELECT *
FROM ( VALUES (0), (1), (2) ) AS T (c) ),
Chars AS ( SELECT *
FROM ( VALUES ('E'), ('F'), ('X') ) AS T (c) ),
T AS ( SELECT N1.c AS id, N2.c AS lang,
C1.c AS letter
FROM Nums AS N1, Nums AS N2, Chars AS C1 )
SELECT * FROM T
EXCEPT
SELECT * FROM T
WHERE id = lang
AND ( letter <> 'E' OR lang = 1 )
AND ( letter <> 'F' OR lang = 2 );

Related

Remove banned words then collapse the data

Gets 'exact' banned words matched against a set of blog comments. It then creates a result set collapsing the banned words into the owner (the blog comment) showing the banned words found and the counts.
Any advice on how to do this in a more efficient manner - less DML - as there likely will be thousands of comments?
Banned words:
though
man
about
hear
Blog comments:
'There are many of us.'
'The man.'
'So glad to hear about.'
'So glad to hear about. A regular guy.'
'though, though, though.'
1st entry: word is NOT banned - it's a variant of a banned word. Entry is NOT to be selected.
2nd entry: 1 banned word. Entry selected as 1 row, 1 banned word, counted as 1 banned word.
3rd entry: 2 different banned words. Entry selected as 1 row, 2 banned words separate by
commas, counted as 2 banned words.
4th entry: 2 different banned words. Entry selected as 1 row, 2 banned words separate by
commas, counted as 2 banned words.
5th entry: 3 same banned words. Entry selected as 1 row, 1 banned word, counted as 3 banned
word.
Rules:
- Get the banned words in the blog comment.
- Only EXACT matches to the banned words. Do NOT include variants of the banned word.
- If there are more than 1 banned words in the same blog comment, only 1 row should be
generated.
- Generate the owner's row, include the banned words BannedWords column - non-unique banned
words separated by comma. 1 word for unique banned words.
Count the banned words and include that column in the generated row.
Desired Result - 4 rows:
BlogCommentId BannedWords t_Text1 t_Text2 t_Text3 CntOfBannedWords
2 man e f g 1
3 hear,about h i j 2
4 hear,about k l m 2
5 though n o p 3
The exact banned word matching code:
DECLARE #tableFinal TABLE (
t0_BlogCommentId int,
t0_Word VARCHAR(50),
t0_Text1 varchar(10),
t0_Text2 varchar(10),
t0_Text3 varchar(10),
t0_CntOfBannedWords int)
DECLARE #table1 TABLE (
t_BlogCommentId int,
t_Word VARCHAR(50),
t_Text1 varchar(10),
t_Text2 varchar(10),
t_Text3 varchar(10));
DECLARE #BlogComment TABLE (
BlogCommentId INT IDENTITY PRIMARY KEY,
BlogCommentContent VARCHAR(MAX),
Text1 varchar(10),
Text2 varchar(10),
Text3 varchar(10));
INSERT INTO #BlogComment
(BlogCommentContent
,Text1
,Text2
,Text3)
VALUES
('There are many of us.', 'a', 'b', 'c')
('The man.', 'e', 'f', 'g')
('So glad to hear about.', 'h', 'i', 'j')
('So glad to hear about. A regular guy.', 'k', 'l', 'm')
('though, though, though.', 'n', 'o', 'p');
DECLARE #BannedWords TABLE (
BannedWordsId INT IDENTITY PRIMARY KEY,
Word varchar(250));
INSERT INTO #BannedWords (Word) VALUES
('though'),
('man'),
('about'),
('hear');
;WITH rs AS
(
SELECT word = REPLACE(REPLACE([value],'.',''),',','')
,BlogCommentId
,Text1
,Text2
,Text3
FROM #BlogComment
CROSS APPLY STRING_SPLIT(BlogCommentContent, SPACE(1))
)
INSERT #table1
(t_Word,
t_BlogCommentId,
t_Text1,
t_Text2,
t_Text3 )
SELECT bw.Word
,rs.BlogCommentId
,rs.Text1
,rs.Text2
,rs.Text3
FROM rs
INNER JOIN #BannedWords bw ON rs.word = bw.Word;
Result from the WITH above before collapsing.
I want the 'Desired Result' to be generated here if possible in the WITH and not have to add the additional code below it.
SELECT *
FROM #table1
Results:
t_BlogCommentId t_BannedWords t_Text1 t_Text2 t_Text3
2 man e f g
3 about h i j
3 hear h i j
4 about k l m
4 hear k l m
5 though n o p
5 though n o p
5 though n o p
-- The 'additional code to collapse':
INSERT #tableFinal
(t0_BlogCommentId
,t0_Word
,t0_Text1
,t0_Text2
,t0_Text3
,t0_CntOfBannedWords )
SELECT DISTINCT t_BlogCommentId
,''
,''
,''
,''
,0
FROM #table1
UPDATE #tableFinal
SET t0_Word = t_Word
,t0_Text1 = t_Text1
,t0_Text2 = t_Text2
,t0_Text3 = t_Text3
FROM #table1
WHERE t0_BlogCommentId = t_BlogCommentId
UPDATE #tableFinal
SET t0_Word = t0_Word + ',' + t_Word
FROM #table1
WHERE t0_BlogCommentId = t_BlogCommentId AND t0_Word <> t_Word
UPDATE #tableFinal
SET t0_CntOfBannedWords = (SELECT Count (t_Word)
FROM #table1
WHERE t0_BlogCommentId = t_BlogCommentId)
Result of collapsing - now it's my 'Desired Result' - but more work and NOT likely suitable if there are a thousands plus comments:
SELECT t0_BlogCommentId as BlogCommentId
,t0_Word as BannedWords
,t0_Text1 as Text1
,t0_Text2 as Text2
,t0_Text3 as Text3
,t0_CntOfBannedWords as CntOfBannedWords
FROM #tableFinal
BlogCommentId BannedWords t_Text1 t_Text2 t_Text3 CntOfBannedWords
2 man e f g 1
3 hear,about h i j 2
4 hear,about k l m 2
5 though n o p 3
https://dbfiddle.uk/?rdbms=sqlserver_2017&fiddle=982989411c1a3e3fb784f1e0e46fd9e1
Excellent post. Thank you for providing all the data in an easy to use format. You were really close to having this all put together. You just needed that final piece to push the values back together. Here I used STUFF for this. I just started with your "rs" cte. I added another cte and then used the old STUFF trick. This produces the desired output for your sample data.
DECLARE #BlogComment TABLE (
BlogCommentId INT IDENTITY PRIMARY KEY,
BlogCommentContent VARCHAR(MAX),
Text1 varchar(10),
Text2 varchar(10),
Text3 varchar(10));
INSERT INTO #BlogComment
(BlogCommentContent
,Text1
,Text2
,Text3)
VALUES
('There are many of us.', 'a', 'b', 'c')
, ('The man.', 'e', 'f', 'g')
, ('So glad to hear about.', 'h', 'i', 'j')
, ('So glad to hear about. A regular guy.', 'k', 'l', 'm')
, ('though, though, though.', 'n', 'o', 'p');
DECLARE #BannedWords TABLE (
BannedWordsId INT IDENTITY PRIMARY KEY,
Word varchar(250));
INSERT INTO #BannedWords (Word) VALUES
('though'),
('man'),
('about'),
('hear');
;WITH rs AS
(
SELECT word = REPLACE(REPLACE([value],'.',''),',','')
,BlogCommentId
,Text1
,Text2
,Text3
FROM #BlogComment
CROSS APPLY STRING_SPLIT(BlogCommentContent, SPACE(1))
)
, ExpandedWords as
(
SELECT bw.Word
,rs.BlogCommentId
,rs.Text1
,rs.Text2
,rs.Text3
FROM rs
INNER JOIN #BannedWords bw ON rs.word = bw.Word
)
select BlogCommentId
, BannedWords = STUFF((select ', ' + e2.Word
from ExpandedWords e2
where e2.BlogCommentId = e1.BlogCommentId
--you could add an order by here if you want the list of words in a certain order.
FOR XML PATH('')), 1, 1, ' ')
, e1.Text1
, e1.Text2
, e1.Text3
, BannedWordsCount = count(*)
from ExpandedWords e1
group by e1.BlogCommentId
, e1.Text1
, e1.Text2
, e1.Text3
order by e1.BlogCommentId
For a more modern version of sql server using STRING_AGG is a bit less verbose and obtuse than using STUFF and FOR XML. Here is how that might look. I also use ROW_NUMBER here so you can only return a single instance of a banned word if it appears multiple times in the input. Same concept as above so this is starting a bit later in the code.
, ExpandedWords as
(
SELECT bw.Word
, rs.BlogCommentId
, rs.Text1
, rs.Text2
, rs.Text3
, RowNum = ROW_NUMBER()over(partition by rs.BlogCommentId, bw.Word order by (select newid())) --order doesn't really matter here
FROM rs
INNER JOIN #BannedWords bw ON rs.word = bw.Word
)
select e.BlogCommentId
, STRING_AGG(e.Word, ', ')
, e.Text1
, e.Text2
, e.Text3
, RowNum
from ExpandedWords e
where RowNum = 1
group by e.BlogCommentId
, e.Text1
, e.Text2
, e.Text3
, RowNum
order by e.BlogCommentId

value substitution/replacement in a string

I have a string x-y+z. The values for x, y and z will be stored in a table. Say
x 10
y 15
z 20
This string needs to be changed like 10-15+20.
Anyway I can achieve this using plsql or sql?
using simple Pivot we can do
DECLARE #Table1 TABLE
( name varchar(1), amount int )
;
INSERT INTO #Table1
( name , amount )
VALUES
('x', 10),
('y', 15),
('Z', 25);
Script
Select CAST([X] AS VARCHAR) +'-'+CAST([Y] AS VARCHAR)+'+'+CAST([Z] AS VARCHAR) from (
select * from #Table1)T
PIVOT (MAX(amount) FOR name in ([X],[y],[z]))p
An approach could be the following, assuming a table like this:
create table stringToNumbers(str varchar2(16), num number);
insert into stringToNumbers values ('x', 10);
insert into stringToNumbers values ('y', 20);
insert into stringToNumbers values ('zz', 30);
insert into stringToNumbers values ('w', 40);
First tokenize your input string with something like this:
SQL> with test as (select 'x+y-zz+w' as string from dual)
2 SELECT 'operand' as typ, level as lev, regexp_substr(string, '[+-]+', 1, level) as token
3 FROM test
4 CONNECT BY regexp_instr(string, '[a-z]+', 1, level+1) > 0
5 UNION ALL
6 SELECT 'value', level, regexp_substr(string, '[^+-]+', 1, level) as token
7 FROM test
8 CONNECT BY regexp_instr(string, '[+-]', 1, level - 1) > 0
9 order by lev asc, typ desc;
TYP LEV TOKEN
------- ---------- --------------------------------
value 1 x
operand 1 +
value 2 y
operand 2 -
value 3 zz
operand 3 +
value 4 w
In the example I used lowercase literals and only +/- signs; you can easily edit it to handle something more complex; also, I assume the input string is well-formed.
Then you can join your decoding table to the tokenized string, building the concatenation:
SQL> select listagg(nvl(to_char(num), token)) within group (order by lev asc, typ desc)
2 from (
3 with test as (select 'x+y-zz+w' as string from dual)
4 SELECT 'operand' as typ, level as lev, regexp_substr(string, '[+-]+', 1, level) as token
5 FROM test
6 CONNECT BY regexp_instr(string, '[a-z]+', 1, level+1) > 0
7 UNION ALL
8 SELECT 'value', level, regexp_substr(string, '[^+-]+', 1, level) as token
9 FROM test
10 CONNECT BY regexp_instr(string, '[+-]', 1, level - 1) > 0
11 order by lev asc, typ desc
12 ) tokens
13 LEFT OUTER JOIN stringToNumbers on (str = token);
LISTAGG(NVL(TO_CHAR(NUM),TOKEN))WITHINGROUP(ORDERBYLEVASC,TYPDESC)
--------------------------------------------------------------------------------
10+20-30+40
This assumes that every literal in you input string has a corrensponding value in table. You can even handle the case of strings with no corrensponding number, for example assigning 0:
SQL> select listagg(
2 case
3 when typ = 'operand' then token
4 else to_char(nvl(num, 0))
5 end
6 ) within group (order by lev asc, typ desc)
7 from (
8 with test as (select 'x+y-zz+w-UNKNOWN' as string from dual)
9 SELECT
.. ...
16 ) tokens
17 LEFT OUTER JOIN stringToNumbers on (str = token);
LISTAGG(CASEWHENTYP='OPERAND'THENTOKENELSETO_CHAR(NVL(NUM,0))END)WITHINGROUP(ORD
--------------------------------------------------------------------------------
10+20-30+40-0
Create a function like this:
create table ttt1
( name varchar(1), amount int )
;
INSERT INTO ttt1 VALUES ('x', 10);
INSERT INTO ttt1 VALUES ('y', 15);
INSERT INTO ttt1 VALUES ('z', 25);
CREATE OR REPLACE FUNCTION replace_vars (in_formula VARCHAR2)
RETURN VARCHAR2
IS
f VARCHAR2 (2000) := UPPER (in_formula);
BEGIN
FOR c1 IN ( SELECT UPPER (name) name, amount
FROM ttt1
ORDER BY name DESC)
LOOP
f := REPLACE (f, c1.name, c1.amount);
END LOOP;
return f;
END;
select replace_vars('x-y+z') from dual
Here's another way to approach the problem that attempts to do it all in SQL. While not necessarily the most flexible or fastest, maybe you can get some ideas from another way to approach the problem. It also shows a way to execute the final formula to get the answer. See the comments below.
Assumes all variables are present in the variable table.
-- First build the table that holds the values. You won't need to do
-- this if you already have them in a table.
with val_tbl(x, y, z) as (
select '10', '15', '20' from dual
),
-- Table to hold the formula.
formula_tbl(formula) as (
select 'x-y+z' from dual
),
-- This table is built from a query that reads the formula a character at a time.
-- When a variable is found using the case statement, it is queried in the value
-- table and it's value is returned. Otherwise the operator is returned. This
-- results in a row for each character in the formula.
new_formula_tbl(id, new_formula) as (
select level, case regexp_substr(formula, '(.|$)', 1, level, NULL, 1)
when 'x' then
(select x from val_tbl)
when 'y' then
(select y from val_tbl)
when 'z' then
(select z from val_tbl)
else regexp_substr(formula, '(.|$)', 1, level, NULL, 1)
end
from formula_tbl
connect by level <= regexp_count(formula, '.')
)
-- select id, new_formula from new_formula_tbl;
-- This puts the rows back into a single string. Order by id (level) to keep operands
-- and operators in the right order.
select listagg(new_formula) within group (order by id) formula
from new_formula_tbl;
FORMULA
----------
10-15+20
Additionally you can get the result of the formula by passing the listagg() call to the following xmlquery() function:
select xmlquery(replace( listagg(new_formula) within group (order by id), '/', ' div ')
returning content).getNumberVal() as result
from new_formula_tbl;
RESULT
----------
15

Matching multiple key/value pairs in SQL

I have metadata stored in a key/value table in SQL Server. (I know key/value is bad, but this is free-form metadata supplied by users, so I can't turn the keys into columns.) Users need to be able to give me an arbitrary set of key/value pairs and have me return all DB objects that match all of those criteria.
For example:
Metadata:
Id Key Value
1 a p
1 b q
1 c r
2 a p
2 b p
3 c r
If the user says a=p and b=q, I should return object 1. (Not object 2, even though it also has a=p, because it has b=p.)
The metadata to match is in a table-valued sproc parameter with a simple key/value schema. The closest I have got is:
select * from [Objects] as o
where not exists (
select * from [Metadata] as m
join #data as n on (n.[Key] = m.[Key])
and n.[Value] != m.[Value]
and m.[Id] = o.[Id]
)
My "no rows exist that don't match" is an attempt to implement "all rows match" by forming its contrapositive. This does eliminate objects with mismatching metadata, but it also returns objects with no metadata at all, so no good.
Can anyone point me in the right direction? (Bonus points for performance as well as correctness.)
; WITH Metadata (Id, [Key], Value) AS -- Create sample data
(
SELECT 1, 'a', 'p' UNION ALL
SELECT 1, 'b', 'q' UNION ALL
SELECT 1, 'c', 'r' UNION ALL
SELECT 2, 'a', 'p' UNION ALL
SELECT 2, 'b', 'p' UNION ALL
SELECT 3, 'c', 'r'
),
data ([Key], Value) AS -- sample input
(
SELECT 'a', 'p' UNION ALL
SELECT 'b', 'q'
),
-- here onwards is the actual query
data2 AS
(
-- cnt is to count no of input rows
SELECT [Key], Value, cnt = COUNT(*) OVER()
FROM data
)
SELECT m.Id
FROM Metadata m
INNER JOIN data2 d ON m.[Key] = d.[Key] AND m.Value= d.Value
GROUP BY m.Id
HAVING COUNT(*) = MAX(d.cnt)
The following SQL query produces the result that you require.
SELECT *
FROM #Objects m
WHERE Id IN
(
-- Include objects that match the conditions:
SELECT m.Id
FROM #Metadata m
JOIN #data d ON m.[Key] = d.[Key] AND m.Value = d.Value
-- And discount those where there is other metadata not matching the conditions:
EXCEPT
SELECT m.Id
FROM #Metadata m
JOIN #data d ON m.[Key] = d.[Key] AND m.Value <> d.Value
)
Test schema and data I used:
-- Schema
DECLARE #Objects TABLE (Id int);
DECLARE #Metadata TABLE (Id int, [Key] char(1), Value char(2));
DECLARE #data TABLE ([Key] char(1), Value char(1));
-- Data
INSERT INTO #Metadata VALUES
(1, 'a', 'p'),
(1, 'b', 'q'),
(1, 'c', 'r'),
(2, 'a', 'p'),
(2, 'b', 'p'),
(3, 'c', 'r');
INSERT INTO #Objects VALUES
(1),
(2),
(3),
(4); -- Object with no metadata
INSERT INTO #data VALUES
('a','p'),
('b','q');

How to have AND & OR condition in SQL Merge statement for DB2

I have a merge statement which works when I did not have to consider null values :
This works :
MERGE INTO LTABLE L
USING (SELECT 1392 UCL_USER_ID,11 REGISTER_ID ,5 REGION_ID FROM DUAL ) B
ON ( L.UCL_USER_ID = B.UCL_USER_ID
AND L.REGISTER_ID = B.REGISTER_ID
AND (L.REGION_ID = B.REGION_ID)
)
WHEN NOT MATCHED
THEN
INSERT (
L.LTABLE_ID
,L.UCL_USER_ID
,L.REGISTER_ID
,L.REGION_ID
)
VALUES (
SEQ_LTABLE_ID.NEXTVAL
,1392
,11
,5);
When I have to consider null values for REGION_ID the below works :
MERGE INTO LTABLE L
USING (SELECT 1392 UCL_USER_ID,11 REGISTER_ID ,NULL REGION_ID FROM DUAL ) B
ON ( L.UCL_USER_ID = B.UCL_USER_ID
AND L.REGISTER_ID = B.REGISTER_ID
AND (L.REGION_ID IS NULL AND B.REGION_ID IS NULL)
)
WHEN NOT MATCHED
THEN
INSERT (
L.LTABLE_ID
,L.UCL_USER_ID
,L.REGISTER_ID
,L.REGION_ID
)
VALUES (
SEQ_LTABLE_ID.NEXTVAL
,1392
,11
,NULL);
Question is how can I combine these two conditions when it can be null or some numeric value. I tried the below but sql developer gives the error that query is not right.
AND ((L.REGION_ID = B.REGION_ID) OR (L.REGION_ID IS NULL AND B.REGION_ID IS NULL))
ERROR :
SQL Error: DB2 SQL Error: SQLCODE=-418, SQLSTATE=42610, SQLERRMC=FCS already resolved to different type, DRIVER=4.17.29
This is just a guess, but perhaps the problem is the default type for NULL. Perhaps a cast will fix the problem:
MERGE INTO LTABLE L
USING (SELECT 1392 UCL_USER_ID, 11 as REGISTER_ID,
CAST(NULL as VARCHAR(255)) as REGION_ID
FROM DUAL ) B
ON ( L.UCL_USER_ID = B.UCL_USER_ID
AND L.REGISTER_ID = B.REGISTER_ID
AND (L.REGION_ID IS NULL AND B.REGION_ID IS NULL)
)
Or whatever the appropriate type is for REGION_ID.

Reordering output to predefined sequence

I am trying to get output from a table sorted in a predefined sequence of 5 alphabet.
i.e. L > C > E > O > A
by using order by I cant get the desired result. I am using SQL server db.
Can any one please suggest me if I can define a sequence inside a query ?
SO that I get my result in L > C > E > O > A.
Thanks in Advance.
select * from your_table
order by case when some_column = 'L' then 1
when some_column = 'C' then 2
when some_column = 'E' then 3
when some_column = 'O' then 4
when some_column = 'A' then 5
end desc
If you want to use those sorting criteria for two or more queries then you can create a table for this:
CREATE TABLE dbo.CustomSort (
Value VARCHAR(10) PRIMARY KEY,
SortOrder INT NOT NULL
);
GO
INSERT INTO dbo.CustomSort (Value, SortOrder) VALUES ('L', 1);
INSERT INTO dbo.CustomSort (Value, SortOrder) VALUES ('C', 2);
INSERT INTO dbo.CustomSort (Value, SortOrder) VALUES ('E', 3);
INSERT INTO dbo.CustomSort (Value, SortOrder) VALUES ('O', 4);
INSERT INTO dbo.CustomSort (Value, SortOrder) VALUES ('A', 5);
GO
and then you can join the source table (x in this example) with dbo.CustomSort table thus:
SELECT x.Col1
FROM
(
SELECT 'E' UNION ALL
SELECT 'C' UNION ALL
SELECT 'O'
) x(Col1) INNER JOIN dbo.CustomSort cs ON x.Col1 = cs.Value
ORDER BY cs.SortOrder
/*
Col1
----
C
E
O
*/
I you update the dbo.CustomSort table then all queries will use the new sorting criteria.