Macro concept in SQL - mysql

Do any database engines have the concept of a C-like macro? Here would be an example where I'd just like to make it more readable:
SELECT
SUM(Profit) AS Total,
(SELECT AVG(Profit) FROM This
WHERE Category=This.Category AND Product=This.Product
AND PARSE_DATE('%M %d%, Y', CONCAT(This.Month ' 1, ' This.Year))
BETWEEN PARSE_DATE('%M %d%, Y', CONCAT(This.Month ' 1, ' This.Year))
AND PARSE_DATE('%M %d%, Y', CONCAT(This.Month ' 1, ' This.Year))-INTERVAL 3 MONTH
FROM Tbl
I would rather have something that looks like:
#define dt PARSE_DATE('%M %d%, Y', CONCAT(This.Month ' 1, ' This.Year))
SELECT
SUM(Profit) AS Total,
(SELECT AVG(Profit) FROM This
WHERE Category=This.Category AND Product=This.Product
AND dt BETWEEN dt AND (dt-INTERVAL 3 MONTH)
FROM Tbl
Does something like that exist or commonly-used in the major DBMSs?

From Oracle 12, you can declare a function inside a sub-query factoring (WITH) clause:
WITH FUNCTION dt (month INT, year INT) RETURN DATE AS
BEGIN
RETURN TO_DATE(year || '-' || month || '-01', 'YYYY-MM-DD');
END;
SELECT *
FROM this
WHERE dt(this.month, this.year)
BETWEEN ADD_MONTHS(dt(this.month, this.year), -3)
AND dt(this.month, this.year);
db<>fiddle here
From Oracle 21, you can write SQL macros:
CREATE FUNCTION dt (month INT, year INT)
RETURN VARCHAR2 SQL_MACRO(SCALAR)
AS
BEGIN
RETURN 'TO_DATE(year || ''-'' || month || ''-01'', ''YYYY-MM-DD'')';
END;
/
Then would use it as:
SELECT *
FROM this
WHERE dt(this.month, this.year)
BETWEEN ADD_MONTHS(dt(this.month, this.year), -3)
AND dt(this.month, this.year);
And the query would get rewritten as:
SELECT *
FROM this
WHERE TO_DATE(this.year || '-' || this.month || '-01', 'YYYY-MM-DD')
BETWEEN ADD_MONTHS(TO_DATE(this.year || '-' || this.month || '-01', 'YYYY-MM-DD'), -3)
AND TO_DATE(this.year || '-' || this.month || '-01', 'YYYY-MM-DD');

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;

Calculate average of some columns, not counting null values

I have a table with some readings that looks like this:
id foo bar baz qux
1 2 4 NULL 3
2 6 11 0 2
I want to calculate an average of some columns, not including null values in the count. Something like this pseudo-code:
select (foo+bar+baz)/countNonNulls(foo,bar,baz) AS result
FROM readings WHERE id=1;
I.e, my expected result is (2+4)/2 = 3.
Is there a way to do this in a single SQL query?
In MySQL, you can use:
select (coalesce(foo, 0) + coalesce(bar, 0) + coalesce(baz, 0) /
((foo is not null) + (bar is not null) + (baz is not null))
) as average
Note that this assumes that at least one value is not null, to prevent division by 0.
To handle the general case, you can use case:
select (case when coalesce(foo, bar, bz) is not null
then (coalesce(foo, 0) + coalesce(bar, 0) + coalesce(baz, 0) /
((foo is not null) + (bar is not null) + (baz is not null))
)
end) as average
try the where clause: where [nameColumn] is not null

Add 28 to last 2 digit of date and replace the order

I have a number such as this : 840106
I need to do the following :
Change the number to date add - and flip the number : 06-01-84
add 28 to the last 2 digit that the date will be : 06-01-12
84 + 16 = 00 + 12 = 12
number is always changing sometimes it cab be 850617 , but format is always same add - and add 28 last 2 digit.
any ideas how to help me here ?
Here is a sqlite solution:
create table t( c text);
insert into t (c) values(990831);
insert into t (c) values(840106);
insert into t (c) values(800315);
insert into t (c) values(750527);
insert into t (c) values(700923);
insert into t (c) values(620308);
select c, substr(c,5,2) || '-' || substr(c,3,2) || '-' ||
case when (substr(c,1,2) + 28) < 100 then (substr(c,1,2) + 28)
else case when ((substr(c,1,2) + 28) - 100) < 10 then '0' || ((substr(c,1,2) + 28) - 100)
else ((substr(c,1,2) + 28) - 100)
end
end
from t;
For formatting you can use
http://www.w3schools.com/sql/func_date_format.asp
For adding days to the date you should take a look at date_add() function
mysql> SELECT DATE_ADD('1998-01-02', INTERVAL 31 DAY);
http://dev.mysql.com/doc/refman/5.5/en/date-and-time-functions.html#function_date-add
Assuming date is the name of the column containing your date:
DATE_FORMAT(DATE_ADD(STR_TO_DATE(date, %y%m%d), INTERVAL 28 YEAR), %d-%m-%y);
What this does is first formats the string into a date, then adds 28 years, then converts back to string with the new format.
SQLite is a lot tricker with this, you'll need to use substrings.
substr(date,5) || "-" || substr(date,3,4) || "-" || CAST(CAST(substr(date,1,2) as integer) + 28 - 100) as text
I'm not too experienced with SQLite so the casting may be a bit weird.
Here is a t-sql solution that you can use and migrate to mysql.
declare #myDate as char(8) = '840106';
declare #y as char(2), #m as char(2), #d as char(2);
set #y = LEFT (#myDate, 2);
declare #yi as int = Convert (int, #y);
IF #y between 30 and 99 ----------- pick a cut-off year
SET #yi = (28 - (100-#yi));
SET #y = CONVERT(char, #yi)
set #m = SUBSTRING(#myDate, 3, 2);
set #d = SUBSTRING(#myDate, 5, 2);
SET #myDate = #d + '-' + #m + '-' + #y;
PRINT #myDate;

Query execution time difference?

I have a query manually like this:
SELECT
to_char( to_date('29-Jul-12 22:25:11','DD-MON-YY HH24:MI:SS')- SYSDATE )
FROM DUAL
I need to show output only like HH24:MM:SS.
SELECT trunc(MOD((SYSDATE - to_date('29-Jul-12 22:25:11', 'DD-MON-YY HH24:MI:SS')) * 24, 24)) || ':' ||
trunc(MOD((SYSDATE - to_date('29-Jul-12 22:25:11', 'DD-MON-YY HH24:MI:SS')) * 24 * 60, 60)) || ':' ||
trunc(MOD((SYSDATE - to_date('29-Jul-12 22:25:11', 'DD-MON-YY HH24:MI:SS')) * 24 * 60 * 60, 60)) Time
FROM dual
write function that takes your stamp date and returns varchar2 (the value like "HH24:MI:SS")
create or replace Function Get_Difference(i_Stamp_Date date) return varchar2 is
v_Result varchar2(8);
begin
v_Result := Round(mod((sysdate - i_Stamp_Date) * 24, 24)) || ':' ||
Round(mod((sysdate - i_Stamp_Date) * 1440, 60)) || ':' ||
Round(mod((sysdate - i_Stamp_Date) * 86400, 60));
return v_Result;
end;
and then you can call it like:
SELECT Get_Difference(to_date('29-Jul-12 22:25:11','DD-MON-YY HH24:MI:SS')) FROM DUAL
and enjoy it!

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.