PostgreSQL: replace generate_series with an array - postgresql

I have a working SQL code that creates geometries according to numbers generated from a generate_series function:
CREATE TEMPORARY TABLE catchments ON COMMIT DROP AS (
SELECT lims, ST_ConcaveHull(the_geom, alpha_factor) AS the_geom_overlap FROM (
SELECT lims, ST_MakeValid(ST_Collect(n.the_geom)) AS the_geom
FROM generate_series(1, 10, 2) AS lims, pgr_drivingDistance(
'SELECT id, source, target, cost, reverse_cost FROM edges',
vertex_id, lims, true
) a, nodes n WHERE a.node = n.vid
GROUP BY lims
) AS conv_hull
ORDER BY lims DESC
);
Now I need to replace the fixed interval series by an array with varying intervals, e.g. [1, 2, 5, 7, 8].
Is there a simple way to "convert" the generate_series by an array with the same logic? I would like to avoid using a for loop if possible.

FROM unnest(ARRAY[1,2,5,7,8]) AS lims
should do it.

Related

Postgres - Repeating an element N times as array

For example, where the element is 'hi', and where N is 3, I need a PostgreSQL snippet I can use in a SELECT query that returns the following array:
['hi', 'hi', 'hi']
Postgres provides array_fill for this purpose, e.g.:
SELECT array_fill('hi'::text, '{3}');
SELECT array_fill('hi'::text, array[3]);
The two examples are equivalent but the 2nd form is more convenient if you wish to replace the dimension 3 with a variable.
See also: https://www.postgresql.org/docs/current/functions-array.html
You may use array_agg with generate_series
select array_agg(s) from ( values('hi')) as t(s) cross join generate_series(1,3)
Generic
select array_agg(s) from ( values(:elem)) as t(s) cross join generate_series(1,:n)
DEMO
sql demo
with cte as (
select 'hi' as rep_word, generate_series(1, 3) as value
) -- ^^^ n = 3
select array(SELECT rep_word::text from cte);

PostgreSQL, Variables

I am using PostgreSQL through Npgsql driver for windows/NET and I see that it is possible to use PL/pgSQL language through it.
So that way I can make use of variables for my calculation scripts which may look like in this example:
DO $$
DECLARE
tlist text='mylistofbills';
tcontent text='mycontentofbills';
BEGIN
CREATE TEMP TABLE tlist
(billno integer, bdate timestamp, rebate double precision)
ON COMMIT DROP;
INSERT INTO tlist
VALUES (1, '10.01.2017. 10:14:56', 10),
(2, '10.01.2017. 11:02:13', 5),
(3, '10.01.2017. 11:45:22', 0),
(4, '10.01.2017. 12:01:01', 6);
CREATE TEMP TABLE tcontent
(billno integer, rowno integer, price double precision, tax double precision)
ON COMMIT DROP;
INSERT INTO tcontent
VALUES (1, 1, 100, 19),
(1, 2, 30, 0),
(2, 1, 20, 19),
(3, 1, 18, 19),
(4, 1, 43, 0);
END $$;
SELECT s.price,
l.rebate,
s.price/100*l.rebate AS valrebate,
s.price-(s.price/100*l.rebate) AS worebate,
((s.price-(s.price/100*l.rebate))/100)*s.tax AS valtax,
s.price-(s.price/100*l.rebate)+(((s.price-(s.price/100*l.rebate))/100)*s.tax) AS finalprice
FROM tlist l, tcontent s
WHERE l.billno=s.billno;
Example is simplified (from real situation) and is suitable for pasting into PgAdmin's SQL editor.
So, now is question: Can I somehow in the body of those code, without adding new functions to server use formulas for writing more elegant and readable code?
If I would be able to add simple formulas like:
rebatec=s.price/100*l.rebate
priceworebate=s.price-rebatec
Then my code may look more readable and less error prone.
Like this:
SELECT s.price,
l.rebate,
rebatec AS valrebate,
priceworebate AS worebate,
(priceworebate/100)*s.tax AS valtax,
priceworebate+((priceworebate/100)*s.tax) AS finalprice
FROM tlist l, tcontent s
WHERE l.billno=s.billno;
If that may be possible where and how to put this formulas so it can be used in my last SELECT code?
SOLUTION:
Based on #Clodoaldo's answer which give something new to me I find a solution which I am able to understand:
SELECT s.price,
l.rebate,
rebatec AS valrebate,
priceworebate AS worebate,
priceworebate/100*s.tax AS valtax,
priceworebate+priceworebate/100*s.tax AS finalprice
FROM tlist l, tcontent s, LATERAL
(SELECT s.price/100*l.rebate AS rebatec,
s.price-s.price/100* l.rebate AS priceworebate
)sub
WHERE l.billno=s.billno;
It works and I hope it is technically correct.
Use lateral:
The LATERAL key word can precede a sub-SELECT FROM item. This allows the sub-SELECT to refer to columns of FROM items that appear before it in the FROM list.
select
s.price,
l.rebate,
rebatec as valrebate,
priceworebate as worebate,
priceworebate / 100 * s.tax as valtax,
priceworebate + priceworebate / 100 * s.tax as finalprice
from
tlist l
inner join
tcontent s using (billno)
cross join lateral (
select
s.price / 100 * l.rebate as rebatec,
s.price - s.price / 100 * l.rebate as priceworebate
) cjl
Use the modern join syntax.
You could use a subquery to define those variables:
select var1 * col3
from (
select col1 / col2 as var1
, *
from YourTable
) sub
Or alternatively a common table expression:
with cte as
(
select col1 / col2 as var1
, *
from YourTable
)
select var1 * col3
from cte

How to rewrite SQL joins into window functions?

Database is HP Vertica 7 or PostgreSQL 9.
create table test (
id int,
card_id int,
tran_dt date,
amount int
);
insert into test values (1, 1, '2017-07-06', 10);
insert into test values (2, 1, '2017-06-01', 20);
insert into test values (3, 1, '2017-05-01', 30);
insert into test values (4, 1, '2017-04-01', 40);
insert into test values (5, 2, '2017-07-04', 10);
Of the payment cards used in the last 1 day, what is the maximum amount charged on that card in the last 90 days.
select t.card_id, max(t2.amount) max
from test t
join test t2 on t2.card_id=t.card_id and t2.tran_dt>='2017-04-06'
where t.tran_dt>='2017-07-06'
group by t.card_id
order by t.card_id;
Results are correct
card_id max
------- ---
1 30
I want to rewrite the query into sql window functions.
select card_id, max(amount) over(partition by card_id order by tran_dt range between '60 days' preceding and current row) max
from test
where card_id in (select card_id from test where tran_dt>='2017-07-06')
order by card_id;
But result set does not match, how can this be done?
Test data here:
http://sqlfiddle.com/#!17/db317/1
I can't try PostgreSQL, but in Vertica, you can apply the ANSI standard OLAP window function.
But you'll need to nest two queries: The window function only returns sensible results if it has all rows that need to be evaluated in the result set.
But you only want the row from '2017-07-06' to be displayed.
So you'll have to filter for that date in an outer query:
WITH olap_output AS (
SELECT
card_id
, tran_dt
, MAX(amount) OVER (
PARTITION BY card_id
ORDER BY tran_dt
RANGE BETWEEN '90 DAYS' PRECEDING AND CURRENT ROW
) AS the_max
FROM test
)
SELECT
card_id
, the_max
FROM olap_output
WHERE tran_dt='2017-07-06'
;
card_id|the_max
1| 30
As far as I know, PostgreSQL Window function doesn't support bounded range preceding thus range between '90 days' preceding won't work. It does support bounded rows preceding such as rows between 90 preceding, but then you would need to assemble a time-series query similar to the following for the Window function to operate on the time-based rows:
SELECT c.card_id, t.amount, g.d as d_series
FROM generate_series(
'2017-04-06'::timestamp, '2017-07-06'::timestamp, '1 day'::interval
) g(d)
CROSS JOIN ( SELECT distinct card_id from test ) c
LEFT JOIN test t ON t.card_id = c.card_id and t.tran_dt = g.d
ORDER BY c.card_id, d_series
For what you need (based on your question description), I would stick to using group by.

Updating multiple rows in PGSQL to the same value

I apologize if this has been asked before, but I googled and could not come up with a good search to find out.
I have a table that looks like this:
col_a, col_b
a, 1,
a, 2,
a, 3,
b, 1,
b, 2,
c, 1,
I need to update the values in col_a using a function that generates a random code value, but all values of 'a' need to update to the same value, same for 'b' etc. Is there a systematic way to do this, other than:
UPDATE tbl SET col_a = make_code() WHERE col_a = 'a';
UPDATE tbl SET col_a = make_code() WHERE col_a = 'b'; ...
If make_code() truly work like random(), multiple update statements does not guarantee the same code per update statements.
You need to calculate a code before the real update takes place (for each distinct original code). You can do it with a sub-query, or (more readable with) a CTE:
with codes as (
select distinct on (col_a)
col_a original_code,
make_code() generated_code
from tbl
)
update tbl
set col_a = generated_code
from codes
where col_a = original_code;
SQLFiddle

Getting the minimum of two values in SQL

I have two variables, one is called PaidThisMonth, and the other is called OwedPast. They are both results of some subqueries in SQL. How can I select the smaller of the two and return it as a value titled PaidForPast?
The MIN function works on columns, not variables.
SQL Server 2012 and 2014 supports IIF(cont,true,false) function. Thus for minimal selection you can use it like
SELECT IIF(first>second, second, first) the_minimal FROM table
While IIF is just a shorthand for writing CASE...WHEN...ELSE, it's easier to write.
The solutions using CASE, IIF, and UDF are adequate, but impractical when extending the problem to the general case using more than 2 comparison values. The generalized
solution in SQL Server 2008+ utilizes a strange application of the VALUES clause:
SELECT
PaidForPast=(SELECT MIN(x) FROM (VALUES (PaidThisMonth),(OwedPast)) AS value(x))
Credit due to this website:
http://sqlblog.com/blogs/jamie_thomson/archive/2012/01/20/use-values-clause-to-get-the-maximum-value-from-some-columns-sql-server-t-sql.aspx
Use Case:
Select Case When #PaidThisMonth < #OwedPast
Then #PaidThisMonth Else #OwedPast End PaidForPast
As Inline table valued UDF
CREATE FUNCTION Minimum
(#Param1 Integer, #Param2 Integer)
Returns Table As
Return(Select Case When #Param1 < #Param2
Then #Param1 Else #Param2 End MinValue)
Usage:
Select MinValue as PaidforPast
From dbo.Minimum(#PaidThisMonth, #OwedPast)
ADDENDUM:
This is probably best for when addressing only two possible values, if there are more than two, consider Craig's answer using Values clause.
For SQL Server 2022+ (or MySQL or PostgreSQL 9.3+), a better way is to use the LEAST and GREATEST functions.
SELECT GREATEST(A.date0, B.date0) AS date0,
LEAST(A.date1, B.date1, B.date2) AS date1
FROM A, B
WHERE B.x = A.x
With:
GREATEST(value [, ...]) : Returns the largest (maximum-valued) argument from values provided
LEAST(value [, ...]) Returns the smallest (minimum-valued) argument from values provided
Documentation links :
MySQL http://dev.mysql.com/doc/refman/5.0/en/comparison-operators.html
Postgres https://www.postgresql.org/docs/current/functions-conditional.html
SQL Server https://learn.microsoft.com/en-us/sql/t-sql/functions/logical-functions-least-transact-sql
I just had a situation where I had to find the max of 4 complex selects within an update.
With this approach you can have as many as you like!
You can also replace the numbers with aditional selects
select max(x)
from (
select 1 as 'x' union
select 4 as 'x' union
select 3 as 'x' union
select 2 as 'x'
) a
More complex usage
#answer = select Max(x)
from (
select #NumberA as 'x' union
select #NumberB as 'x' union
select #NumberC as 'x' union
select (
Select Max(score) from TopScores
) as 'x'
) a
I'm sure a UDF has better performance.
Here is a trick if you want to calculate maximum(field, 0):
SELECT (ABS(field) + field)/2 FROM Table
returns 0 if field is negative, else, return field.
Use a CASE statement.
Example B in this page should be close to what you're trying to do:
http://msdn.microsoft.com/en-us/library/ms181765.aspx
Here's the code from the page:
USE AdventureWorks;
GO
SELECT ProductNumber, Name, 'Price Range' =
CASE
WHEN ListPrice = 0 THEN 'Mfg item - not for resale'
WHEN ListPrice < 50 THEN 'Under $50'
WHEN ListPrice >= 50 and ListPrice < 250 THEN 'Under $250'
WHEN ListPrice >= 250 and ListPrice < 1000 THEN 'Under $1000'
ELSE 'Over $1000'
END
FROM Production.Product
ORDER BY ProductNumber ;
GO
This works for up to 5 dates and handles nulls. Just couldn't get it to work as an Inline function.
CREATE FUNCTION dbo.MinDate(#Date1 datetime = Null,
#Date2 datetime = Null,
#Date3 datetime = Null,
#Date4 datetime = Null,
#Date5 datetime = Null)
RETURNS Datetime AS
BEGIN
--USAGE select dbo.MinDate('20120405',null,null,'20110305',null)
DECLARE #Output datetime;
WITH Datelist_CTE(DT)
AS (
SELECT #Date1 AS DT WHERE #Date1 is not NULL UNION
SELECT #Date2 AS DT WHERE #Date2 is not NULL UNION
SELECT #Date3 AS DT WHERE #Date3 is not NULL UNION
SELECT #Date4 AS DT WHERE #Date4 is not NULL UNION
SELECT #Date5 AS DT WHERE #Date5 is not NULL
)
Select #Output=Min(DT) FROM Datelist_CTE;
RETURN #Output;
END;
Building on the brilliant logic / code from mathematix and scottyc, I submit:
DECLARE #a INT, #b INT, #c INT = 0;
WHILE #c < 100
BEGIN
SET #c += 1;
SET #a = ROUND(RAND()*100,0)-50;
SET #b = ROUND(RAND()*100,0)-50;
SELECT #a AS a, #b AS b,
#a - ( ABS(#a-#b) + (#a-#b) ) / 2 AS MINab,
#a + ( ABS(#b-#a) + (#b-#a) ) / 2 AS MAXab,
CASE WHEN (#a <= #b AND #a = #a - ( ABS(#a-#b) + (#a-#b) ) / 2)
OR (#a >= #b AND #a = #a + ( ABS(#b-#a) + (#b-#a) ) / 2)
THEN 'Success' ELSE 'Failure' END AS Status;
END;
Although the jump from scottyc's MIN function to the MAX function should have been obvious to me, it wasn't, so I've solved for it and included it here: SELECT #a + ( ABS(#b-#a) + (#b-#a) ) / 2. The randomly generated numbers, while not proof, should at least convince skeptics that both formulae are correct.
Use a temp table to insert the range of values, then select the min/max of the temp table from within a stored procedure or UDF. This is a basic construct, so feel free to revise as needed.
For example:
CREATE PROCEDURE GetMinSpeed() AS
BEGIN
CREATE TABLE #speed (Driver NVARCHAR(10), SPEED INT);
'
' Insert any number of data you need to sort and pull from
'
INSERT INTO #speed (N'Petty', 165)
INSERT INTO #speed (N'Earnhardt', 172)
INSERT INTO #speed (N'Patrick', 174)
SELECT MIN(SPEED) FROM #speed
DROP TABLE #speed
END
Select MIN(T.V) FROM (Select 1 as V UNION Select 2 as V) T
SELECT (WHEN first > second THEN second ELSE first END) the_minimal FROM table