Drupal 8 and PostgreSQL: How can I use the result of an expression inside a WHERE clause? - postgresql

Hi there!
I construct a database query using Drupal's query interface. With that, I also successfully add an expression that generates a conditional value. The contents of that conditional field is a ISO formatted datestring.
I now would like to use that computed field inside my WHERE clause. In another project using MySQL I was able to just use the name carrying the expression result inside the where-clause, but that does not seem to work in PostgreSQL. I always get the "column does not exist"-error, wheter if I prefix the expression name with the database table alias or not.
The (raw and shortened) query looks like the following (still contains the value placeholders):
SELECT
base.nid AS nid,
base.type AS type,
date.field_date_value AS start,
date.field_date_end_value AS "end",
CASE WHEN COALESCE(LENGTH(date.field_date_end_value), 0) > 0 THEN date.field_date_end_value ELSE date.field_date_value END AS datefilter
FROM {node} base
LEFT JOIN {node__field_date} date ON base.type = date.bundle AND base.nid = date.entity_id AND base.vid = date.revision_id AND attr.langcode = date.langcode AND date.deleted = 0
WHERE (base.type = :db_condition_placeholder_0) AND (datefilter > :db_condition_placeholder_2)
ORDER BY field_date_value ASC NULLS FIRST
As already stated, it does not make any difference if I apply the filter using "date.datefilter" or just "datefilter". The field/expression turns out just fine on the result list if I do not attempt to use it in the WHERE part. So, how can I use the result of the expression to filter the query result?
Any hint is appreciated! Thanks in advance!

You can try to create a derived table that defines the new column "datefilter".
For example instead of coding:
select case when c1 > 0 then c1 else -c1 end as c, c2, c3
from t
where c = 0;
ERROR: column "c" does not exist
LINE 1: ...c1 > 0 then c1 else -c1 end as c, c2, c3 from t where c = 0;
Define a derived table that defines the new column and used it in a outer SELECT:
select c, c2, c3
from
(select case when c1 > 0 then c1 else -c1 end as c, c2, c3 from t) as t2
where c=0;

Related

What does a column assignment using an aggregate in the columns area of a select do?

I'm trying to decipher another programmer's code who is long-gone, and I came across a select statement in a stored procedure that looks like this (simplified) example:
SELECT #Table2.Col1, Table2.Col2, Table2.Col3, MysteryColumn = CASE WHEN y.Col3 IS NOT NULL THEN #Table2.MysteryColumn - y.Col3 ELSE #Table2.MysteryColumn END
INTO #Table1
FROM #Table2
LEFT OUTER JOIN (
SELECT Table3.Col1, Table3.Col2, Col3 = SUM(#Table3.Col3)
FROM Table3
INNER JOIN #Table4 ON Table4.Col1 = Table3.Col1 AND Table4.Col2 = Table3.Col2
GROUP BY Table3.Col1, Table3.Col2
) AS y ON #Table2.Col1 = y.Col1 AND #Table2.Col2 = y.Col2
WHERE #Table2.Col2 < #EnteredValue
My question, what does the fourth column of the primary selection do? does it produce a boolean value checking to see if the values are equal? or does it set the #Table2.MysteryColumn equal to some value and then inserts it into #Table1? Or does it just update the #Table2.MysteryColumn and not output a value into #Table1?
This same thing seems to happen inside of the sub-query on the third column, and I am equally at a loss as to what that does as well.
MysteryColumn = gives the expression a name also called a column alias. The fact that a column in the table#2 also has the same name is besides the point.
Since it uses INTO syntax it also gives the column its name in the resulting temporary table. See the SELECT CLAUSE and note | column_alias = expression and the INTO CLAUSE

How do I convert a character to integer within a PostgreSQL (9.1) function?

I have this following code:
BEGIN
x := split_part(text, ',', 1);
UPDATE albumphoto SET order = 1 WHERE idtable = 1 AND idx = x;
END
But my column table named idx is a numeric type, and the split_part returns a character type to the variable x. I've tried using CAST, but I don't know how to use it properly.
Any ideas?
Like this:
UPDATE albumphoto SET order = 1 WHERE idtable = 1 AND idx = CAST (x AS INTEGER);
(Use appropriate numeric type instead of INTEGER).
Or simpler:
UPDATE albumphoto
SET "order" = 1
WHERE idtable = 1
AND idx = split_part(text, ',', 1)::int -- cast to actual type (if not a string type)
AND "order" IS DISTINCT FROM 1;
expression::type is the simple (non-SQL-standard) Postgres way to cast. Details in the manual in the chapter Type Casts.
More about data types in PostgreSQL.
The last predicate I added is useful if "order" could already be 1, in which case the update wouldn't change anything - at full cost. Rather do nothing instead. Related (see last paragraph):
How do I (or can I) SELECT DISTINCT on multiple columns?
And you don't need a variable.

Multiple ordering options

It is very common for a web page to have multiple ordering options for a table. Right now I have a case where there are 12 options (ordenable columns). The easiest (that I know of) way to do it is to build the SQL query concatenating strings. But I'm wondering if it is the best approach. The string concatenation is something like this (python code):
order = {
1: "c1 desc, c2",
2: "c2, c3",
...
12: "c10, c9 desc"
}
...
query = """
select c1, c2
from the_table
order by %(order)s
"""
...
cursor.execute(query, {'order': AsIs(order[order_option])})
...
My alternative solution until now is to place a series of cases in the order by clause:
select c1, c2
from the_table
order by
case %(order_option)s
when 1 then array[c1 * -1, c2]
when 2 then array[c2, c3]
else [0.0, 0.0]
end
,
case %(order_option)s
when 3 then c4
else ''
end
,
...
,
case when %(order_option)s < 1 or %(order_option)s > 12 then c5 end
;
What is the best practice concerning multiple ordering choices? What happens with index utilization in my alternative code?
First of all, #order is not valid PostgreSQL syntax. You probably borrowed the syntax style from MS SQL Server or MySQL. You cannot use variables in a plain SQL query like that.
In PostgreSQL you would probably create a function. You can use variables there, just drop the #.
Sorting by ARRAY is generally rather slow - and not necessary in your case. You could simplify to:
ORDER BY
CASE _order
WHEN 1 THEN c2
WHEN 2 THEN c3 * -1
ELSE NULL -- undefined!
END
, c1
However, a CASE expression like this cannot use plain indexes. So, if you are looking for performance, one way (of several) would be a plpgsql function like this:
CREATE OR REPLACE FUNCTION foo(int)
RETURNS TABLE(c1 int, c2 int) AS
$BODY$
BEGIN
CASE $1
WHEN 1 THEN
RETURN QUERY
SELECT t.c1, t.c2
FROM tbl t
ORDER BY t.c2, t.c1;
WHEN 2 THEN
RETURN QUERY
SELECT t.c1, t.c2
FROM tbl t
ORDER BY t.c3 DESC, t.c1;
ELSE
RAISE WARNING 'Unexpected parameter: "%"', $1;
END CASE;
END;
$BODY$
LANGUAGE plpgsql STABLE;
This way, even plain indexes can be used.
If you actually only have two alternatives for ORDER BY, you could also just write two
functions.
Create multi-column indexes on (c2, c1) and (c3 DESC, c1) for maximum performance. But be aware that maintaining indexes carries a cost, too, especially if your table sees a lot of write operations.
Additional answer for rephrased question
As I said, the CASE construct will not use plain indexes. Indexes on expressions would be an option, but what you have in your example is outside the scope.
So, if you want performance, build the query in your app (your first approach) or write a server side function (possibly with dynamic SQL and EXECUTE) that does something similar inside PostgreSQL. The WHERE clause with a complex CASE statement works, but is slower.

SQL Server cast varchar to int

I have a table that has a column 'Value' that is a varchar. One row puts a '10' in this column. This "number" will need to be added and substracted to, but I can do so directly b/c its a varchar.
So, the following gives an error:
update Fields
set Value = Value - 1
from Fields f, FTypes ft
where ft.Name = 'Field Count'
and ft.ID = f.ID_FT
and f.ID_Project = 186
GO
How do I cast/convert the value to an int, perform the math, then set as a varchar again?
Martin Smith's point is an excellent one --> If it is only numeric data going in there and you are always going to be doing operations like this, it will save you time and hassle not having to do this conversion work.
That being said you can do -
update Fields
set ColumnName = cast( (cast(ColumnName as int) - 1) as varchar(nn))
from Fields f, FTypes ft
where ft.Name = 'Field Count'
and ft.ID = f.ID_FT
and f.ID_Project = 186
where nn is the original definition of your varchar column
You need to use CAST twice - once to make your Value column an INT so you can subtract 1 from it, and then back to a VARCHAR(x):
update dbo.Fields
set Value = CAST((CAST(Value AS INT) - 1) AS VARCHAR(20))
from dbo.Fields f
inner join dbo.FTypes ft ON ft.ID = f.ID_FT
where ft.Name = 'Field Count'
and f.ID_Project = 186
Also, I would recommend using the dbo. prefix always, on all your database objects, and I would always argue for the new, ANSI standard JOIN syntax which is more expressive (clearer to read and understand) and helps avoid unwanted cartesian products (by forgetting to specify a JOIN condition in the WHERE clause....)

updating a table in tsql with multiple conditions

If I have a table MyTable with columns a,b and c, which are ints. Given that I want to update all 'a's based on the values of b and c.
Update MyTable set a = 2 where b = 1 and c = 1
It's far too late, and I cannot for the life of me see why this statement doesn't work, am I missing something silly?
Edit, woops, forgot the error.
"Subquery returned more than 1 value. This is not permitted when the subquery follows =, !=, <, <= , >, >= or when the subquery is used as an expression."
Edit2: That was the exact query I was using (different column names). Turns out there was a trigger on the table which was broken. I feel a little silly now, but thanks for the help anyway :)
There's nothing wrong with the statement you posted. The error is elsewhere.
Could you have posted the wrong query? Or perhaps you over-simplified it? A subquery looks something like this:
UPDATE MyTable
SET a = 2
WHERE b = 1 AND c = (SELECT c FROM MyTable2 WHERE id = 5)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ <--- subquery
An invalid query that could give the error message you get could look like this:
UPDATE MyTable
SET a = 2
WHERE b = 1 AND c = (SELECT c, d FROM MyTable2 WHERE id = 5)
The second query is invalid because it returns two values but the = operator only allows comparison to a single value.
The solution is to ensure that all subqueries used in equality comparisons only return a single row consisting of a single column.