How to use order by with modified column - sql

There is a Table with a age-column. The Value of this column is a varchar because the age of persons under the age of 1 is saved in months with an additional 'm' (9 Month old -> '9m')
I know that this is generally a bad idea and one should rather persist the date of birth, but in this case the age refers to the age on a specific day in history - and additionally this is part of a lesson and the whole point is learning how to treat "weird" data.
My first idea was to put a leading zero on all ages which are not purely numeric:
SELECT *
FROM db
ORDER BY REPLACE(age, (IF ISNUMERIC(age) age ELSE CONCAT('0', age))) DESC;
However this is not a valid SQL-statement and neither are my other attempts.
The question is: How can I adjust the value used for ORDER BY without altering the db?
Another approach would be to select only the rows with a purely numeric age value and a separate select for the remaining rows order both of them separately and combine them afterwards.
My take on this was the following:
(SELECT name, age
FROM titanic
WHERE ISNUMERIC(age)
ORDER BY age DESC)
UNION
(SELECT name, age
FROM titanic
WHERE NOT ISNUMERIC(age)
ORDER BY age);
This is in fact valid or at least it gives me a result. But in the result I can't really see what happened to the order, it looks like the UNION undos everything.
Thanks in advance, will take any tip or even just the name of the function/method I should look into!

Would this work?
SELECT name, age
FROM titanic
ORDER BY isnumeric(age), age

I would use "Case When" structure with some transformations in the "Order by" to get the total number of months for both types of ages.
Select name, age
From tbl
Where age SIMILAR TO '[1-9][0-9]*' Or
age SIMILAR TO '[1-9][0-2]?m'
Order by Case When age SIMILAR TO '[1-9][0-2]?m' Then Substring(age,1,CHAR_LENGTH(age)-1)::int
When age SIMILAR TO '[1-9][0-9]*' Then age::int * 12 End

First off storing age is a poor idea, you must update every row regularly, in this case at least monthly. Storing it as string turns a poor idea into a terrible idea. Not only do you have the maintenance, it is not straight forward nor is it straight forward to insert it. Instead store date-of-birth as a timestamp or date. You can then quickly get age (via the age) you can then use the extract function on the resulting interval at whatever level is desired. If you absolutely must present age with text indicator for either years/months (or even days) you create a view that derives the appropriate value. (see demo)
create or replace view titanic as
select name "Name"
, case when extract( year from age(dob))::integer > 0 then to_char (extract( year from age(dob)), '999') || ' years'
when extract( month from age(dob))::integer> 0 then to_char (extract( month from age(dob)), '99') || ' months'
else to_char (extract( day from age(dob)),'99') || ' days'
end "Age"
from titanic_tbl;

Related

How to write SQL statement to select for data broken up for each month of the year?

I am looking for a way to write an SQL statement that selects data for each month of the year, separately.
In the SQL statement below, I am trying to count the number of instances in the TOTAL_PRECIP_IN and TOTAL_SNOWFALL_IN columns when either column is greater than 0. In my data table, I have information for those two columns ("TOTAL_PRECIP_IN" and "TOTAL_SNOWFALL_IN") for each day of the year (365 total entries).
I want to break up my data by each calendar month, but am not sure of the best way to do this. In the statement below, I am using a UNION statement to break up the months of January and February. If I keep using UNION statements for the remaining months of the year, I can get the answer I am looking for. However, using 11 different UNION statements cannot be the optimal solution.
Can anyone give me a suggestion how I can edit my SQL statement to measure from the first day of the month, to the last day of the month for every month of the year?
select monthname(OBSERVATION_DATE) as "Month", sum(case when TOTAL_PRECIP_IN or TOTAL_SNOWFALL_IN > 0 then 1 else 0 end) AS "Days of Rain" from EMP_BASIC
where OBSERVATION_DATE between '2019-01-01' and '2019-01-31'
and CITY = 'Olympia'
group by "Month"
UNION
select monthname(OBSERVATION_DATE) as "Month", sum(case when TOTAL_PRECIP_IN or TOTAL_SNOWFALL_IN > 0 then 1 else 0 end) from EMP_BASIC
where OBSERVATION_DATE between '2019-02-01' and '2019-02-28'
and CITY = 'Olympia'
group by "Month"```
Your table structure is too unclear to tell you the exact query you will need. But a general easy idea is to build the sum of your value and then group by monthname and/or by month. Sice you wrote you only want sum values greater 0, you can just put this condition in the where clause. So your query will be something like this:
SELECT MONTHNAME(yourdate) AS month,
MONTH(yourdate) AS monthnr,
SUM(yourvalue) AS yoursum
FROM yourtable
WHERE yourvalue > 0
GROUP BY MONTHNAME(yourdate), MONTH(yourdate)
ORDER BY MONTH(yourdate);
I created an example here: db<>fiddle
You might need to modify this general construct for your concrete purpose (maybe take care of different years, of NULL values etc.). And note this is an example for a MYSQL DB because you wrote about MONTHNAME() which is in most cases used in MYSQL databases. If you are using another DB type, maybe you need to do some modifications. To make sure that answers match your DB type, tag it in your question, please.

What is a good way to return a text field in a query for a calculated column?

I have an SQLite database with a members table on it. The columns on my table are first_name, last_name, date_dues_paid. I need to return a fourth column labeled active which would either be “true” or “false” depending on if date_dues_paid is a year old or more.
I’ve tried CAST(WHEN ) AS active and used DATEDIFF() and several other methods but just can’t get it right. I can provide more samples and code base later today, posting on mobile right now.
Just add one year to date_dues_paid and compare it against the current date like this:
SELECT
first_name,
last_name,
date_dues_paid,
SELECT CASE WHEN DATE(date_dues_paid,'+1 year') > DATE('now') THEN 'TRUE' ELSE 'FALSE' END AS active
FROM members
Instead of TRUE/FALSE strings you could also use bool (1,0) as a result:
SELECT DATE(date_dues_paid,'+1 year') > DATE('now') AS active

sql select number divided aggregate sum function

I have this schema
and I want to have a query to calculate the cost per consultant per hour per month. In other words, a consultant has a salary per month, I want to divide the amount of the salary between the hours that he/she worked that month.
SELECT
concat_ws(' ', consultants.first_name::text, consultants.last_name::text) as name,
EXTRACT(MONTH FROM tasks.init_time) as task_month,
SUM(tasks.finish_time::timestamp::time - tasks.init_time::timestamp::time) as duration,
EXTRACT(MONTH FROM salaries.payment_date) as salary_month,
salaries.payment
FROM consultants
INNER JOIN tasks ON consultants.id = tasks.consultant_id
INNER JOIN salaries ON consultants.id = salaries.consultant_id
WHERE EXTRACT(MONTH FROM tasks.init_time) = EXTRACT(MONTH FROM salaries.payment_date)
GROUP BY (consultants.id, EXTRACT(MONTH FROM tasks.init_time), EXTRACT(MONTH FROM salaries.payment_date), salaries.payment);
It is not possible to do this in the select
salaries.payment / SUM(tasks.finish_time::timestamp::time - tasks.init_time::timestamp::time)
Is there another way to do it? Is it possible to solve it in one query?
Assumptions made for this answer:
The model is not entirely clear to me, so I am assuming the following:
you are using PostgreSQL
salaries.date is defined as a date column that stores the day when a consultant was paid
tasks.init_time and task.finish_time are defined as timestamp storing the data & time when a consultant started and finished work on a specific task.
Your join on only the month is wrong as far as I can tell. For one, because it would also include months from different years, but more importantly because this would lead to a result where the same row from salaries appeared several times. I think you need to join on the complete date:
FROM consultants c
JOIN tasks t ON c.id = t.consultant_id
JOIN salaries s ON c.id = s.consultant_id
AND t.init_time::date = s.payment_date --<< here
If my assumptions about the data types are correct, the cast to a timestamp and then back to a time is useless and wrong. Useless because you can simply subtract to timestamps and wrong because you are ignoring the actual date in the timestamp so (although unlikely) if init_time and finish_time are not on the same day, the result is wrong.
So the calculation of the duration can be simplified to:
t.finish_time - t.init_time
To get the cost per hour per month, you need to convert the interval (which is the result when subtracting one timestamp from another) to a decimal indicating the hours, you can do this by extracting the seconds from the interval and then dividing that by 3600, e.g.
extract(epoch from sum(t.finish_time - t.init_time)) / 3600)
If you divide the sum of the payments by that number you get your cost per hour per month:
SELECT concat_ws(' ', c.first_name, c.last_name) as name,
to_char(s.payment_date, 'yyyy-mm') as salary_month,
extract(epoch from sum(t.finish_time - t.init_time)) / 3600 as worked_hours,
sum(s.payment) / (extract(epoch from sum(t.finish_time - t.init_time)) / 3600) as cost_per_hour
FROM consultants c
JOIN tasks t ON c.id = t.consultant_id
JOIN salaries s ON c.id = s.consultant_id AND t.init_time::date = s.payment_date
GROUP BY c.id, to_char(s.payment_date, 'yyyy-mm') --<< no parentheses!
order by name, salary_month;
As you want the report broken down by month you should convert the month into something that contains the year as well. I used to_char() to get a string with only year and month. You also need to remove salaries.payment from the group by clause.
You also don't need the "payment month" and "salary month" because both will always be the same as that is the join condition.
And finally you don't need the cast to ::text for the name columns because they are most certainly defined as varchar or text anyway.
The sample data I made up for this: http://sqlfiddle.com/#!15/ae0c9
Somewhat unrelated, but:
You should also not put the column list of the group by in parentheses. Putting a column list in parentheses in Postgres creates an anonymous record which is something completely different then having multiple columns. This is also true for the columns in the select list.
If at all the target is putting it in one query, then just confirming, have you tried to achieve it using CTEs?
Like
;WITH cte_pymt
AS
(
//Your existing query 1
)
SELECT <your required data> FROM cte_pymt

SQL SUBQUERY in Sqlplus Oracle 11g Help please

Im trying to write a query that will list the Consultant_Id, Name and age for all consultants who have a Grade of 'D' in my Consultant table, and were born more than 30 years ago and have a name that begins with the letter 'L'. I need the output ascending age order. so far i have this but i presume there is multiple errors, any help would be greatly appreciated!
I don't want to seem to be attacking you, but since you said you've been trying to figure this out for a long time and you're stuck, let's look at some of the problems with what you have at the moment.
SELECT Consultant_Id, Name, DOB,
This line has a trailing comma.
FROM Consultant;
This line has a semicolon at the end, terminating the statement, so your where clause is a separate invalid command.
WHERE DOB = (SELECT MAX(DOB <= 01-jan-85) FROM CONSULTANT)
You seem to be using 01-jan-85 as a date, but even if it was in single quotes it would be a string not a date and you should explicitly convert it to a date type; without quotes it would get an invalid identifier (trying to treat 'jan' as a column name). Using the <= comparator inside a max() call isn't valid anyway, and I'm not quite sure what it's supposed to achieve. At best you're getting the most recent DOB from the table in the subquery, and then using that to filter the main query so you will only get the row (or maybe rows) that match that exact date. It won't give you all DOBs more than 30 years ago.
WHERE name( SELECT SUBSTR(Name,1,20) LIKE 'L%' AS ShortName)
name() isn't a function, so perhaps you meant to compare it with =; but as with the DOB check that isn't really what you wan. Your subquery doesn't have a from clause, and isn't correlated with the main query so would return multiple rows, which is an error in itself. As this is the second filter you should be using AND rather than a second WHERE. And the substr() isn't really adding anything since you're using like anyway.
WHERE Grade = 'D'
This is almost OK, but should also be AND not `WHERE.
ORDER BY SUBSTR(DOB, 7,9);
This is doing an implicit conversion of DOB to a string, then getting characters 7 to 15 of whatever your session converts it to by default. Based on the date string you used earlier you means substr(dob, 7, 2), which would give you the 2-digit year in that format; but you're supposed to be ordering by the whole DOB, not just the year.
#HepC has given the actual command you need (aside from the trailing comma on the first line).
I believe this is what you're looking for.
SELECT consultant_id, name, DOB
FROM consultant
WHERE DOB <= ADD_MONTHS((SYSDATE), -360)
AND grade = 'D'
AND name LIKE 'L%'
ORDER BY DOB;
SELECT consultant_id, name, DOB
FROM consultant
WHERE DOB <= ADD_MONTHS(TRUNC(SYSDATE), 12 * -30)
AND name LIKE 'L%'
AND grade = 'D'
ORDER BY DOB;
SELECT Consultant_Id, Name AS ShortName, DOB
FROM Consultant
WHERE DOB < dateadd(year, -30, getdate())
AND name LIKE 'L%'
AND Grade = 'D'
ORDER BY DOB

How to group by a date column by month

I have a table with a date column where date is stored in this format:
2012-08-01 16:39:17.601455+0530
How do I group or group_and_count on this column by month?
Your biggest problem is that SQLite won't directly recognize your dates as dates.
CREATE TABLE YOURTABLE (DateColumn date);
INSERT INTO "YOURTABLE" VALUES('2012-01-01');
INSERT INTO "YOURTABLE" VALUES('2012-08-01 16:39:17.601455+0530');
If you try to use strftime() to get the month . . .
sqlite> select strftime('%m', DateColumn) from yourtable;
01
. . . it picks up the month from the first row, but not from the second.
If you can reformat your existing data as valid timestamps (as far a SQLite is concerned), you can use this relatively simple query to group by year and month. (You almost certainly don't want to group by month alone.)
select strftime('%Y-%m', DateColumn) yr_mon, count(*) num_dates
from yourtable
group by yr_mon;
If you can't do that, you'll need to do some string parsing. Here's the simplest expression of this idea.
select substr(DateColumn, 1, 7) yr_mon, count(*) num_dates
from yourtable
group by yr_mon;
But that might not quite work for you. Since you have timezone information, it's sure to change the month for some values. To get a fully general solution, I think you'll need to correct for timezone, extract the year and month, and so on. The simpler approach would be to look hard at this data, declare "I'm not interested in accounting for those edge cases", and use the simpler query immediately above.
It took me a while to find the correct expression using Sequel. What I did was this:
Assuming a table like:
CREATE TABLE acct (date_time datetime, reward integer)
Then you can access the aggregated data as follows:
ds = DS[:acct]
ds.select_group(Sequel.function(:strftime, '%Y-%m', :date_time))
.select_append{sum(:reward)}.each do |row|
p row
end