Behavior of VALUES clause in PostgreSQL - sql

In Postgres, according to its doc, the following works:
select 1 as column1, 'one' as column2
union all
select 2, 'two'
union all
select 3, 'three'
however its extension:
select * from (select 1 as column1, 'one' as column2
union all
select 2, 'two'
union all
select 3, 'three')
results in error:
Similarly, while this works (assuming Postgres creates internal alias?):
values(1,'a'), (2, 'b')
the following results in error, requiring an alias:
select * from (values(1,'a'), (2, 'b'))
and only starts working when such alias is provided:
select * from (values(1,'a'), (2, 'b')) t(z,y)
Same inconsistency seems to also apply to SELECT clause, i.e.:
select 1, 2
works independently but not as a subselect:
select * from (select 1, 2)
unless provided with an alias:
select * from (select 1, 2) t(a, b)
Would it not be a logical extension of the behavior of clauses dealing with sets (SELECT, VALUES, etc.) to assume some internal alias when used inside another select like Postgres already does when use those clauses independently?
What was the reasoning that lead to such inconsistency in design?

The syntax for using VALUES with select is certainly different than how it used in most flavors of SQL with insert. That being said, the issue here with Postgres actually seems to be that the VALUES clause needs to be wrapped in parentheses as a subquery in order for it to be eligible for use with select (it won't work otherwise). Therefore, we can think of the following as being equivalent to any other subquery:
select *
from
(
values (1,'a'), (2, 'b')
) t;
If the VALUES clause were replaced with a select, we would have to alias the subquery, and the same holds true with VALUES. As to why Postgres chose to do this, you might have to check the documentation or ask a question on their forum.

Related

Comparing values returned by query to another query to see if they exist in both

I'm writing a query that needs to check if the results of one query (each distinct value) don't exist in another query (that queries distinct values).
For example,
If Query1 results are like: (1, 2, 3, 4) and Query2 results are (1, 2, 5), I need to return a fail message that I've written.
In another example, if Query1 results are something like: (1, 1, 1, 2, 3) and Query2 results are: (1, 2, 3), then I won't return a message.
Currently, I've ran the query using a TOP 1, but that only checks to see if the top records are equal. How would I go through to check that each result is matching? If I can use a COUNT function to do it, that would be really beneficial as well.
Here's what I have:
SELECT TOP 1 #result = t1.result FROM results t1 WHERE t1.id LIKE '%00%'
SELECT TOP 1 #result2 = t1.result FROM results t1 WHERE t1.id LIKE '%11%'
IF ISNULL(#result, '') <> ISNULL(#result, '')
PRINT 'ERROR'
I've been able to use a COUNT with and EXCEPT to check if they match. However, I'm not sure if the result is correct or if it's what I'm looking for.
SELECT COUNT(*) FROM results t1 WHERE t1.id LIKE '%00%'
EXCEPT
SELECT COUNT(*) FROM results t1 WHERE t1.id LIKE '%11%'
Using Except would seem appropriate, if I understand your question, but you don't want to compare the counts, you want to compare the rows, perhaps something like
select Result = case when exists (
select distinct <col> from results where...
except
select distinct <col> from results where...
) then 'Error' else 'No error' end;
I'm not sure if you need to be "2-way" but you might also want to union the opposite too.

SQL - expand dataset into lookup table?

I currently have a legacy table that looks like the one below.
This is a set of rules that our business has stored over the years. the issue is the "all" and "both" values really should be separated out into rows so they can be queried more efficiently.
For example, the contract length column can only ever be between 1 and 5, the type column can only ever be "gas" or "water" and the sales channel "internal" or "external". Instead of saying all or both, another row should exist with the specific rule and the table should look like the below.
So this will have a row for every variation in the first table.
I didn't think it would be a long task to manually do myself. but I was wrong :)
Does anyone have any idea on how to achieve this quickly in SQL? I would say what I have tried so far...but I am completely stumped on this one so am wondering if it can even be done at all?
This could be done in a single sql statement, but for the sake of your mental health and the ability to check interim result sets before you get to the final output is probably a lot healthier and less risky.
I would approach this with a UNION query, one set of UNIONs for each column that should be split out to more granular rows.
For instance for contractlength:
SELECT Supplier, 1, Type, SalesChannel FROM yourtable WHERE contractLength in ('1', 'All')
UNION ALL
SELECT Supplier, 2, Type, SalesChannel FROM yourtable WHERE contractLength in ('2', 'All')
UNION ALL
SELECT Supplier, 3, Type, SalesChannel FROM yourtable WHERE contractLength in ('3', 'All')
UNION ALL
SELECT Supplier, 4, Type, SalesChannel FROM yourtable WHERE contractLength in ('4', 'All')
UNION ALL
SELECT Supplier, 5, Type, SalesChannel FROM yourtable WHERE contractLength in ('5', 'All')
You can write those results out to a temp table, and then build your query for type on top of it writing to a new temp table.
SELECT Supplier, contractLength, 'Gas', SalesChannel FROM previousTempTable WHERE type in ('Gas','Both')
UNION ALL
SELECT Supplier, contractLength, 'Water', SalesChannel FROM previousTempTable WHERE type in ('Gas','Both')
Rinse and repeat for SalesChannel.
There's other more elegant ways to solve this with some SELECT DISTINCT and cross joins, but your list of values for each column is limited and this solution I'm proposing feels like a quick easy way to get your data in shape. It's also easy to understand if this is auditable data or the process needs to be repeated.
You don't need to query your table multiple times, or use temp tables. You can do this pretty elegantly with conditional unpivots, by using CROSS APPLY
SELECT
t.Supplier,
c1.ContractLength,
c2.Type,
c3.SalesChannel
FROM YourTable t
CROSS APPLY (
SELECT t.ContractLength
WHERE t.ContractLength <> 'All'
UNION ALL
SELECT *
FROM (VALUES
(1),(2),(3),(4),(5)
) v(ContractLength)
WHERE t.ContractLength = 'All'
) c1
CROSS APPLY (
SELECT t.Type
WHERE t.Type <> 'Both'
UNION ALL
SELECT *
FROM (VALUES
('Gas'),('Water')
) v(Type)
WHERE t.Type = 'Both'
) c2
CROSS APPLY (
SELECT t.SalesChannel
WHERE t.SalesChannel <> 'Both'
UNION ALL
SELECT *
FROM (VALUES
('Internal'),('External')
) v(SalesChannel)
WHERE t.SalesChannel = 'Both'
) c3;
A somewhat less efficient, but more compact, version of the same, is to use normal joins against the VALUES clauses
SELECT
t.Supplier,
c1.ContractLength,
c2.Type,
c3.SalesChannel
FROM YourTable t
JOIN (VALUES
(1),(2),(3),(4),(5)
) c1(ContractLength)
ON c1.ContractLength = t.ContractLength OR t.ContractLength = 'All'
JOIN (VALUES
('Gas'),('Water')
) c2(Type)
ON c2.Type = t.Type OR t.Type = 'Both'
JOIN (VALUES
('Internal'),('External')
) c3(SalesChannel)
ON c3.SalesChannel = t.SalesChannel OR t.SalesChannel = 'Both';

Select where clause by exact numbers

I can use WHERE CLAUSE IN when my COLUMN1 datatype is CHARACTER:
SELECT * FROM TABLE1
WHERE COLUMN1 IN ('A', 'B', 'C');
But when I tried to use WHERE CLAUSE IN when my COLUMN2 datatype is NUMERIC, I will have Syntax Error. As below statement:
SELECT * FROM TABLE2
WHERE COLUMN2 IN (1,2,3);
I know there is a BETWEEN but it's not fulfill what I need as I'm not selecting in Range but in Exact number series.
Thanks.
The one problem that I can think of is when column1 is not a numeric type. This will return an error when you try to compare to an integer (such as using in).
You can try the following to mimic the error:
select *
from (select cast('1' as varchar(255)) as val) t1
where val in (1, 2);
Be careful to have constants be the correct type.

How to create an empty anonymous table in Postgres?

In Postgres if I want to create an "anonymous table" (i.e. a temporary query based on data not in the database) I can use VALUES, for example:
select * from (values (1, 'Hello world'), (100, 'Another row')) as foo (mycol1, mycol2);
But how can I create an anonymous table with no rows? (This is for a code generator, so the question isn't quite as odd as it sounds!). The following does not work
select * from (values ) as foo (mycol1, mycol2);
because I get
ERROR: syntax error at or near ")"
LINE 1: select * from (values ) as foo (mycol1, mycol2);
^
I know a work around
select * from (values (NULL, NULL)) as foo (mycol1, mycol2) where mycol1 is not NULL;
but is there a better or "more official" way?
(I would also be interested to know if it is possible to create a table with no columns!)
I think you can do something like this:
select null::text as a, null::int as b
limit 0
SELECT *
FROM generate_series(0, -1)

Using tuples in SQL "IN" clause

I have a table containing the fields group_id and group_type and I want to query the table for all the records having any tuple (group id, group type) from a list of tuples. For example, I want to be able to do something like:
SELECT *
FROM mytable
WHERE (group_id, group_type) IN (("1234-567", 2), ("4321-765", 3), ("1111-222", 5))
A very similar question is already asked at: using tuples in sql in clause , but the solution proposed there presumes the tuple list is to be fetched from another table. This doesn't work in my case is the tuple values are hard coded.
One solution is to use string concatenation:
SELECT *
FROM mytable
WHERE group_id + STR(group_type, 1) IN ("1234-5672", "4321-7653", "1111-2225")
But the problem is that the table is quite big and doing a string concatenation and conversion for each record would be very expensive.
Any suggestion?
Given a very minor tweak (replace double quotes with single and add the VALUES keyword), your proposed syntax is valid Standard SQL-92 syntax i.e.
SELECT *
FROM mytable
WHERE (group_id, group_type) IN (
VALUES ('1234-567', 2),
('4321-765', 3),
('1111-222', 5)
);
Sadly, MSFT have not added it to SQL Server and consider it an 'unplanned' feature.
FWIW PostgreSQL and Sqlite are examples of SQL products that support this syntax.
In SQL Server 2008 you can do like this:
select *
from mytable as T
where exists (select *
from (values ('1234-567', 2),
('4321-765', 3),
('1111-222', 5)) as V(group_id, group_type)
where T.group_id = V.group_id and
T.group_type = V.group_type
)
EDIT: this is a dated answer, although it was the accepted answer in 2011, other answers with more upvotes reflect more recent approaches.
Why not construct the OR statements?
SELECT *
FROM mytable
WHERE (group_id = '1234-567' and group_type = 2)
OR (group_id = '4321-765' and group_type = 3)
OR (group_id = '1111-222' and group_type = 5)
Granted, it doesn't look as nice and neat as your concept example but it will do the job (and if you IN with tuples did exist, it would implement it exactly the same way under the covers most likely.
You can use a common table expression to pretend that these tuples are in another table:
;WITH Tuples as (
select '1234-567' as group_id, 2 as group_type union all
select '4321-765', 3 union all
select '1111-222', 5
)
SELECT * /* TODO - Pick appropriate columns */
from mytable m where exists (
select * from Tuples t
where m.group_id = t.group_id and m.group_type = t.group_type)
Using that solution, this should work:
SELECT *
FROM mytable m
WHERE EXISTS (
SELECT * FROM (
SELECT "1234-567" group_id, 2 group_type UNION ALL
SELECT "4321-765", 3 UNION ALL
SELECT "1111-222", 5) [t]
WHERE m.group_id = t.group_id AND m.group_type = t.group_type)
BTW, you should probably use a CTE to create that inner table.
I haven't seen this yet, but something like this should work
SELECT * FROM AgeGroup ag JOIN
(VALUES
('18-24', 18, 24),
('25-34 ', 25, 39),
('35-44 ', 35, 49),
('45-54 ', 45, 59),
('55-64 ', 55, 69),
('65+ ', 65, 299)
) AS x (agegroup, minage, maxage)
ON ag.age_group = x.agegroup
AND ag.min_age=x.minage
AND ag.max_age=x.maxage
Here is another tuple solution using a join:
SELECT
*
FROM mytable m
JOIN
(
SELECT "1234-567" group_id, 2 group_type
UNION ALL SELECT "4321-765", 3
UNION ALL SELECT "1111-222", 5
) [t]
ON m.group_id = t.group_id
AND m.group_type = t.group_type
I had a similar problem but my tuple collection was dynamic - it was sent over to the SQL Server in a query parameter. I came up with the following solution:
Pass a tuple as an XML:
DECLARE #tuplesXml xml = '<tuples><tuple group-id="1234-567" group-type="2"/><tuple group-id="4321-765" group-type="3"/></tuples>';
Inner join the table that you want to filter with the XML nodes:
SELECT t.* FROM mytable t
INNER JOIN #tuplesXml.nodes('/tuples/tuple') AS tuple(col)
ON tuple.col.value('./#group-id', 'varchar(255)') = t.group_id
AND tuple.col.value('./#group-type', 'integer') = t.group_type
It seems to work fine in my situation which is a bit more complex than the one described in the question.
Keep in mind that it is necessary to use t.* instead of * and the table returned from nodes method needs to be aliased (it's tuple(col) in this case).
select * from table_name where 1=1 and (column_a, column_b) in ((28,1),(25,1))