Concatenated field conversion error when executing a view in Oracle - html

I have a view (DB_ADMIN.VW_DBA_MONITOR_CURRENTLYEXEC) to be able to visualize the transactions in Oracle. This view makes use of those already existing in the system: v$sql, V$SQLSTATS, v$sqlarea and gv$process.
And the structure and type of columns was as follows:
I have a procedure that attempts to use such a view, format the output to a single HTML string under certain conditions, and send it via email. Here is a simple summary of that procedure:
DECLARE
V_BODY_TEXT CLOB;
BEGIN
select utl_i18n.unescape_reference(xmlagg(xmlelement(e,'
<tr>
<td>' || ltrim(rtrim(TO_CLOB(SID))) || '</th>
<td>' || ltrim(rtrim(TO_CLOB(SQL_ID))) || '</th>
<td>' || ltrim(rtrim(TO_CLOB(USERNAME))) || '</th>
<td>' || ltrim(rtrim(TO_CLOB(ONAME))) || '</th>
<td>' || ltrim(rtrim(TO_CLOB(MACHINE))) || '</th>
<td>' || ltrim(rtrim(TO_CLOB(WAIT_CLASS))) || '</th>
<td>' || ltrim(rtrim(TO_CLOB(EVENT))) || '</th>
<td>' || ltrim(rtrim(TO_CLOB(MODULE))) || '</th>
<td>' || ltrim(rtrim(TO_CLOB(LOGON_TIME))) || '</th>
<td>' || ltrim(rtrim(TO_CLOB(START_TIME))) || '</th>
<td>' || ltrim(rtrim(TO_CLOB(TIME))) || '</th>
<td>' || ltrim(rtrim(TO_CLOB(SQL_TEXT))) || '</th>
</tr>
','').extract('//text()')).getclobval()) x
INTO V_BODY_TEXT
FROM DB_ADMIN.VW_DBA_MONITOR_CURRENTLYEXEC WHERE "TIME" > 300;
END;
The error presented by the execution of the above code is because the concatenated result only admits 4000 characters and it doesn't matter if I try to cast (TO_CLOB) field by field or the entire concatenated string, I still get the same error every time. result size is exceeded:
About the above code, the use of the TO_CLOB function, is my last attempt to try to solve the problem.
Said error does not allow any "substring" or "len" type action to be applied to it either.
I've also tried creating a table with all the fields of type CLOB, then inserting the result of the view, and using the above procedure now with the table, but I keep getting the same error every time the generated html code exceeds 4000 characters.
Therefore, I go to this community to see what other options they give me to get out of the problem.

The xmlelement function only accepts up to 4000 characters if the initialization parameter MAX_STRING_SIZE = STANDARD, and 32767 characters if MAX_STRING_SIZE = EXTENDED, so there's no point in converting to CLOB that early in your process. See here.
Also, the utl_i18n.unescape_reference function only accepts VARCHAR2 input, not CLOB. See here. This is the most likely place your query is failing.
Try this, working exclusively with VARCHAR2, doing explicit conversions from NUMBER and DATE types, and saving the aggregation into the largest string to the very end:
DECLARE
V_BODY_TEXT CLOB;
BEGIN
select xmlagg(utl_i18n.unescape_reference(xmlelement(e,'
<tr>
<td>' || TO_CHAR(SID) || '</th>
<td>' || ltrim(rtrim(SQL_ID)) || '</th>
<td>' || ltrim(rtrim(USERNAME)) || '</th>
<td>' || ltrim(rtrim(ONAME)) || '</th>
<td>' || ltrim(rtrim(MACHINE)) || '</th>
<td>' || ltrim(rtrim(WAIT_CLASS)) || '</th>
<td>' || ltrim(rtrim(EVENT)) || '</th>
<td>' || ltrim(rtrim(MODULE)) || '</th>
<td>' || TO_CHAR(LOGON_TIME,'DD-MON-YYYY') || '</th>
<td>' || TO_CHAR(START_TIME,'DD-MON-YYYY') || '</th>
<td>' || TO_CHAR(TIME) || '</th>
<td>' || ltrim(rtrim(TO_CLOB(SQL_TEXT))) || '</th>
</tr>
',''))) x
INTO V_BODY_TEXT
FROM DB_ADMIN.VW_DBA_MONITOR_CURRENTLYEXEC WHERE "TIME" > 300;
END;

XMLType methods are deprecated since Oracle 11gr2, use modern (though, it's hard to tell about XML) SQL functions to access XML data without additional serialization/deserialization issues:
XMLQUERY for extraction.
XMLCAST for deserialization. It does decoding of HTML/XML stuff like & etc to their text values.
Below is an example of three characters padded with zeroes up to 5000 bytes (so resulting in too large value for varchar2)
with t(col) as (
select
xmlelement(e,
to_clob(rpad('<>&', 4000, '0'))
|| to_clob(rpad('0', 1000, '0'))
)
from dual
)
, deser as (
select
xmlcast(xmlquery(
'//text()'
passing col
returning content
) as clob) as res
from t
)
select
length(res),
trim(trailing '0' from res) as res
from deser
LENGTH(RES)
RES
5000
<>&
fiddle
However, you may avoid concatenation at all and serialize SQL result directly into XML with dbms_xmlgen package. Below is an example:
with function f_get_html(
p_rows int
)
return clob
as
l_ctx number;
l_cursor sys_refcursor;
l_res clob;
begin
/* Cursor object is used to be able to use parameters for statement.
In case of static statement may be replaced by string literal
containing SQL statement directly in newcontext call
*/
open l_cursor for
select
dummy as "td", level as "td",
'<>&' as "td"
from dual
connect by level < p_rows
;
/*Use context to set row tags to <tr>*/
l_ctx := dbms_xmlgen.newcontext(
l_cursor
);
dbms_xmlgen.setrowtag(l_ctx, 'tr');
/*Extract content as HTML-formatted clob*/
select xmlserialize(content xmlquery(
'/ROWSET/*'
passing dbms_xmlgen.getxmltype(l_ctx)
returning content
) as clob)
into l_res
from dual;
return l_res;
end;
select f_get_html(4) as res_html
from dual
RES_HTML
<tr><td>X</td><td>1</td><td><>&</td></tr><tr><td>X</td><td>2</td><td><>&</td></tr><tr><td>X</td><td>3</td><td><>&</td></tr>
fiddle

Finally, the solution I found to avoid the various restrictions of the functions used to generate xml code or converters like the utl_i18n.unescape_reference function, was to use a loop with FOR:
DECLARE
V_BODY CLOB;
V_TABLE_BODY CLOB := NULL;
BEGIN
--> DataSource:
FOR r IN (
SELECT SID,
SQL_ID,
USERNAME,
ONAME,
MACHINE,
WAIT_CLASS,
EVENT,
MODULE,
LOGON_TIME,
START_TIME,
TIME,
SQL_TEXT
FROM DB_ADMIN.VW_DBA_MONITOR_CURRENTLYEXEC
WHERE "TIME" > 300
)
LOOP
--> Table Body:
V_TABLE_BODY := V_TABLE_BODY ||
'<tr>'||
'<td>'||trim(r.SID)||'</td>' ||
'<td>'||trim(r.SQL_ID)||'</td>' ||
'<td>'||trim(r.USERNAME)||'</td>' ||
'<td>'||trim(r.ONAME)||'</td>' ||
'<td>'||trim(r.MACHINE)||'</td>' ||
'<td>'||trim(r.WAIT_CLASS)||'</td>' ||
'<td>'||trim(r.EVENT)||'</td>' ||
'<td>'||trim(r.MODULE)||'</td>' ||
'<td>'||trim(r.LOGON_TIME)||'</td>' ||
'<td>'||trim(r.START_TIME)||'</td>' ||
'<td>'||trim(r.TIME)||'</td>' ||
'<td>'||trim(r.SQL_TEXT)||'</td>' ||
'</tr>';
END LOOP;
----
IF V_TABLE_BODY IS NOT NULL THEN
--> HTML Body:
V_BODY :=
'
<body>
<table border="1">
<tr>
<th>SID</th>
<th>SQL_ID</th>
<th>USERNAME</th>
<th>ONAME</th>
<th>MACHINE</th>
<th>WAIT_CLASS</th>
<th>EVENT</th>
<th>MODULE</th>
<th>LOGON_TIME</th>
<th>START_TIME</th>
<th>TIME</th>
<th>SQL_TEXT</th>
</tr>' ||
V_TABLE_BODY || --> Table Body
'</table>
</body>
';
-- Send Mail:
DB_ADMIN.USP_SEND_MAIL(V_BODY); --> Referential
END IF;

Related

Alternate of Varchar2?

I am getting data in Refcursor and binding in HTML table using for loop to send an email. When I am executive my procedure then it is giving me error of "numeric or value error: character string buffer too small". I have tried CLOB but still not get any success.
Error :-
ORA-06502: PL/SQL: numeric or value error: character string buffer too small
ORA-06512: at "XXCOMM.XX_EMS_MAIL_PRC_MAINTENANCE", line 113
ORA-06512: at line 6
Below id my procedure in which I am trying to send email.
create or replace PROCEDURE XX_EMS_MAIL_PRC_maintenance
(
p_site_id IN VARCHAR2
)
AS
l_in_date VARCHAR2(200) := NULL;
CURSOR cur_maintenance_list (p_site_id VARCHAR2)
IS
SELECT
c.instrument_no "Instrument_Number",
d.instrument_name "Instrument_Name",
d.eqp_srl_no "EQP_Serial_Number",
d.ownership "Ownership",
d.mfg_name "Mfg_Name",
d.model_no "Model_Number",
b.plan "Plan",
a.detail "Plan_Detail",
c.activity_id "Activity_Id",
c.maintenance_date "Maintenance_Date",
c.maintenance_due_date "Maintenance_Due_Date"
FROM
activity_detail a,
activity_master b,
maintenance_schedule c,
instrument_master d,
emp_master m
WHERE
a.plan_id = b.plan_id
AND d.instrument_no = c.instrument_no
AND c.plan_id = b.plan_id
AND c.activity_id = a.activity_id
AND ( c.instrument_no,
c.plan_id,
c.activity_id,
c.maintenance_due_date ) = (
SELECT
e.instrument_no,
e.plan_id,
e.activity_id,
MAX(e.maintenance_due_date)
FROM
maintenance_schedule e
WHERE
c.plan_id = e.plan_id
AND c.instrument_no = e.instrument_no
AND c.activity_id = e.activity_id
GROUP BY
e.instrument_no,
e.plan_id,
e.activity_id
)
AND trunc(c.maintenance_due_date) BETWEEN to_date('01-JUL-2022', 'DD-MON-RRRR') AND to_date('01-JUL-2022', 'DD-MON-RRRR')
AND d.p_manitenance_req <> 'N'
AND ( m.emp_initial = d.prepared_by
OR to_char(m.emp_no) = d.prepared_by )
AND ( b.site_id = d.site_id
OR d.site_id = '001' );
p_to sys_refcursor;
p_cc sys_refcursor;
p_bcc sys_refcursor;
v_html_msg VARCHAR2(32672) := NULL;
--v_txt_msg varchar2(250) := 'WelCome ....'||chr(10)||'this is a test mail';
v_to VARCHAR2 (1000) := NULL;
v_cc VARCHAR2 (1000) := NULL;
v_bcc VARCHAR2 (2000) := NULL;
v_from VARCHAR2 (150) := 'abc.com';
v_db_name varchar2(25) := null;
BEGIN
BEGIN
v_db_name := null;
SELECT listagg(EMAIL, ',') WITHIN GROUP (order by Email) INTO v_to FROM sml.xx_lsp_email_master WHERE email_type = 'TO' AND function_name = 'EMS_App_maintenance' AND isactive='Y';
SELECT listagg(EMAIL, ',') WITHIN GROUP (order by Email) INTO v_cc FROM sml.xx_lsp_email_master WHERE email_type = 'CC' AND function_name = 'EMS_App_maintenance' AND isactive='Y';
--SELECT listagg(EMAIL, ',') WITHIN GROUP (order by Email) INTO v_bcc FROM sml.xx_lsp_email_master WHERE email_type = 'BCC' AND function_name = 'EMS_App';
v_html_msg :='<html><head></head><body><p>Dear All,<br/><br/>
Please find the details of Maintenance due list for this week in Equipment Management System</p><br/>
<table border=1>
<tr>
<th> I/S Number </th>
<th> Instrument Name </th>
<th> EQP Serial Number </th>
<th> Ownership </th>
<th> Make </th>
<th> Model Number </th>
<th> Plan </th>
<th> Plan Detail </th>
<th> Activity Id </th>
<th> Maintenance Date </th>
<th> Maintenance Due Date </th>
</tr>';
FOR i IN cur_maintenance_list (p_site_id)
LOOP
--SELECT (trunc(to_date(i."maintenance_date", 'DD-MON-RRRR') - 15)) into l_in_date from dua
v_html_msg :=
v_html_msg
|| '<tr align="left"><td>'
|| i."Instrument_Number"
|| '</td><td>'
|| i."Instrument_Name"
|| '</td><td>'
|| i."EQP_Serial_Number"
|| '</td><td>'
|| i."Ownership"
|| '</td><td>'
|| i."Mfg_Name"
|| '</td><td>'
|| i."Model_Number"
|| '</td><td>'
|| i."Plan"
|| '</td><td>'
|| i."Plan_Detail"
|| '</td><td>'
|| i."Activity_Id"
|| '</td><td>'
|| i."Maintenance_Date"
|| '</td><td>'
|| i."Maintenance_Due_Date"
|| '</td>
</Tr>';
END LOOP;
v_html_msg :=
v_html_msg
|| '</table>
<p>This is a system generated mail. Please do not reply on this mail.
<br/>
<br/>
Regards,
<br/>
EQP.
</p>
</body>
</html>';
send_mail_recipient(p_to => v_to,
p_cc => v_cc,
p_bcc => v_bcc,
p_from => v_from,
p_subject => 'Mail notification about Maintenance Due for this week Equipment Management System',
--p_text_msg => v_txt_msg,
p_html_msg => v_html_msg,
p_smtp_host => 'smtprelay.com');
END;
null;
END XX_EMS_MAIL_PRC_maintenance;
Line 113 is
v_html_msg :=
v_html_msg
|| '<tr align="left"><td>'
...
That variable is declared as
v_html_msg VARCHAR2(32672)
which means that - as you're processing data in a loop - its length exceeds 32672 characters in length.
What to do? Use CLOB instead, or check whether there's really that much data (i.e. maybe cursor returned more rows than you'd expect - check its SELECT statement).
The main problem is that v_html_msg is defined as VARCHAR2(32672) and the loop is trying to add more than 32672 characters there.
The full solution is to change v_html_msg datatype to CLOB. However, it will require you to refactor this procedure XX_EMS_MAIL_PRC_maintenance and other dependent one, like send_mail_recipient.
The workaround may be to send mail with only partial data (of course if it is OK with your business requirements). You can add an "if" in the loop so once the length of v_html_msg is larger than 32000 - stop adding more items there. For example:
FOR i IN cur_maintenance_list (p_site_id)
LOOP
if length(v_html_msg) <30000 then --<<< NEW CODE
v_html_msg :=
v_html_msg
|| '<tr align="left"><td>'
|| i."Instrument_Number"
|| '</td><td>'
|| i."Instrument_Name"
|| '</td><td>'
|| i."EQP_Serial_Number"
|| '</td><td>'
|| i."Ownership"
|| '</td><td>'
|| i."Mfg_Name"
|| '</td><td>'
|| i."Model_Number"
|| '</td><td>'
|| i."Plan"
|| '</td><td>'
|| i."Plan_Detail"
|| '</td><td>'
|| i."Activity_Id"
|| '</td><td>'
|| i."Maintenance_Date"
|| '</td><td>'
|| i."Maintenance_Due_Date"
|| '</td>
</Tr>';
else --<<< NEW CODE
v_html_msg :=
v_html_msg ||'... and more rows exist...';
end if;
END LOOP;

Convert Oracle cursor to MYSQL

How can I convert following Oracle line to MYSQL?
cur_var rout_acc_cursor%ROWTYPE;
type of rout_acc_cursor is CURSOR
these cur_var and rout_acc_cursor is used as follows
OPEN rout_acc_cursor;
FETCH rout_acc_cursor
INTO cur_var;
WHILE rout_acc_cursor%FOUND
LOOP
routing_ac := cur_var.routing_ac;
-- sharicomplient := cur_var.u06_sharia_complient;
sexchange := cur_var.u06_exchange;
comm_cat := cur_var.u06_commision_type;
comm_group_id := cur_var.u06_commision_group_id;
parms :=
parms
|| '!'
|| cur_var.u06_exchange
|| ','
|| cur_var.u06_routing_ac_type
|| ','
|| NVL (routing_ac, '*')
|| ',';

MySQL performance differences between a different "FROM" operator usage

Can someone please explain to me why this:
SELECT
A.id,
A.name,
B.id AS title_id
FROM title_information AS A
JOIN titles B ON B.title_id = A.id
WHERE
A.name LIKE '%testing%'
is considerably slower (6-7 times) than this:
SELECT
A.id,
A.name,
B.id AS title_id
FROM (SELECT id, name FROM title_information) AS A
JOIN titles B ON B.title_id = A.id
WHERE
A.name LIKE '%testing%'
I know it's probably hard to answer this question without knowing full details about the schema and MySQL configuration, but I'm looking for any generic reasons why the first example could be so significantly slower than the second?
Running EXPLAIN gives this:
|| *id* || *select_type* || *table* || *type* || *possible_keys* || *key* || *key_len* || *ref* || *rows* || *Extra* ||
|| 1 || SIMPLE || B || index || || id || 12 || || 80407 || Using index ||
|| 1 || SIMPLE || A || eq_ref || PRIMARY,id_UNIQUE,Index 4 || PRIMARY || 4 || newsql.B.title_id || 1 || Using where ||
and
|| *id* || *select_type* || *table* || *type* || *possible_keys* || *key* || *key_len* || *ref* || *rows* || *Extra* ||
|| 1 || PRIMARY || B || index || || id || 12 || || 80407 || Using index ||
|| 1 || PRIMARY || <derived2> || ALL || || || || || 71038 || Using where; Using join buffer ||
|| 2 || DERIVED || title_information || index || || Index 4 || 206 || || 71038 || Using index ||
UPDATE:
A.id and B.id are both PRIMARY KEYS, while A.name is an index. Both tables have around 50,000 rows (~15MB). MySQL configuration is pretty much a default one.
Not sure if that helps (or if it adds more to the confusion - as it does for me) but using more generic LIKE statement that is likely to have more matching fields (e.g. "LIKE '%x%'") makes the first query run considerably faster. On the other hand, using "LIKE '%there are no records matching this%'" will make the second query a lot faster (while the first one struggles).
Anyone can shed some light on what's going on here?
Thank you!
This is speculation (my powers of reading MySQL explain output are weaker than they should be, because I want to see data flow diagrams).
But here is what I think is happening. The first query is saying "Let's go through B and look up the appropriate value in A". It then looks up the appropriate value using the id index, then it needs to fetch the page and compare to name. These accesses are inefficient, because they are not sequential.
The second version appears to recognize the condition on name as being important. It is going through the name index on A and only fetching the matching rows as needed. This is faster, because the data is in the index and few pages are needed for the matching names. The match to B is then pretty simple, with only one row to match.
I am surprised at the performance difference. Usually, derived tables are bad performance-wise, but this is clearly an exception.

What are the differences in the following variable initialization styles in MySQL?

I'm fairly new to queries which involve variable declaration in MySQL. I have seen various styles and I'm not fully clear of what these actually do. I've questions about what these actually do.
1)
set #row:=0;
SELECT name, #row:=#row + 1 AS rownum
FROM animal
2)
SELECT name, #row:=#row + 1 AS rownum
FROM (SELECT #row:= 0) c, animal
Both returns the same:
name rownum
|| cat || 1 ||
|| cat || 2 ||
|| dog || 3 ||
|| dog || 4 ||
|| dog || 5 ||
|| ant || 6 ||
What are the differences in the above two queries and which of the two to adopt as to their scope, efficiency, coding habit, use-cases?
3) Now if I do this:
set #row:=0;
SELECT name, #row:=#row + 1 AS rownum
FROM (SELECT #row:= 123) c, animal
I get
name rownum
|| cat || 124 ||
|| cat || 125 ||
|| dog || 126 ||
|| dog || 127 ||
|| dog || 128 ||
|| ant || 129 ||
So doesn't that mean that the inner variable initialization is overriding the outer initialization and leaving the latter redundant hence (and hence its always a better practice to initialize in a SELECT?
4) If I merely do:
SELECT name, #row:=#row + 1 AS rownum
FROM animal
I get
name rownum
|| cat || NULL ||
|| cat || NULL ||
|| dog || NULL ||
|| dog || NULL ||
|| dog || NULL ||
|| ant || NULL ||
I can understand that since row isn't initialized. But if I run any of the other queries (may be variable row is getting initialized?) I see that row variable is incremented every time I run the above query. That is it gives me the result on first run:
name rownum
|| cat || 1 ||
|| cat || 2 ||
|| dog || 3 ||
|| dog || 4 ||
|| dog || 5 ||
|| ant || 6 ||
and then when re-run it yields in
name rownum
|| cat || 7 ||
|| cat || 8 ||
|| dog || 9 ||
|| dog || 10 ||
|| dog || 11 ||
|| ant || 12 ||
So is row being stored somewhere? And what is its scope and lifespan?
5) If I have query like this:
SELECT (CASE WHEN #name <> name THEN #row:=1 ELSE #row:=#row + 1 END) AS rownum,
#name:=name AS name
FROM animal
This always yields the right result:
rownum name
|| 1 || cat ||
|| 2 || cat ||
|| 1 || dog ||
|| 2 || dog ||
|| 3 || dog ||
|| 1 || ant ||
So doesn't that mean its not always necessary to initialize variable at the top or in a SELECT depending on the query?
Make sure to read the manual section on user variables.
What are the differences in the above two queries and which of the two to adopt as to their scope, efficiency, coding habit, use-cases?
Query 1) uses multiple statements. It can therefore rely on the order in which these statements are executed, ensuring that the variable is set before it gets incremented.
Query 2) on the other hand does the initialization in a nested subquery. This turns the whole thing into a single query. You don't risk forgetting the initialization. But the code relies more heavily on the internal workings of the mysql server, particularly the fact that it will execute the subquery before it starts computing results for the outer query.
So doesn't that mean that the inner variable initialization is overriding the outer initialization and leaving the latter redundant hence (and hence its always a better practice to initialize in a SELECT?
This is not about inner and outer, but about sequential order: the subquery is executed after the SET, so it will simply overwrite the old value.
So is row being stored somewhere? And what is its scope and lifespan?
User variables are local to the server connection. So any other process will be unaffected by the setting. Even the same process might maintain multiple connections, with independent settings of user variables. Once a connection is closed, all variable settings are lost.
So doesn't that mean its not always necessary to initialize variable at the top or in a SELECT depending on the query?
Quoting from the manual:
If you refer to a variable that has not been initialized, it has a value of NULL and a type of string.
So you can use a variable before it is initialized, but you have to be careful that you can actually deal with the resulting NULL value in a reasonable way. Note however that your query 5) suffers from another problem explicitely stated in the manual:
As a general rule, you should never assign a value to a user variable and read the value within the same statement. You might get the results you expect, but this is not guaranteed. The order of evaluation for expressions involving user variables is undefined and may change based on the elements contained within a given statement; in addition, this order is not guaranteed to be the same between releases of the MySQL Server. In SELECT #a, #a:=#a+1, ..., you might think that MySQL will evaluate #a first and then do an assignment second. However, changing the statement (for example, by adding a GROUP BY, HAVING, or ORDER BY clause) may cause MySQL to select an execution plan with a different order of evaluation.
So in your case, the #name:=name part could get executed before the #name <> name check, causing all your rownum values to be the same. So even if it does work for now, there are no guarantees that it will work in the future.
Note that I've been very sceptic about using user variables in this fashion. I've already quoted the above warning from the manual in comments to several answers. I've also asked questions like the one about Guarantees when using user variables to number rows. Other users are more pragmatic, and therefore more willing to use code that appears to work without express guarantees that things will continue to work as intended.

Does mysql have function for returning a number with ordinal suffix?

Basically I'm looking for something like
SELECT ordinal(my_number) FROM my_table
which would return
1st
11th
1071st
...
etc
but preferrably without the use of a stored procedure
I don't know of a built-in function but it's pretty easy to write:
SELECT
CONCAT(my_number, CASE
WHEN my_number%100 BETWEEN 11 AND 13 THEN "th"
WHEN my_number%10 = 1 THEN "st"
WHEN my_number%10 = 2 THEN "nd"
WHEN my_number%10 = 3 THEN "rd"
ELSE "th"
END)
FROM my_table;
mysql doesn't have support for this. You'll have to handle the strings in whichever language you are getting the mysql data from.
Based on Ken's code, a custom MySQL function would be as follows:
DELIMITER $$
CREATE FUNCTION ordinal(number BIGINT)
RETURNS VARCHAR(64)
DETERMINISTIC
BEGIN
DECLARE ord VARCHAR(64);
SET ord = (SELECT CONCAT(number, CASE
WHEN number%100 BETWEEN 11 AND 13 THEN "th"
WHEN number%10 = 1 THEN "st"
WHEN number%10 = 2 THEN "nd"
WHEN number%10 = 3 THEN "rd"
ELSE "th"
END));
RETURN ord;
END$$
DELIMITER ;
Then it can be used as:
SELECT ordinal(1) -- 1st
SELECT ordinal(11) -- 11th
SELECT ordinal(21) -- 21st
SELECT ordinal(my_number) FROM my_table
It is possible in MySQL using the string functions but it gets messy real fast. You'd better just do the suffix in the language you're using. For example, in PHP you could do something like this:
function ordSuffix($num) {
if(empty($num) || !is_numeric($num) || $num == 0) return $num;
$lastNum = substr($num, -1);
$suffix = 'th';
if($lastNum == 1 && $num != 11) { $suffix = 'st'; }
elseif($lastNum == 2 && $num != 12) { $suffix = 'nd'; }
elseif($lastNum == 3 && $num != 13) { $suffix = 'rd'; }
return $num.$suffix;
}
echo ordSuffix(4); // 4th
echo ordSuffix(1); // 1st
echo ordSuffix(12); // 12th
echo ordSuffix(1052); // 1052nd
I found a way that works for me but its a bit of a hack
DATE_FORMAT(CONCAT('2010-01-', my_number), '%D')
That works because currently the number I'm looking at never gets above 25. But it doesn't generalize well so someone might be entertained by this:
CONCAT(
IF(my_number % 100 BETWEEN 11 AND 13,
FLOOR(my_number / 100),
FLOOR(my_number / 10)),
DATE_FORMAT(
CONCAT('2010-01-',
IF(my_number % 100 BETWEEN 11 AND 13
my_number % 100,
my_number % 10)),
'%D'))
But that's a lot of work just to get at the DATE_FORMAT functionality when Ken's code is simpler.