Better solution to MySQL nested select in's - mysql

Currently I have two MySQL tables
Properties
id name
1 Grove house
2 howard house
3 sunny side
Advanced options
prop_id name
1 Wifi
1 Enclosed garden
1 Swimming pool
2 Swimming pool
As you can see table two contains specific features about the properties
When I only have max 3 options the query below worked just fine. (maybe a little slow but ok) now things have expanded somewhat and i have a max of 12 options that it is possible to search by and its causing me some major speed issues. The query below is for 8 options and as you can see its very messy. Is there a better way of doing what I'm trying to achieve?
SELECT * FROM properties WHERE id in (
select prop_id from advanced_options where name = 'Within 2 miles of sea or river' and prop_id in (
select prop_id from advanced_options where name = 'WiFi' and prop_id in (
select prop_id from advanced_options where name = 'Walking distance to pub' and prop_id in (
select prop_id from advanced_options where name = 'Swimming pool' and prop_id in (
select prop_id from advanced_options where name = 'Sea or River views' and prop_id in (
select prop_id from advanced_options where name = 'Pet friendly' and prop_id in (
select prop_id from advanced_options where name = 'Open fire, wood burning stove or a real flame fire-place' and prop_id in (
select prop_id from advanced_options where name='Off road parking')
)
)
)
)
)
)
)

Like Mike Brant suggest I would consider altering your datamodel to a limit to set and creating a column for each of these in your properties table. But some times the boss comes: "We also need 'flatscreen tv'" and then you have to go back to the DB and update the scheme and your data access layer.
A way to move this logic somehow out if the database it to use bitwise comparison. This allows you to make simple queries, but requires a bit of preprocessing before you make your query.
Judge for yourself.
I've put everything in a test suite for you here sqlfiddle
The basic idea is that each property in your table has an id that is the power of 2. Like this:
INSERT INTO `advanced_options` (id, name)
VALUES
(1, 'Wifi'),
(2, 'Enclosing Garden'),
(8, 'Swimming Pool'),
(16, 'Grill');
You can then store a single value in your properties table buy adding up the options:
Wifi + Swimming Pool = 1 + 8 = 9
If you want to find all properties with wifi and a swimming pool you then do like this:
SELECT * FROM `properties` WHERE `advanced_options` & 9 = 9
If you just wanted swimming pool this would be it:
SELECT * FROM `properties` WHERE `advanced_options` & 8 = 8
Go try out the fiddle

You really need to consider a schema change to your table. It seems that advanced options in and of themselves don't have any properties, so instead of an advanced_options table that is trying to be a many-to-many JOIN table, why not just have a property_options table with a field for each "options". Something like this
|prop_id | wifi | swimming_pool | etc..
-----------------------------------
| 1 | 0 | 1 |
| 2 | 1 | 0 |
Here each field is a simple TINYINT field with 0/1 boolean representation.
To where you could query like:
SELECT * FROM properties AS p
INNER JOIN property_options AS po ON p.id = po.prop.id
WHERE wifi = 1 AND swimming_pool = 1 ....
Here you would just build your WHERE clause based on which options you are querying for.
There actually wouldn't be any need to even have a separate table, as these records would have a one-to-one relationship with the properties, so you could normalize these fields onto you properties table if you like.

Join back to the advanced_options table multiple times. Here's a sample with 2 (lather, rinse, repeat).
select o1.prop_id
from advanced_options o1
inner join advanced_options o2 on o1.prop_id = o2.prop_id and o2.name = "WiFi"
where o1.name = 'Within 2 miles of sea or river'

Could you do something like this?:
select p.*,count(a.prop_id) as cnt
from properties p
inner join advanced_options a on a.prop_id = p.id
where a.name in ('Enclosed garden','Swimming pool')
group by p.name
having cnt = 2
That query would get all the properties that have ALL of those advanced_options...
I would also suggest normalizing your tables by creating a separate table Called Advanced_option (id,name) where you store your unique Option values and then create a junction entity table like Property_x_AdvancedOption (fk_PropertyID, FK_AdvancedOptionID) that way you use less resources and avoid data integrity issues.

Related

How do you check for matching value in third column based on distinct combinations of other two columns?

I have a table with Building name, such as A, B, C. These building names could be repeated. I have another column which has the floor. Such as floor 1 ,floor 2. Again there could be multiple floor 1 for every building. There is a third column which has object present such as television, bricks, fans.
I want to check for every combination of building with corresponding floors, such as Building A - floor 1, Building A- floor 2, if an object 'brick' exists then 'wall' must exist.
EXAMPLE DATA:
For every building and location, if 'WALL' exists , 'WINDOW', 'WINDOW1' or 'WINDOW2' must exist, ELSE FLAG
BUILDING LOCATION OBJECT
A FLOOR1 WALL
A FLOOR1 WINDOW
A FLOOR2 WALL
B FLOOR1 WALL
C FLOOR1 WALL
C FLOOR1 WINDOW
DESIRED OUPUT
BUILDING LOCATION ACTION
A FLOOR2 FLAG
B FLOOR1 FLAG
I have tried using GROUP BY, DISTINCT, WHERE EXISTS, but I cant seem to come up with the right logic.
You can group by building, location for the rows where object in ('WALL', 'WINDOW'):
select building, location, 'FLAG' action
from tablename
where object in ('WALL', 'WINDOW')
group by building, location
having count(distinct object) < 2
The condition count(distinct object) < 2 in the having clause returns combination of building, location where 'WALL' and 'WINDOW' do not both exist.
See the demo.
Results:
| building | location | action |
| -------- | -------- | ------ |
| A | FLOOR2 | FLAG |
| B | FLOOR1 | FLAG |
Or with NOT EXISTS:
select t.building, t.location, 'FLAG' action
from tablename t
where object in ('WALL', 'WINDOW')
and not exists (
select 1 from tablename
where building = t.building and location = t.location and object <> t.object
)
See the demo.
nested tables is what you want. Something like
select column_3
from (
select *
from table
where table.column_3="brick"
) as e
join table t on t.id = e.id
where table.column_3="window"
fyi: I reccomend you use this as a start, but for your exact case, id imagine this needs to be modified

MySQL Exclude results from JOIN

I have two tables:
locations
---------------------
| id INT, PK, AI |
| x INT |
| y INT |
---------------------
signals
---------------------
| id INT, PK, AI |
| location_id INT |
| mac INT |
| strength INT |
---------------------
One location can (will) have up to maximum of 4 signals. Where location is X,Y point on an image and signal is Access Point in range of X,Y and it's signal strength.
Anyhow I have a method to which I provide a List of up to 4 MAC addresses and I need to find all locations that contain those 4 addresses.
Right now I'm doing this programmatically by:
1. Take top signal strength at X,Y and it's mac addresses
2. SELECT * FROM signals WHERE mac = ScanResult.BSSID
3. Create array of ID's from returned signals.location_id
4. In bulk select all locations if their ID is in the array along with all signals related to those locations
5. Complex loop in loop creates an array containing all locations that have in their relationship all the 4 mac addresses that I provided in the List and delete others.
This is incredibly messy and has redundant queries but since I'm not very good with SQL it was a patch that sort of worked.
Now I was wondering if I can do this using SQL alone and return locations that contain those 4 mac addresses.
I have:
SELECT locations.*, signals.* FROM locations INNER JOIN signals ON locations.id = signals.location_id;
I would be less confused if I only had to exclude locations where a relation would be 1:1 but here each locations has up to 4 signals. Is there a way I could provide an "array" to the query and say from that JOIN remove all locations that do not contain this number of mac addresses and these mac addresses.
You can use a having clause to ensure a location has all four MAC addresses:
SELECT l.id
, l.x
, l.y
FROM locations l
JOIN signals s
ON s.location_id = l.location_id
WHERE s.MAC in (1, 2, 3, 4)
GROUP BY
l.id
, l.x
, l.y
HAVING COUNT(DISTINCT s.MAC) = 4
Is there a way I could provide an "array" to the query
Yes, there is an operator for this. You can even provide another query as parameter
SELECT * FROM locations WHERE id IN (SELECT location_id FROM signals WHERE mac = ScanResult.level);
you can also use NOT IN to exclude elements which are contained in the list
Andomar's solution is correct based on the sample query. However, the data structure differs from the query, and the query doesn't really make sense. I think this is all that is necessary:
SELECT s.location_id
FROM signals s
WHERE s.MAC in (1, 2, 3, 4)
GROUP BY s.location_id
HAVING COUNT(DISTINCT s.MAC) = 4;

SELECTing the total count of multiple cells based upon a shared ID

I have two databases, one holds family names and the other holds family members.
Since separate family names (lastnames) can be shared between people not related, they are assigned a family "ID" which is shared across both databases.
familyNames : lastName | ID
memberNames : firstName | ID
I want to count how many members each family has, the output looking like:
Family Name | Members
----------------------
Johnson | 14
----------------------
Brown | 21
----------------------
White | 33
Is there a way to do this without creating a new column? Thanks.
(The familyNames holds many more columns that are irrelevant to the problem, hence the reason to have two tables)
select f.lastname, count(m.firstname) as cnt
from familynames f
left join membernames m on m.id = f.id
group by f.id, f.lastname
order by f.lastname
HERE Database names are prefixed to table names , DB1, DB2 are database names.
SELECT FN.lastName as 'Family Name', COUNT(*) as Members FROM
DB1.familyNames FN
JOIN DB2.memberNames MN
on FN.ID = MN.ID
group by FN.lastName

MySQL Query Distinct ID over multiple lines

MySQL table 'Features'
prop_id name
----------------------------
1 Wifi
2 Off Road Parking
1 Off Road Parking
2 Close to beach
3 Close to Pub
1 Close to Pub
Prop_id is the id in another table of the property
what i would like to do is get the id's of all the properties where they have 'Wifi' and 'Close to pub'
so in this case i would like it to only return 1
Hope that i have made sence!
There are several ways to achieve this, one ugly way is:
select prop_id from features
where name = 'Wifi' and prop_id in (
select prop_id from features where name = 'Close to Pub'
)
Use SELECT DISTINCT.
SELECT DISTINCT prop_id FROM table WHERE name="Wifi" or name="Close to pub"

MySQL select only new records

How to write a MySQL query to achieve this task?
Table: writers
w_id w_name
---------------
1 Michael
2 Samantha
3 John
---------------
Table: articles
a_id w_id timestamp a_name
----------------------------------------
1 1 0000000001 PHP programming
2 3 0000000003 Other programming languages
3 3 0000000005 Another article
4 2 0000000015 Web design
5 1 0000000020 MySQL
----------------------------------------
Need to SELECT only those writers who published their first article not earlier than 0000000005. (only writers who published at least one article can be selected)
In this example the result would be:
2 Samantha
SQL code can be tested here http://sqlfiddle.com/#!2/7a308
Untested, but close:
SELECT w_id, MIN(timestamp) as min_time
from writers w
JOIN articles a on w.w_id = a.w_id
GROUP BY 1
HAVING min_time > 5
Here's one approach, using an inline view (or "derived table" as MySQL calls it) to get the earliest timestamp for each writer:
SELECT w.w_id
, w.w_name
-- , e.earliest_timestamp
FROM writers w
LEFT
JOIN ( SELECT a.w_id
, MIN(a.timestamp) AS earliest_timestamp
FROM articles a
GROUP BY a.w_id
) e
ON e.w_id = w.w_id
WHERE e.earliest_timestamp >= '0000000005'
ORDER BY w.w_id
This may not be the most efficient approach, but you can run just the query in the inline view (aliased as e) to see what it returns. We can then reference the result set from that query like we do a table (with some restrictions.)
(Other approaches can make better use of suitable indexes.)
I'm unclear on the datatype of earliest_timestamp column. The SQL above assumes it's character datatype. If it's integer rather than character, the WHERE clause could look like this:
WHERE e.earliest_timestamp >= 5