MySQL returning arrays from subqueries, and NULL - mysql

I have two tables, "records", and "info".
The "records" table looks like:
mysql> SELECT * FROM records WHERE num = '7';
+-----+--------+----+------+-----+-----+------------+-----------+----------+---------------------+
| id | city | st | type | num | val | startdate | status | comments | updated |
+-----+--------+----+------+-----+-----+------------+-----------+----------+---------------------+
| 124 | Encino | CA | AAA | 7 | 1 | 1993-09-01 | allocated | | 2014-02-26 08:16:07 |
+-----+--------+----+------+-----+-----+------------+-----------+----------+---------------------+
and so on. Think of the "num" field in this table as a Company ID.
The "info" table contains information about certain companies, and uses that company id as a unique identifier. Not all companies listed in "records" will be in "info". An example of the "info" table:
mysql> SELECT * FROM info LIMIT 2;
+-----+-------+--------------------------+---------------------+
| org | name | description | updated |
+-----+-------+--------------------------+---------------------+
| 0 | ACME | | 2014-02-19 10:35:39 |
| 1 | AT&T | Some Phone Company, Inc. | 2014-02-18 15:29:50 |
+-----+-------+--------------------------+---------------------+
So "org" here will match "num" in the first table.
I want to be able to run a query that returns, on one line, everything but 'id', 'type' and 'val' from the 1st table, and IF APPLICABLE, the 'name' and 'description' from the 2nd table.
I can achieve what I want using this query:
SELECT city,st,num,startdate,status,comments,updated, \
( SELECT name FROM info WHERE org = '7') AS name, \
( SELECT description FROM info WHERE org = '7') AS description \
FROM records WHERE num = '7'
But I see at least two problems with it:
It seems inefficient to run two subqueries
When there is no record in "info", NULL is printed for the name and
description. I would like to print some string instead.
To address the first problem, I tried to return an array. But when no corresponding record exists in the "info" table, then I get nothing, not even the valid info from the "records" table. Here's my array query:
SELECT city,st,num,startdate,status,comments,updated,asinfo.name AS name,asinfo.description AS description \
FROM records, \
( SELECT name,description FROM info WHERE org = '7') AS asinfo \
WHERE num = '7'
This query works fine if a given company id exists in both tables.
To address the second problem, I tried various incantations of IFNULL and coalesce, to no avail.
I'd appreciate any insight.
Thanks.

Apply LEFT JOIN syntax:
SELECT
r.city,
r.st,
r.num,
r.startdate,
r.status,
r.comments,
r.updated,
IF(d.name IS NULL, 'Default', d.name) AS name,
IF(d.description IS NULL, 'Default', d.description) AS description
FROM
records AS r
LEFT JOIN info AS d ON r.num=d.org
WHERE
r.num='7'
that will work such way: LEFT JOIN looks into first table, and, if there are no corresponding records in second, it applies NULL. So you'll discover that with IF (or IFNULL) and do substitution of default string.

Use a LEFT JOIN to get null values when there's no matching row in the info table.
SELECT city,st,num,startdate,status,comments,updated,
IFNULL(name, 'Default Name') name,
IFNULL(description, 'Default Description') description
FROM records r
LEFT JOIN info i ON r.num = i.org
WHERE r.num = 7

It sounds like a simple LEFT JOIN from record to info will do the trick.
LEFT JOIN rather than JOIN in order to ensure you ALWAYS get all rows from the record table, and then the corresponding data in info table if a xref exists for that ID.
Whether using your sub-queries or using joins, if you always want to see all rows in record table, then you will always get NULLs corresponding to the info table where no xref exists. The only way to avoid that is to run some code that calls everything from record, and then iterates over the results to query info, to conditionally add to the record data.

Related

2 inner joins between same 2 tables

I am trying to select columns from 2 tables,
The INNER JOIN conditions are $table1.idaction_url=$table2.idaction AND $table1.idaction_name=$table2.idaction.
However, From the query below, there is no output. It seems like the INNER JOIN can only take 1 condition. If I put AND to include both conditions as shown in the query below, there wont be any output. Please look at the picture below. Please advice.
$mysql=("SELECT conv(hex($table1.idvisitor), 16, 10) as visitorId,
$table1.server_time, $table1.idaction_url,
$table1.time_spent_ref_action,$table2.name,
$table2.type, $table1.idaction_name, $table2.idaction
FROM $table1
INNER JOIN $table2
ON $table1.idaction_url=$table2.idaction
AND $table1.idaction_name=$table2.idaction
WHERE conv(hex(idvisitor), 16, 10)='".$id."'
ORDER BY server_time DESC");
Short answer:
You need to use two separate inner joins, not only a single join.
E.g.
SELECT `actionurls`.`name` AS `actionUrl`, `actionnames`.`name` AS `actionName`
FROM `table1`
INNER JOIN `table2` AS `actionurls` ON `table1`.`idaction_url` = `actionurls`.`idaction`
INNER JOIN `table2` AS `actionnames` ON `table1`.`idaction_name` = `actionurls`.`idaction`
(Modify this query with any additional fields you want to select).
In depth: INNER JOIN, when done on a value unique to the second table (the table joined to the first in this operation) will only ever fetch one row. What you want to do is fetch data from the other table twice, into the same row, reading the select part of the statement.
INNER JOIN table2 ON [comparison] will, for each row selected from table1, grab any rows from table2 for which [comparison] is TRUE, then copy the row from table1 N times, where N is the amount of rows found in table2. If N = 0, then the row is skipped. In our case N=1 so INNER JOIN of idaction_name in table1 to idaction in table2 for example will allow you to select all the action names.
In order to get the action urls as well we have to INNER JOIN a second time. Now you can't join the same table twice normally, as SQL won't know which of the two joined tables is meant when you type table2.name in the first part of your query. This would be ambiguous if both had the same name. There's a solution for this, table aliases.
The output (of my answer above) is going to be something like:
+-----+------------------------+-------------------------+
| Row | actionUrl | actionName |
+-----+------------------------+-------------------------+
| 1 | unx.co.jp/ | UNIX | Kumamoto Home |
| 2 | unx.co.jp/profile.html | UNIX | Kumamoto Profile |
| ... | ... | ... |
+-----+------------------------+-------------------------+
While if you used only a single join, you would get this kind of output (using OR):
+-----+-------------------------+
| Row | actionUrl |
+-----+-------------------------+
| 1 | unx.co.jp/ |
| 2 | UNIX | Kumamoto Home |
| 3 | unx.co.jp/profile.html |
| 4 | UNIX | Kumamoto Profile |
| ... | ... |
+-----+-------------------------+
Using AND and a single join, you only get output if idaction_name == idaction_url is TRUE. This is not the case, so there's no output.
If you want to know more about how to use JOINS, consult the manual about them.
Sidenote
Also, I can't help but notice you're using variables (e.g. $table1) that store the names of the tables. Do you make sure that those values do not contain user input? And, if they do, do you at least whitelist a list of tables that users can access? You may have some security issues with this.
INNER JOIN does not put any restriction on number of conditions it can have.
The zero resultant rows means, there is no rows satisfying the two conditions simultaneously.
Make sure you are joining using correct columns. Try going step by step to identify from where the data is lost

SQL LEFT JOIN "JOIN expression is not supported"

This question relates to one I raised here SQL help in finding missing operations on the Sparx website. They suggested I try StackOverflow so here I am. I had tried to do a lot of research on the problem before posting on the Sparx website. I have slightly tweaked it to make it more of a general SQL issue than fixed to the tool that I am using.
I am not an SQL guru so please be kind to me!
The situation
I have 2 tables that has the following elements
Table t_object
--------------
Object_ID AutoNumber
Object_Type Text
Name Text
ParentID Number
Table t_operation
-----------------
OperationID AutoNumber
Object_ID Number
Name Text
The table t_object contains lots of different types of items. The Object_ID is the unique key. The items that I am interested are where Object_Type = 'class' OR 'activity' OR 'activityparameter'. The Name is the name of the item. The ParentID is only applicable to 'activityparameter' items and is the Object_ID of the 'activity' that the 'activityparameter' belongs to.
The table t_operation contains all operations belonging to a class. The OperationID is the unique key. The Object_ID is how this operation is linked to its class in t_object.
The problem
In our system all classes have an equally named activity and all operations belonging to a class have an equally named activityparameter belonging to the activity.
I am trying to find erroneous entries where operation MyOp in class MyClass does not have an equally named activityparameter MyOp in activity MyClass.
Using the test data below
t_object
+-----------+-------------------+-------+----------+
| Object_ID | Object_Type | Name | ParentID |
+-----------+-------------------+-------+----------+
| 1 | Class | c1 | 0 |
| 2 | Class | c2 | 0 |
| 3 | Activity | c1 | 0 |
| 4 | Activity | c2 | 0 |
| 5 | ActivityParameter | MyOp1 | 3 |
| 6 | ActivityParameter | MyOp2 | 3 |
| 7 | ActivityParameter | MyOp3 | 3 |
| 8 | ActivityParameter | MyOp1 | 4 |
| 9 | ActivityParameter | MyOp2 | 4 |
+-----------+-------------------+-------+----------+
t_operation
+-------------+-----------+-------+
| OperationID | Object_ID | Name |
+-------------+-----------+-------+
| 1 | 1 | MyOp1 |
| 2 | 1 | MyOp2 |
| 3 | 2 | MyOp1 |
| 4 | 2 | MyOp2 |
| 5 | 2 | MyOp3 |
| 6 | 2 | MyOp4 |
+-------------+-----------+-------+
The above tables represent the following
Operation c1::MyOp1 (from class c1)
Operation c1::MyOp2 (from class c1)
Operation c2::MyOp1 (from class c2)
Operation c2::MyOp2 (from class c2)
Operation c2::MyOp3 (from class c2)
Operation c2::MyOp4 (from class c2)
Activity parameter c1::MyOp1 (from activity c1)
Activity parameter c1::MyOp2 (from activity c1)
Activity parameter c1::MyOp3 (from activity c1)
Activity parameter c2::MyOp1 (from activity c2)
Activity parameter c2::MyOp2 (from activity c2)
We can see the following errors
Operation c2::MyOp3 has no equivalent activity parameter
Operation c2::MyOp4 has no equivalent activity parameter
Activity parameter c1::MyOp3 has no equivalent operation
For the purpose of this question I am not interested in the final error. When I get the SQL query for "operation class::operation has no equivalent activity parameter" then I will have the logic to do the reverse.
I tried the SQL query (I am using Access and also MySQL). Note, text searches are case insensitive. The 2 IDs at the end of the SELECT should be equal if the activity parameter belongs to the mentioned activity. If they are different then the returned activity parameter belongs to a different activity. This acts as a quick cross-check.
SELECT o_class.name, o_operation.name, o_activity.name, o_actparam.name, o_activity.object_ID AS "activity ID", o_actparam.parentID AS "belongs to activity ID"
FROM
(((
t_object o_class
INNER JOIN t_object o_activity ON
( o_activity.name = o_class.name
AND
o_class.object_type = 'class'
AND
o_activity.object_type = 'activity'
)
)
INNER JOIN t_operation o_operation ON o_operation.object_id = o_class.object_id)
LEFT JOIN t_object o_actparam ON
( o_actparam.name = o_operation.name
AND
o_actparam.object_type = 'activityparameter'
AND
o_actparam.parentid = o_activity.object_id
)
)
WHERE
o_actparam.name is NULL
ORDER BY
o_class.name, o_operation.name, o_activity.name, o_actparam.name, o_activity.object_ID, o_actparam.parentID
The above aims to get a class, then an activity with the same name, then all operations belonging to the class, then for each operation try and find an activity parameter in this activity with the same name. Any that didn't match should return NULL (since it is a LEFT JOIN) and so the WHERE statement shows the operations that didn't have a related activityparameter, i.e. the errors.
The above does not work; I get a "JOIN expression not supported".
If I take out the "o_actparam.parentid = o_activity.object_id" then it returns no results at all. This is clearly wrong. I believe this is because the LEFT JOIN matches on the first expression, i.e. "o_actparam.name = o_operation.name", then applies any other expressions to that result. So it returns 10 rows (one NULL for c2::MyOp4) but the 2nd expression (o_actparam.object_type = 'activityparameter') then throws away the NULL (c2::MyOp4). Then all 9 results are thrown away by the WHERE clause.
If I change the LEFT JOIN to
LEFT JOIN t_object o_actparam ON
( o_actparam.name = o_operation.name
AND
( o_actparam.object_type = 'activityparameter'
OR
o_actparam.object_type is NULL
)
)
)
then I get the result
c2::MyOp4
It has failed to find c2::MyOp3. This is because operation c2::MyOp3 matches activityparmater c1::MyOp3 (same activity parameter name even though it belongs to the wrong class/activity c1). The LEFT JOIN comparison ignores the class/activity. Remember, if I put the 'o_actparam.parentid = o_activity.object_id' check then I get "JOIN expression not supported".
If I change the WHERE (and keep the above LEFT JOIN) to
WHERE
o_actparam.parentid <> o_activity.object_id
OR
o_actparam.parentid is NULL
then I get the result
c1::MyOp1
c1::MyOp2
c2::MyOp1
c2::MyOp2
c2::MyOp3
c2::MyOp4
It is now finding lots of wrong items since the same activityparameter name exists in different activities. The WHERE is too late to throw away items, however, putting the parentid in the LEFT JOIN gave me the "JOIN expression is not supported" error.
In my research I noticed that I could concatenate the expressions in an ON clause, e.g.
LEFT JOIN t_object o_actparam ON
o_actparam.name + o_actparam.object_type + o_actparam.parentid = o_operation.name + 'activityparameter' + o_activity.object_id
)
The idea here is that if the LEFT JOIN works on the first expression then filters on any subsequent ones then putting them all as one would do what I wanted. It also has the benefit of not requiring "OR xxx is NULL" all over the place. It also reinforces what I am looking for, the combination of name, object type and parent. Naturally the above didn't work (or I wouldn't be asking here); it still gave the same "JOIN expression not supported". Again, removing the "parentID" aspect gave wrong results.
I hope I have given a detailed situation, test cases, expected answers, my reasonings and my research to show this is not just a "I couldn't be bothered to work it out myself, please help". I have spent days googling and trying out SQL but, as I said, I am not an SQL expert.
Is someone able to help me here?
Thanks
Darren
I have found the answer so am posting it here in case someone finds it useful. Thanks Sandra for your help.
The answer is that my problem LEFT JOIN was trying to access entries in earlier tables but it couldn't see them because I referred to 2 different tables. I am not an SQL expert so I don't know the correct terminology or exactly why.
Anyway, the solution is to place all previous tables into a subquery so that it then gets a new single table name.
Note, since I am using Access, I have been told that the parentheses after the ON part of the LEFT JOIN are essential since I am joining on a constant value ('activityparameter').
The changes needed were to insert
(SELECT * FROM
before the first 2 tables. Note that this required me to break the single line with 3 parentheses into 2 lines as the SELECT needed to go after the first parenthesis.
After that add an extra
) AS q1
after the 2 tables. In my final answer below I've indented this whole section and put a blank line before and after for clarity.
Then in my LEFT JOIN I refer to the old tables by adding q1. at the start of each reference.
The following query works perfectly and gives the results that I expected.
SELECT o_class.name, o_operation.name, o_activity.name, o_actparam.name, o_activity.object_ID AS "activity ID", o_actparam.parentID AS "belongs to activity ID"
FROM
(
(SELECT * FROM
((
t_object o_class
INNER JOIN t_object o_activity ON
( o_activity.name = o_class.name
AND
o_class.object_type = 'class'
AND
o_activity.object_type = 'activity'
)
)
INNER JOIN t_operation o_operation ON o_operation.object_id = o_class.object_id)
) AS q1
LEFT JOIN t_object o_actparam ON
( o_actparam.name = q1.o_operation.name
AND
o_actparam.object_type = 'activityparameter'
AND
o_actparam.parentid = q1.o_activity.object_id
)
)
WHERE
o_actparam.name is NULL
ORDER BY
o_class.name, o_operation.name, o_activity.name, o_actparam.name, o_activity.object_ID, o_actparam.parentID

COALESCE and IFNULL not working as expected

I'm trying to output a default row when there's no row found in the query.
This is a sample of my query:
SELECT
COALESCE(site, 'STE') as site,
instrument,
field
FROM Table1
WHERE site IN ('East', 'West')
AND DATE(tstamp) = "2016-09-07"
ORDER BY id desc
The output is
+------+------------+-------+
| site | instrument | field |
+------+------------+-------+
| West | 0 | 0 |
+------+------------+-------+
For the tsamp 2016-09-07 we have a row for the site "West" and there's no row for "East". I tried to search and found that I can use COALESCE and also tried IFNULL but I'm only getting the output above. I also tried if(count(site) = 0, "STE", site) but i can't get it to work.
My expected result is
+------+------------+-------+
| site | instrument | field |
+------+------------+-------+
| West | 0 | 0 |
| STE | NULL | NULL |
+------+------------+-------+
I hope you guys can help me. Thanks in advance
Both coalesce() and ifnull() work on a row basis, meaning they can replace a null value if that null value exists in a record. However, they cannot create a record that does not exist - and you do not have any records matching East (or STE).
A possible solution is to create a table that has all possible values for the site field and you can left join on this table:
SELECT
COALESCE(Table1.site, "STE") as site,
Table1.instrument,
Table1.field
FROM LookupTable lt
LEFT JOIN Table1 ON lt.site=Table1.site
WHERE lt.site IN ('East', 'West')
AND DATE(Table1.tstamp) = "2016-09-07"
ORDER BY id desc
If 'STE' isn't in table1.site then it won't come back in the results.
You could do a union instead:
SELECT
site,
instrument,
field
FROM (SELECT 'West' [Site], 0 [instrument], 0 [field]) table1
WHERE site IN ('East', 'West')
UNION
SELECT 'STE', NULL, NULL
Note that by using "STE" you're looking for the column name "STE", not the value 'STE' (single quotes)
EDIT
You need a control table to dictate which values to look for, or you have to hard code your rules. You can't look for something that's missing without first specifying the things that should be there.
Here's an option:
--Create and populate reference table "sites"
create table sites
([site] char (4))
INSERT INTO sites VALUES ('East')
INSERT INTO sites VALUES ('West')
-- Query against reference table
SELECT
ISNULL(table1.site, 'STE'),
instrument,
field
FROM (SELECT 'West' [Site], 0 [instrument], 0 [field]) table1
RIGHT JOIN sites on table1.[Site] = sites.[Site]
--or
-- Query against reference table
SELECT
ISNULL(table1.site, 'STE'),
instrument,
field
FROM sites
LEFT JOIN (SELECT 'West' [Site], 0 [instrument], 0 [field]) table1 on table1.[Site] = sites.[Site]
Let me know if you have questions on how this works.

How to use PostgreSQL json_array_elements whit WHERE <value> IN clause

I have occasion and relation tables. relation.occasions is a json type field, which contains an array of occasion keys. I want to get occasion records by relation, where occasion.key in relation.occasions. Assume, that this is my DB data:
occasion
key | name |
-------------------------------------
BIRTHDAY | Birthday |
ANNIVERSARY | Anniversary |
relation
key | occasions |
------------------------------------------------------
FATHER | [ "BIRTHDAY", "ANNIVERSARY" ] |
FRIEND | [ "BIRTHDAY" ] |
Here is the query I'm trying to use:
SELECT * FROM occasion o WHERE o.key IN
(SELECT json_array_elements(r.occasions)::text from relation r WHERE r.key = 'FATHER')
The result is 0 rows instead of expected 2. Can somebody give me a hint what am I doing wrong? How else can I achieve desired result?
The function json_array_elements() returns a set, not an array, so you should use it as a row source, not in the select list.
The correct query would be:
SELECT o.*
FROM occasion o
JOIN (
SELECT DISTINCT j.value
FROM relation
JOIN LATERAL json_array_elements_text(occasions) j(value) ON true
WHERE key = 'FATHER') sub ON o.key = sub.value;
You can also flatten it out without the sub-query, but I find this version to be more readable (the query planner will do the flattening anyway).

complex sql query issue

I have a little SQL but I can't find the way to get back text just numbers. - revised!
SELECT if( `linktype` = "group",
(SELECT contactgroups.grname
FROM contactgroups, groupmembers
WHERE contactgroups.id = groupmembers.id ???
AND contactgroups.id = groupmembers.link_id),
(SELECT contactmain.contact_sur
FROM contactmain, groupmembers
WHERE contactmain.id = groupmembers.id ???
AND contactmain.id = groupmembers.link_id) ) AS adat
FROM groupmembers;
As now I have improved a bit gives back some info but ??? (thanks to minitech) indicate my problem. I can't see how could I fix... Any advice welcomed! Thansk
Contactmain (id, contact_sur, email2)
data:
1 | Peter | email#email.com
2 | Andrew| email2#email.com
Contactgroups (id, grname)
data:
1 | All
2 | Trustee
3 | Comitee
Groupmembers (id, group_id, linktype, link_id)
data:
1 | 1 | contact | 1
2 | 1 | contact | 2
3 | 2 | contact | 1
4 | 3 | group | 2
And I would like to list out who is in the 'Comitee' the result should be Andrew and Trustee if I am right:)
It does look a bit redundant on the join since you are implying both the ID and Link_ID columns are the same value. Since BOTH select values are derived from a qualification to the group members table, I have restructured the query to use THAT as the primary table and do a LEFT JOIN to each of the other tables, anticipating from your query that the link should be found from ONE or the OTHER tables. So, with each respective LEFT JOIN, you will go through the GroupMembers table only ONCE. Now, your IF(). Since the group members is the basis, and we have BOTH tables available and linked, we just grab the column from one table vs the other respectively. I've included the "linktype" too just for reference purposes. By using the STRAIGHT_JOIN will help the engine from trying to change the interpretation of how to join the tables.
SELECT STRAIGHT_JOIN
gm.linktype,
if( gm.linktype = "group", cg.grname, cm.contact_sur ) ADat
from
groupmembers gm
left join contactgroups cg
ON gm.link_id = cg.id
left join contactmain cm
ON gm.link_id = cm.id
If contactgroups.id must equal groupmembers.id but must also equal 2, that's redundant and also probably where your problem is. It works fine as you've written it: http://ideone.com/7EGLZ so without knowing what it's actually supposed to do I can't help more.
EDIT: I'm unfamiliar with the comma-separated FROM, but it gives the same result since you don't select anything from the other table so it doesn't really matter.