SQL: Complicated Select Statement and returned radius - mysql

This query is driving me insane and I honestly just don't know how to accomplish it.
I need to grab all USERS and SHOPS longitude and latitude that are within a certain radius, and return back some other information attached to those resulting IDs... the radius part, I have figured out:
( 3959 * acos( cos( radians('48.453541') ) * cos( radians( lat ) ) * cos( radians( lng ) - radians('-123.491765') ) + sin( radians('48.453541') ) * sin( radians( lat ) ) ) ) AS distance
^This radians in the statement above are just a random address that I want to query against.
But grabbing results that essentially are two different rows AS ONE result confuses me right out. Both tables are constructed in this manner:
id
meta_key
meta_value
So I need to grab the "meta_value" of two keys for both a USER and a SHOP ... the "meta_key" for the USER is "bid_user_lat" and "bid_user_lng" ... and the "meta_key" for the SHOP are "bid_resource_lat" and "bid_resource_lng"
Ideal results (just showing one of each, but should be multiple, depending on radius):
id - 223
first_name - "Mark"
last_name - "Johnson"
bid_user_lat - 45.0000
bid_user_lng - -150.0000
id - 688
company_name - "Joe's Shop"
bid_resource_lat - 45.0000
bid_resource_lng - -150.0000
I really hope I'm explaining this correctly, because this is just driving me nuts!
My updated query is as follow, yet it's still not producing any results and it should:
SET #lat = '48.453541';
SET #lng = '-123.491765';
SET #radius = '10000';
SELECT
wp_usermeta.user_id,
bid_user_lat.meta_value,
bid_user_lng.meta_value,
( 3959 * acos( cos( radians( #lat ) ) * cos( radians( bid_user_lat.meta_value ) ) * cos( radians( bid_user_lng.meta_value ) - radians( #lng ) ) + sin( radians( #lat ) ) * sin( radians( bid_user_lat.meta_value ) ) ) ) AS distance
FROM
wp_usermeta
JOIN
wp_usermeta bid_user_lat
ON wp_usermeta.user_id = bid_user_lat.user_id
AND wp_usermeta.meta_key = "bid_user_lat"
JOIN
wp_usermeta bid_user_lng
ON wp_usermeta.user_id = bid_user_lng.user_id
AND wp_usermeta.meta_key = "bid_user_lng"
HAVING distance < #radius ORDER BY distance LIMIT 0 , 20;

You can get this information by using self-joins:
SELECT
table.id,
bid_user_lat.meta_value,
bid_user_lng.meta_value
FROM
table
JOIN
table bid_user_lat
ON table.id = bid_user_lat.id
AND table.meta_key = "bid_user_lat"
JOIN
table bid_user_lng
ON table.id = bid_user_lng.id
AND table.meta_key = "bid_user_lng"
WHERE
{ distance clause }
...the distance clause is where you ensure that it's within the proper distance. You can use bid_user_lat.meta_value as the bid_user_lat value, and bid_user_lng.meta_value as the bid_user_lng value in your calculation.
If you want to return the User results in the same query as the Shop results, then you can do the same query as the above one (using Shops instead of Users, of course), and connect them with a UNION or UNION ALL.
Side note:
If you have any control over the database, you may want to re-visit the design. The solution above is not ideal by any means... If a User always has a bid_user_lat and a bid_user_lng field, or even if it's just a common attribute of User, then it really should have its own column in the User table.
Correction:
I was JOINing incorrectly. The ON clause should use bid_user_lng.meta_key = "bid_user_lat" instead of wp_usermeta.meta_key = "bid_user_lat". You also need to add a GROUP BY clause so that you don't have duplicate records. Applying these to your query above, you get:
SET #lat = '48.453541';
SET #lng = '-123.491765';
SET #radius = '10000';
SELECT
wp_usermeta.user_id,
bid_user_lat.meta_value,
bid_user_lng.meta_value,
( 3959 * acos( cos( radians( #lat ) ) * cos( radians( bid_user_lat.meta_value ) ) * cos( radians( bid_user_lng.meta_value ) - radians( #lng ) ) + sin( radians( #lat ) ) * sin( radians( bid_user_lat.meta_value ) ) ) ) AS distance
FROM
wp_usermeta
JOIN
wp_usermeta bid_user_lat
ON wp_usermeta.user_id = bid_user_lat.user_id
AND bid_user_lat.meta_key = "bid_user_lat"
JOIN
wp_usermeta bid_user_lng
ON wp_usermeta.user_id = bid_user_lng.user_id
AND bid_user_lng.meta_key = "bid_user_lng"
GROUP BY wp_usermeta.user_id
HAVING distance < #radius ORDER BY distance LIMIT 0 , 20;
Here's confirmation that it works:
mysql> CREATE TABLE wp_usermeta (user_id INTEGER UNSIGNED, meta_key VARCHAR(255), meta_value VARCHAR(255));
Query OK, 0 rows affected (0.25 sec)
mysql> INSERT INTO wp_usermeta (user_id, meta_key, meta_value)
-> VALUES
-> (1, "bid_user_lat", "45.000"), (1, "bid_user_lng", "-150.000"),
-> (2, "bid_user_lat", "20.000"), (2, "bid_user_lng", "20.000"),
-> (3, "bid_user_lat", "-300.000"), (3, "bid_user_lng", "70.000");
Query OK, 6 rows affected (0.16 sec)
Records: 6 Duplicates: 0 Warnings: 0
mysql> SELECT
-> wp_usermeta.user_id,
-> bid_user_lat.meta_value,
-> bid_user_lng.meta_value,
->
-> ( 3959 * acos( cos( radians( #lat ) ) * cos( radians( bid_user_lat.meta_value ) ) * cos( radians( bid_user_lng.meta_value ) - radians( #lng ) ) + sin( radians( #lat ) ) * sin( radians( bid_user_lat.meta_value ) ) ) ) AS distance
->
-> FROM
-> wp_usermeta
-> JOIN
-> wp_usermeta bid_user_lat
-> ON wp_usermeta.user_id = bid_user_lat.user_id
-> AND bid_user_lat.meta_key = "bid_user_lat"
-> JOIN
-> wp_usermeta bid_user_lng
-> ON wp_usermeta.user_id = bid_user_lng.user_id
-> AND bid_user_lng.meta_key = "bid_user_lng"
->
-> GROUP BY wp_usermeta.user_id
->
-> HAVING distance < #radius ORDER BY distance LIMIT 0 , 20;
+---------+------------+------------+--------------------+
| user_id | meta_value | meta_value | distance |
+---------+------------+------------+--------------------+
| 1 | 45.000 | -150.000 | 1271.3329043047352 |
| 3 | -300.000 | 70.000 | 4905.4310014631055 |
| 2 | 20.000 | 20.000 | 7198.549954690863 |
+---------+------------+------------+--------------------+
3 rows in set (0.05 sec)

Related

Find a partial match in MySQL query with multiple "OR"

I need to find partial or full matches in a MySQL query. I am performing queries in multiple tables using 'OR' and need to know whether all conditions have been met or just some of them.
I believe the easiest way to do this would be to create a new column called 'partial' which would either be 0 or 1 (Feel free to correct me if I am wrong).
Here is my query:
SELECT
id, confirmed, name, addressLine1, addressLine2, addressCity, addressPostcode,
addressLat, addressLon,
(6371 * acos( cos( radians( '53.649779' ) )
* cos( radians( addressLat ) )
* cos( radians( addressLon ) - radians( '-1.6026266' ) )
+ sin( radians( '53.649779' ) ) * sin( radians( addressLat ) ) )
) distance
FROM TABLE_NAME
WHERE active = '1'
AND deleted = '0'
AND
(
id < 0
OR
id IN
(
SELECT establishmentID
FROM appEstablishmentDrinks
WHERE active = '1'
AND deleted = '0'
AND manufacturerDrinkID IN (101)
GROUP BY establishmentID
HAVING count(distinct manufacturerDrinkID) = 1
)
OR
id IN
(
SELECT establishmentID
FROM appEstablishmentFacilities
WHERE active = '1'
AND deleted = '0'
AND facilityID IN (37)
)
)
HAVING distance < '15'
In MySQL true = 1 and false = 0. So simply add conditions: (id < 0) + (id in (...)) + (id in (...)) and check whether you got all three matches or less.
SELECT
id, confirmed, name, addressLine1, addressLine2, addressCity, addressPostcode,
addressLat, addressLon, distance,
case when count_matches = 3 then 'full' else 'partial' end as matching
FROM
(
SELECT
id, confirmed, name, addressLine1, addressLine2, addressCity, addressPostcode,
addressLat, addressLon,
(6371 * acos( cos( radians( 53.649779 ) )
* cos( radians( addressLat ) )
* cos( radians( addressLon ) - radians( -1.6026266 ) )
+ sin( radians( 53.649779 ) ) * sin( radians( addressLat ) ) )
) as distance,
(id < 0 )
+
(
id IN
(
SELECT establishmentID
FROM appEstablishmentDrinks
WHERE active = 1
AND deleted = 0
AND manufacturerDrinkID IN (101)
GROUP BY establishmentID
HAVING count(distinct manufacturerDrinkID) = 1
)
)
+
(
id IN
(
SELECT establishmentID
FROM appEstablishmentFacilities
WHERE active = 1
AND deleted = 0
AND facilityID IN (37)
)
) as count_matches
FROM TABLE_NAME
WHERE active = 1
AND deleted = 0
) x
WHERE distance < 15
AND count_matches > 0;

MySQL - SELECT col, function(col) WHERE IN (..) returns same value

I have defined a function that calculates the nearest item using the haversine formula. This function works correctly when I execute the query in an ad-hoc fashion such as:
SELECT CONCAT(address, ", ", deadline, ", NC") AS addr FROM tblTickets WHERE ticket = 'A152012363' LIMIT 1 INTO #Address;
SELECT lat, lng FROM AddressGeocode WHERE address = #Address LIMIT 1 INTO #LAT, #LNG;
SELECT ROUND(( 3959 * acos( cos( radians(#LAT) ) * cos( radians( startlat ) ) * cos( radians( startlon ) - radians(#LNG) ) + sin( radians(#LAT) ) * sin( radians( startlat ) ) ) ) * 5280.0 ) AS distance
FROM tblAsBuiltPolys
ORDER BY distance
LIMIT 1 INTO #RET;
SELECT #RET
When I place the logic in a function so that I can re use it, the result is always the same until I start another session.
I initially thought the issue was that I was using session variables so I adjusted the function to be this:
CREATE FUNCTION `getTicketBuffer`(`ticket` VARCHAR(50))
RETURNS DOUBLE
LANGUAGE SQL
NOT DETERMINISTIC
READS SQL DATA
COMMENT ''
BEGIN
DECLARE ADDR VARCHAR(250) DEFAULT NULL;
DECLARE LT DOUBLE DEFAULT NULL;
DECLARE LG DOUBLE DEFAULT NULL;
DECLARE RET DOUBLE DEFAULT NULL;
SET ADDR = (SELECT CONCAT(address, ", ", deadline, ", NC") AS addr FROM tblTickets WHERE ticket = ticket LIMIT 1);
SELECT lat, lng INTO LT, LG FROM AddressGeocode WHERE address = ADDRESS LIMIT 1;
SET RET = (SELECT ROUND(( 3959 * acos( cos( radians(LT) ) * cos( radians( startlat ) ) * cos( radians( startlon ) - radians(LG) ) + sin( radians(LT) ) * sin( radians( startlat ) ) ) ) * 5280.0 ) AS distance
FROM tblAsBuiltPolys
ORDER BY distance
LIMIT 1);
RETURN RET;
END
I am pulling my hair out trying to make this work so that the following query will execute correctly.
SELECT ticket, getTicketBuffer(ticket) AS d
FROM tblTickets WHERE ticket IN
("A152012363","C152011366","A152012358","C152011309","A152012353","A152011315");
When I exectute the logic directly in a query I get the following:
mysql> SELECT ROUND(( 3959 * acos( cos( radians(#LAT) ) * cos( radians( startlat ) ) * cos( radians( startlon ) - radians(#LNG) ) + sin( radians(#LAT) ) * sin( radians( startlat ) ) ) ) * 5280.0 ) AS distance
-> FROM tblAsBuiltPolys
-> ORDER BY distance
-> LIMIT 1 INTO #RET;
Query OK, 1 row affected (0.02 sec)
mysql> SELECT #RET
-> ;
+------+
| #RET |
+------+
| 130 |
+------+
1 row in set (0.00 sec)
mysql>
mysql> SELECT ticket, getTicketBuffer(ticket) AS d FROM tblTickets WHERE ticket IN("A152012363","C152011366","A152012358","C152011309","A152012353","A152011315");
+------------+------+
| ticket | d |
+------------+------+
| A152011315 | 81 |
| A152012353 | 81 |
| A152012358 | 81 |
| A152012363 | 81 |
| C152011309 | 81 |
| C152011366 | 81 |
+------------+------+
6 rows in set (0.12 sec)
I am trying to get the "TicketBuffer" for each ticket in a list of tickets, if this can be made into a view, then that would work as well.
Does anyone see any obvious mistakes? I am not an expert at SQL, particularly stored functions/procedures so I am sure this can be done a better way.

GTFS SQL Find Closest Routes

I have a MySQL database setup with GTFS data and I am trying to query the database to return a list of routes (no duplicates) ordered by the distance to the closest stop on each respective route. (Note: The coordinates will change depending on where the user is)
The database is rather large (millions of rows in the stop_times table, 10,000s of rows in the stops table) so I want to make this as efficient as possible.
One thing I tried was to create a temporary table called stop_routes_link (created when the GTFS data is imported into my database) that links stops with routes such that there is an entry for each stop-route pair like so:
route_id | stop_id
-----------------------
25 | 366072709
21 | 366072709
21 | 194326291
F | 60745282
Q | 198000482
And then I ran this query which works perfectly:
SELECT routes.route_short_name AS route_short_name
FROM routes
LEFT JOIN (
SELECT ( 3959 * acos( cos( radians(39.94868155755109) ) * cos( radians( stops.stop_lat ) ) * cos( radians( stops.stop_lon ) - radians(-75.15972534860013) ) + sin( radians(39.94868155755109) ) * sin( radians( stops.stop_lat ) ) ) ) AS distance, stop_routes_link.route_id AS route_id
FROM stops
LEFT JOIN stop_routes_link ON stops.stop_id = stop_routes_link.stop_id ORDER BY distance)
AS stops ON routes.route_id = stops.route_id
ORDER BY stops.distance
And it returns:
route_short_name
----------------
23
23
12
12
9
21
38
Which is what I want (I know those are the closest routes to that location), except it returns duplicates for routes since I think it is returning an row for each stop on a route.
How do I return only unique routes? I thought that the "LEFT JOIN" would cause only one row per entry in the routes table but it didn't.
I've also tried:
SELECT DINSTINCT routes.route_short_name AS route_short_name
FROM routes
LEFT JOIN (
SELECT ( 3959 * acos( cos( radians(39.94868155755109) ) * cos( radians( stops.stop_lat ) ) * cos( radians( stops.stop_lon ) - radians(-75.15972534860013) ) + sin( radians(39.94868155755109) ) * sin( radians( stops.stop_lat ) ) ) ) AS distance, stop_routes_link.route_id AS route_id
FROM stops
LEFT JOIN stop_routes_link ON stops.stop_id = stop_routes_link.stop_id ORDER BY distance)
AS stops ON routes.route_id = stops.route_id
ORDER BY stops.distance
And:
SELECT routes.route_short_name AS route_short_name
FROM routes
LEFT JOIN (
SELECT ( 3959 * acos( cos( radians(39.94868155755109) ) * cos( radians( stops.stop_lat ) ) * cos( radians( stops.stop_lon ) - radians(-75.15972534860013) ) + sin( radians(39.94868155755109) ) * sin( radians( stops.stop_lat ) ) ) ) AS distance, stop_routes_link.route_id AS route_id
FROM stops
LEFT JOIN stop_routes_link ON stops.stop_id = stop_routes_link.stop_id ORDER BY distance)
AS stops ON routes.route_id = stops.route_id
GROUP BY route_short_name
ORDER BY stops.distance
But they both don't return the closest routes they return a random ordered list of routes (I'm not sure how it is calculated) which I'm assuming is because the grouping messes it up.
Any help would be greatly appreciated!

SQL syntax error - Haversine formula

I'm trying to get nearest places from a WordPress database using Haversine formula
My table structure below
posts
+--------------+
| Field |
+--------------+
| ID |
| post_author |
| post_title |
| post_type |
+--------------+
postmeta
+--------------+
| Field |
+--------------+
| meta_id |
| post_id |
| meta_key |
| meta_value |
+--------------+
and have records with meta_key values latitude and longitude
See the answer to my previous question for the SQL I've used to get the latitude and longitude.
SELECT p.ID,
p.post_title,
p.post_author,
max(case when pm.meta_key='latitude' then pm.meta_value end) latitude,
max(case when pm.meta_key='longitude' then pm.meta_value end) longitude
FROM `wp_posts` p
LEFT JOIN `wp_postmeta` pm
on p.ID=pm.post_id
WHERE p.post_type='place'
AND (pm.meta_key='latitude' OR pm.meta_key='longitude')
GROUP BY p.ID, p.post_title, p.post_author
ORDER BY p.ID ASC
Now I want to incorporate above query into answer for this question
SELECT item1, item2,
( 3959 * acos( cos( radians(37) )
* cos( radians( lat ) )
* cos( radians( lng )
- radians(-122) )
+ sin( radians(37) )
* sin( radians( lat ) )
)
) AS distance
FROM geocodeTable
HAVING distance < 25
ORDER BY distance LIMIT 0 , 20;
Below is by combined query
SELECT ID,
post_title,
post_author,
max(case when meta_key='latitude' then meta_value end) latitude,
max(case when meta_key='longitude' then meta_value end) longitude,
( 3959 * acos( cos( radians(18.204540500000) )
* cos( radians( latitude ) )
* cos( radians( longitude )
- radians(-66.450958500000) )
+ sin( radians(18.204540500000 )
* sin( radians( latitude ) )
)
) AS distance
FROM `wp_posts`
LEFT JOIN `wp_postmeta`
on ID=post_id
WHERE post_type='place'
AND (meta_key='latitude' OR meta_key='longitude')
GROUP BY ID, post_title, post_author
ORDER BY ID ASC
But this yields syntax error
#1064 - You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'AS distance FROM `wp_posts` LEFT JOIN `wp_postmeta` on ID=post_id WHERE po' at line 13
You are missing a closing ) for the first sin()
( 3959 * acos( cos( radians(18.204540500000) )
* cos( radians( latitude ) )
* cos( radians( longitude )
- radians(-66.450958500000) )
+ sin( radians(18.204540500000 ) ) /* <--- here */
* sin( radians( latitude ) )
)
) AS distance
Though it is difficult to spot visually, I found this by copying your code into a text editor that supports brace matching. It is highly recommended to use one, if not for query development and testing, then at least for debugging.
Try this:
SELECT *, ( 3959 * ACOS( COS( RADIANS(18.204540500000) )
* COS( RADIANS( latitude ) )
* COS( RADIANS( longitude )
- RADIANS(-66.450958500000) )
+ SIN( RADIANS(18.204540500000 ) )
* SIN( RADIANS( latitude ) )
)
) AS distance
FROM
(SELECT ID,
post_title,
post_author,
MAX(CASE WHEN meta_key='geo_latitude' THEN meta_value END) latitude,
MAX(CASE WHEN meta_key='geo_longitude' THEN meta_value END) longitude
FROM `wp_posts`
LEFT JOIN `wp_postmeta`
ON ID=post_id
WHERE post_type='place'
AND (meta_key='geo_latitude' OR meta_key='geo_longitude')
GROUP BY ID, post_title, post_author
ORDER BY ID ASC) AS A

Mysql, select value of one cell depending on contents of another cell

My current code
SELECT post_id, ( 3959 * ACOS( COS( RADIANS( 34.09 ) ) * COS( RADIANS( lat ) ) * COS( RADIANS( lng ) - RADIANS( -117.55 ) ) + SIN( RADIANS( 34.09 ) ) * SIN( RADIANS( lat ) ) ) ) AS distance FROM wp_postmeta WHERE `meta_key` LIKE '%location_l%' HAVING distance < 2500 ORDER BY distance LIMIT 0 , 20
Here are two sample row of data (meta_key is not always long or lat):
post_id = 123
meta_key = location_longitude
meta_value = -119.890000
post_id = 123
meta_key = location_latitude
meta_value = 42.170000
How do I modify my query to replace 'lat' and 'lng' in my original query to be the contents of the meta_value listed above? Something like this?
select meta_value where meta_key = location_latitude
If you GROUP BY post_id, you should be able to use MAX() aggregates in place of lat,lng, surrounding CASE statements which determine whether the current row is latitude or longitude. The others will be NULL, and therefore eliminated by the aggregate.
I think this ought to work so you won't need any subselects.
SELECT
post_id,
( 3959 * ACOS( COS( RADIANS( 34.09 ) ) * COS( RADIANS( MAX(CASE WHEN meta_key='location_latitude' THEN meta_value ELSE NULL END) ) ) * COS( RADIANS( MAX(CASE WHEN meta_key='location_longitude' THEN meta_value ELSE NULL END) ) - RADIANS( -117.55 ) ) + SIN( RADIANS( 34.09 ) ) * SIN( RADIANS( MAX(CASE WHEN meta_key='location_latitude' THEN meta_value ELSE NULL END) ) ) ) ) AS distance
FROM wp_postmeta
WHERE `meta_key` IN ('location_latitude','location_longitude')
GROUP BY post_id
HAVING distance < 2500
ORDER BY distance
LIMIT 0 , 20
The moving parts here are:
MAX(CASE WHEN meta_key='location_latitude' THEN meta_value ELSE NULL END)
This translates as: If this row's meta_key is 'location_latitude', return the meta_value, otherwise return NULL. We expect then that since two rows are returned for the post_id (lat,lng), the MAX() value returned above is always the non-null one -- the correct latitude or longitude value from meta_value.
You need to join the table to itself (this is common in databases with EAV structure):
SELECT
post_id,
distance
FROM
( SELECT
lng.post_id,
( 3959 * ACOS( COS( RADIANS( 34.09 ) )
* COS( RADIANS( lat.meta_value ) )
* COS( RADIANS( lng.meta_value )
- RADIANS( -117.55 ) )
+ SIN( RADIANS( 34.09 ) )
* SIN( RADIANS( lat.meta_value ) )
)
) AS distance
FROM wp_postmeta AS lng
JOIN wp_postmeta AS lat
ON lat.post_id = lng.post_id
WHERE lng.meta_key = 'location_longitude'
AND lat.meta_key = 'location_latitude'
) AS tmp
WHERE distance < 2500
ORDER BY distance
LIMIT 0 , 20 ;
One approach is to replace the reference to the table (i.e. FROM wp_postmeta) in your query with an inline view, something like this:
FROM
( SELECT plat.post_id
, plat.meta_value AS lat
, plng.meta_value AS lng
FROM wp_postmeta plat
JOIN wp_postmeta plng
ON plat.post_id = plng.post_id
AND plat.meta_key = 'location_latitude'
AND plng.meta_ley = 'location_longitude'
) wpm
(NOTE: this assumes that there is only one row for each post_id for each of the two meta_key values of interest...)
This uses an "inline view" to combine the latitude and longitude values for each post_id into a single row. (You can run just the query for the inline view to confirm that its returning the results you expect.) We can use an inline view in place of a table reference, in more recent versions of MySQL (version >= 5.0).
SELECT a.post_id,
( 3959 * ACOS( COS( RADIANS( 34.09 ) ) * COS( RADIANS( lat.meta_value ) ) * COS( RADIANS( lng.meta_value ) - RADIANS( -117.55 ) ) + SIN( RADIANS( 34.09 ) ) * SIN( RADIANS( lat.meta_value ) ) ) ) AS distance
FROM wp_postmeta lat, wp_postmeta lng
WHERE lat.post_id = lng.postID
AND lat.meta_key = 'location_latitude'
AND lng.meta_key = 'location_longitude'
HAVING distance < 2500
ORDER BY distance LIMIT 0 , 20