In mariadb/mysql I can use variables in the following way to perform calculations in a select statement. In this simple instance, I create a range of dates and use variables to calculate a simple opening balance and closing balance, with a payment of 10 each day.
with RECURSIVE dates as (
select '2017-11-01' as `dt`
union all
select adddate(dt, INTERVAL 1 DAY)
from dates
where dt < CURDATE()
)
select
#vardate:=d.dt
, #openbal
, #payment:= 10
, #closebal:= #openbal+#payment
, #openbal:=#closebal
from dates d;
gives the results....
╔══════════════╦════════════╦═══════════════╦═════════════╗
║ "ac_date" ║ "open_bal" ║ "trans_total" ║ "close_bal" ║
╠══════════════╬════════════╬═══════════════╬═════════════╣
║ "2017-11-01" ║ "0" ║ "10" ║ "10" ║
║ "2017-11-02" ║ "10" ║ "10" ║ "20" ║
║ "2017-11-03" ║ "20" ║ "10" ║ "30" ║
...
Using this technique I can perform simple calculations on the fly in a select statement. My question is, is it possible to use variables in this way in a PL/pgSQL function or is there an alternative method I am overlooking?
I am not entirely sure how that statement works in MariaDB, but this seems to do the same thing:
with vars (openbal, payment) as (
values (0, 10)
), balance as (
select t.dt::date as ac_date,
openbal,
payment,
sum(payment) over (order by t.dt) as close_bal
from vars,
generate_series(date '2017-11-01', current_date, interval '1' day ) as t(dt)
)
select ac_date,
openbal + lag(close_bal) over (order by ac_date) as open_bal,
payment,
close_bal
from balance;
In general to get a running total you use sum() over (order by ...) in SQL. To access values from a previous row, you use the lag() function.
The two CTEs are needed because window functions can't be nested.
To generate a list of rows, use generate_series() in Postgrse.
Related
How can i add order, group by and limit for each arr in query?
users table:
╔════╦══════════════╦
║ id ║ name ║
╠════╬══════════════╬
║ 1 ║ Jeff Atwood ║
║ 2 ║ Geoff Dalgas ║
║ 3 ║ Jarrod Dixon ║
║ 4 ║ Joel Spolsky ║
╚════╩══════════════╩
Output query example without filter and limit:
SELECT JSON_AGG(u) filter (where id > 1) as arr1,
JSON_AGG(u) filter (where id < 3) as arr2
FROM users u
Expected:
╔═══════════════════╦═════════════════════╦
║ arr1 ║ arr2 ║
╠═══════════════════╬═════════════════════╬
║ [{id:1, name: ''},║ [{id:1, name: ''}, ║
║ {id:2, name: ''}] ║ {id:2, name: ''}] ║
╚═══════════════════╩══════════════════════
Query arguments example for one array:
SELECT *
FROM ps
LEFT JOIN u on u.id = ps.id
WHERE ps.date <= now()
GROUP BY ps.id
ORDER BY ps.date DESC
Order BY
You can put order by with in aggregate function like this:
SELECT json_agg(users.* order by id desc) FILTER (WHERE id > 1 ) AS arr1,
json_agg(users.*) FILTER (WHERE id < 2) AS arr2
FROM users;
As per my understanding LIMIT is not allowed to use in this way i.e. array wise
I want to have a weekly timetable table in postgres, That stores availability time of a person in a week
Then I want to get a time range from a user and check that the person in available in that range or not
How can I do that in postgres , I know that Range type can help me, But I don't know how implement weekly tine table
timetable
╔════════╦═════════════╦═════════════════════╗
║ userId ║ day_of_week ║ during ║
╠════════╬═════════════╬═════════════════════╣
║ 1 ║ 0 ║ [10:00:00 12:00:00] ║
║ 1 ║ 0 ║ [14:30:00 16:45:00] ║
║ 1 ║ 1 ║ [10:00:00 12:00:00] ║
║ 1 ║ 1 ║ [08:30:00 12:00:00] ║
║ 1 ║ 1 ║ [13:30:00 17:00:00] ║
║ 1 ║ 1 ║ [19:30:00 23:00:00] ║
║ 1 ║ 2 ║ [10:00:00 12:00:00] ║
╚════════╩═════════════╩═════════════════════╝
day_of_week => The day of the week as Sunday(0) to Saturday(6)
I want to know , is user #1 available in a specific time range?
(2020-05-31 11:00, 2020-05-31 11:30) => true
(2020-05-31 11:00, 2020-05-31 15:30) => false
(2020-05-31 11:00, 2020-06-02 00:00) => false
the timetable is just a sample , and You can offer better schema
A user can be available 24 hours, i.e in Sunday-Tuesday
timetable
╔════════╦═════════════╦═════════════════════╗
║ userId ║ day_of_week ║ during ║
╠════════╬═════════════╬═════════════════════╣
║ 2 ║ 0 ║ [00:00:00 24:00:00] ║
║ 2 ║ 1 ║ [00:00:00 24:00:00] ║
║ 2 ║ 2 ║ [00:00:00 24:00:00] ║
╚════════╩═════════════╩═════════════════════╝
So we have
(2020-05-31 11:00, 2020-05-31 11:30) => true
(2020-05-31 11:00, 2020-05-31 15:30) => true
(2020-05-31 11:00, 2020-06-02 00:00) => true
(2020-06-02 00:00, 2020-06-03 01:00) => false
If you can split up the input value (parameter) into each day, you can do something like this:
select bool_and(tt.user_id is not null)
from (
values
(0, timerange('11:00', '24:00')),
(1, timerange('00:00', '24:00'))
) as t(dow, day_range)
left join timetable tt
on tt.day_of_week = t.dow
and tt.during #> t.day_range
and tt.user_id = 2;
If you are limited by passing a timestamp range, you will need to turn the range into the corresponding number of days and time ranges per week day.
with parm (user_id, input) as (
values
-- these are essentially your query parameters
(2, tsrange('2020-05-31 11:00', '2020-06-02 00:00', '[)'))
), days as (
select user_id,
timerange(
case
when g.day::date = lower(input)::date then lower(input)::time
when g.day::date > lower(input)::date and g::date <= upper(input)::date then time '00:00'
end,
case
when g.day::date = upper(input)::date then upper(input)::time
when g.day::date < upper(input)::date and g::date >= lower(input)::date then time '24:00'
end, '()') day_range,
extract(dow from g.day) as dow
from parm
cross join generate_series(lower(input)::date, upper(input)::date, interval '1 day') as g(day)
)
select d.day_range as input_range,
d.dow as input_dow,
tt.user_id,
tt.during,
tt.user_id is not null as is_match
from days d
left join timetable tt
on d.dow = tt.day_of_week
and tt.during #> d.day_range
and tt.user_id = d.user_id
Listing each column of the result is mainly there to show how it works. If you just need a true/false result, you can do bool_and(tt_user_id is not null) as I did in the first select. The first CTE (with parm) can be removed if you replace the parameter in the second CTE directly.
I'm quite new with SQL Server (2017) and i've this kind of need:
Consider this record:
╔═════════════╦═══════════════╦══════════════╦═══════╗
║ Surname ║ Name ║ Day of birth ║ City ║
╠═════════════╬═══════════════╬══════════════╬═══════╣
║ Rivers Lara ║ Wanda Leticia ║ 07/04/1956 ║ Paris ║
╚═════════════╩═══════════════╩══════════════╩═══════╝
I've to find all the matching records in following list highlighting the type of matching:
╔═════════════╦═══════════════╦══════════════╦════════╗
║ Surname ║ Name ║ Day of birth ║ City ║
╠═════════════╬═══════════════╬══════════════╬════════╣
║ Rivers Lara ║ Wanda Leticia ║ 07/04/1956 ║ London ║
║ Rivers ║ Leticia ║ 07/04/1956 ║ Rome ║
║ Rivers ║ Leticia ║ 14/03/1995 ║ Rome ║
║ Rivers Lara ║ Leticia ║ 07/04/1956 ║ Paris ║
║ Rivers Lara ║ Wanda Leticia ║ 08/07/1983 ║ Paris ║
╚═════════════╩═══════════════╩══════════════╩════════╝
For example:
1st row is matching for Surname+Name+dayofbirth
2nd for Part of Surname+Part of Name+dayofbirth
3rd for Part of Surname+Part of Name
4th for Surname+Part of Name+dayofbirth+City
and so on...
Any ideas on how to approach this type of query will be appreciated considering also that at the moment we have fixed number of possible matching but in the future they could increase (maybe adding more columns like Tax number or other).
assuming the presentation layer is html and your ok with bits of html in query output, this is a rough idea, though working it's not precisely efficient and no partial matches, only exact. to match partial you'll need to use charindex() or patindex() and split on ' ' with left() or right(), can get convoluted.
one split for left/right word is like, at least this is the way I do splitting still.
--this is only an example on the convoluted nature of string manipulation.
declare #Surname varchar(128) = 'Rivers Lara';
select
rtrim(iif(charindex(' ',#Surname) = 0,#Surname,Left(#Surname, charindex(' ',#Surname)))) first_part_Surname
,ltrim(reverse(iif(charindex(' ',#Surname) = 0,reverse(#Surname),Left(reverse(#Surname), charindex(' ',reverse(#Surname)))))) last_part_Surname
declare #StartRed varchar(50) = '<span style="color: red;">'
,#StopRed varchar(50) = '</span>';
select
case when tm.Surname = tr.Surname then #StartRed + tr.Surname + #StopRed else tr.Surname end Surname
,case when tm.Name = tr.Name then #StartRed + tr.Name + #StopRed else tr.Name end [Name]
,case when tm.[Day of Birth] = tr.[Day of Birth] then #StartRed + convert(varchar, tr.[Day of Birth], 1) + #StopRed end [Day of Birth]
,case when tm.City = tr.City then #StartRed + tr.City + #StopRed else tr.City end City
from TableMatch tm
inner join TableRecords tr on (tm.Surname = tr.Surname or tm.Name = tr.Name)
and (tm.[Day of Birth] = tr.[Day of Birth] or tm.City = tr.City)
-- requires either Surname or Name to match and at least 1 of the 2 others to match
additionally, you may be able to use soundex() to find names that sound like other names as a stop-gap without any manipulation. you can also Left() the soundex() value to get broader and broader matches, though you'll end up with all names that start with a the first letter if you goto left(soundex(name),1) matches.
In addition to Andrew's comment, you can also approach it with a single self-join with an OR-linked condition to each column you want to check:
ON Col1=Col1
OR Col2=Col2
OR Col3=Col3
etc...
Then the extra column you want with the kind of matching would be a massive CASE expression with a WHEN..THEN for every possible combination that you would want to see in this column.
WHEN Col1=Col1 and Col2<>Col2 and Col3<>Col3 THEN 'Col1'
WHEN Col1=Col1 and Col2=Col2 and Col3<>Col3 THEN 'Col1, Col2'
etc...
In the above example, I am assuming none of the columns can contain NULL, but if they can you'll have to handle that in the logic as well.
So I'm storing the user's birthday and month. I'm sending them birthday deals and these deals expire each at a different interval, that is "number of days after their birthday".
I have constructed the formula successfully (below is a chunk of it) but am left with this problem:
('2015-10-10'::date >= make_date(2015, users.birth_month, users.birth_day) ...
If the user was born on Feb 29, make_date would raise an exception for invalid years ( ERROR: date field value out of range: 2015-02-29 )
How can I gracefully handle this? (I have other ways of handling this but they require me to give special treatment for leap years)
This can be achieved by using combination of make_date and interval PostgreSQL fucntions (see https://www.postgresql.org/docs/9.6/functions-datetime.html for more info).
The trick is to ensure the date from make_date is always valid. This can be easily achieved by using the 1st day of the month and then use interval (minus 1 day) to increment the desired number of days (while avoiding ERROR: date field value out of range errors).
This is exemplified by creating a little table users: -
create table users (
id serial primary key,
birth_day integer,
birth_month integer
);
... and populating it with some data ...
insert into users (birth_day, birth_month) values
(1, 2),
(28, 2),
(29, 2),
(30, 2),
(31, 2);
If the year is 2015 you can run the following query: -
select birth_day,
birth_month,
make_date(2015, birth_month, 1) + (birth_day - 1) as birthday
from users;
╔═══════════╦═════════════╦══════════════╗
║ birth_day ║ birth_month ║ birthday ║
╠═══════════╬═════════════╬══════════════╣
║ 1 ║ 2 ║ "2015-02-01" ║
║ 28 ║ 2 ║ "2015-02-28" ║
║ 29 ║ 2 ║ "2015-03-01" ║ <---- wrapped to march 1st
║ 30 ║ 2 ║ "2015-03-02" ║
║ 31 ║ 2 ║ "2015-03-03" ║
╚═══════════╩═════════════╩══════════════╝
However, if the year is 2016 (leap year) then it's: -
select birth_day,
birth_month,
make_date(2016, birth_month, 1) + (birth_day - 1) as birthday
from users;
╔═══════════╦═════════════╦══════════════╗
║ birth_day ║ birth_month ║ birthday ║
╠═══════════╬═════════════╬══════════════╣
║ 1 ║ 2 ║ "2016-02-01" ║
║ 28 ║ 2 ║ "2016-02-28" ║
║ 29 ║ 2 ║ "2016-02-29" ║
║ 30 ║ 2 ║ "2016-03-01" ║ <---- wrapped to march 1st
║ 31 ║ 2 ║ "2016-03-02" ║
╚═══════════╩═════════════╩══════════════╝
Thanks a_horse_with_no_name for pointing out that you don't need to use interval if it's days - simple addition is all you need i.e. + ((birth_day - 1) || ' days')::interval can be simplified to + (birth_day - 1)
Okay, I have added a special case for when the birthday is 02-29. This would solve my problem but I would totally love to hear other suggestions.
('2015-10-10'::date >=
case when users.birth_month = 2 and users.birth_day = 29 make_date(2015,2, 28)
else make_date(2015, users.birth_month, users.birth_day)
end
)
The user in this case would lose a day (I can make them win a day by doing march 1st) but you get the point =)
I'm trying to replace some empty (NULL) fields, which I get as a result of my query, with any string I want. Those empty fields are placed in a "timestamp without timezone" column. So I tried to use COALESCE function, but no result (I got error: invalid input syntax for timestamp: "any_string":
select column1, coalesce(date_trunc('seconds', min(date)), 'any_string') as column2
What could be wrong?
Table:
╔════╦═════════════════════╦═════════════════════╗
║ id ║ date ║ date2 ║
╠════╬═════════════════════╬═════════════════════╣
║ 1 ║ 2013-12-17 13:54:59 ║ 2013-12-17 09:03:31 ║
║ 2 ║ 2013-12-17 13:55:07 ║ 2013-12-17 09:59:11 ║
║ 3 ║ 2013-12-17 13:55:56 ║ empty field ║
║ 4 ║ 2013-12-17 13:38:37 ║ 2013-12-17 09:14:01 ║
║ 5 ║ 2013-12-17 13:54:46 ║ empty field ║
║ 6 ║ 2013-12-17 13:54:46 ║ empty field ║
║ 7 ║ 2013-12-17 13:55:40 ║ empty field ║
╚════╩═════════════════════╩═════════════════════╝
Sample query:
select q1.id, q2.date, q3.date2
from (select distinct id from table1) q1
left join (select id, date_trunc('seconds', max(time)) as date from table2 where time::date = now()::date group by id) q2 on q1.id = q2.id
left join (select id, date_trunc('seconds', min(time2)) as date2 from table1 where time2:date = now()::date group by id) q3 on q1.id = q3.id
order by 1
And the matter is to replace those empty field above with any string I imagine.
You can simply cast timestamp to text using ::text
select column1, coalesce(date_trunc('seconds', min(date))::text, 'any_string') as column2
The date_trunc() function returns a timestamp, thus you cannot fit a string like any_string in the same column.
You'll have to pick a format and convert the resulting date to string, though of course it'll no longer be usable as date.
the coalesce function only works on the integer data type. It will not work on any other data type .
In one of cases I used to convert a varchar data type to integer inside the coalesce function by using _columnName_ :: integer syntax .
But in your case i dont think so that time stamp will be converted to the integer data type.