SQLite - WHERE 子句和 UDF
Posted
技术标签:
【中文标题】SQLite - WHERE 子句和 UDF【英文标题】:SQLite - WHERE Clause & UDFs 【发布时间】:2013-05-12 12:26:42 【问题描述】:简介
我有以下 SQLite 表,其中包含 198,305 个地理编码的葡萄牙邮政编码:
CREATE TABLE "pt_postal" (
"code" text NOT NULL,
"geo_latitude" real(9,6) NULL,
"geo_longitude" real(9,6) NULL
);
CREATE UNIQUE INDEX "pt_postal_code" ON "pt_postal" ("code");
CREATE INDEX "coordinates" ON "pt_postal" ("geo_latitude", "geo_longitude");
我在 php 中还有以下用户定义的函数,它返回两个坐标之间的距离:
$db->sqliteCreateFunction('geo', function ()
if (count($data = func_get_args()) < 4)
$data = explode(',', implode(',', $data));
if (count($data = array_map('deg2rad', array_filter($data, 'is_numeric'))) == 4)
return round(6378.14 * acos(sin($data[0]) * sin($data[2]) + cos($data[0]) * cos($data[2]) * cos($data[1] - $data[3])), 3);
return null;
);
只有 874 条记录与 38.73311, -9.138707
的距离小于或等于 1 公里。
问题
UDF 在 SQL 查询中完美运行,但由于某种原因,我无法在 WHERE
子句中使用它的返回值 - 例如,如果我执行查询:
SELECT
"code",
geo(38.73311, -9.138707, "geo_latitude", "geo_longitude") AS "distance"
FROM "pt_postal" WHERE 1 = 1
AND "geo_latitude" BETWEEN 38.7241268076 AND 38.7420931924
AND "geo_longitude" BETWEEN -9.15022289523 AND -9.12719110477
AND "distance" <= 1
ORDER BY "distance" ASC
LIMIT 2048;
它返回 1035 条记录由 distance
在约 0.05 秒内排序,然而最后一条记录的“距离”为 @987654328 @km(比我在上一个WHERE
中定义的最大值1km还大)。
如果我删除以下子句:
AND "geo_latitude" BETWEEN 38.7241268076 AND 38.7420931924
AND "geo_longitude" BETWEEN -9.15022289523 AND -9.12719110477
现在查询需要将近 6 秒,并返回由 distance
排序的 2048 条记录(我的 LIMIT
)。它应该需要这么长时间,但它应该只返回具有"distance" <= 1
的 874 条记录。
原始查询返回的EXPLAIN QUERY PLAN
:
SEARCH TABLE pt_postal USING INDEX coordinates (geo_latitude>? AND geo_latitude<?)
#(~7500 rows)
USE TEMP B-TREE FOR ORDER BY
并且没有坐标边界:
SCAN TABLE pt_postal
#(~500000 rows)
USE TEMP B-TREE FOR ORDER BY
我想做的事
我想我知道为什么会这样,SQLite 正在这样做:
-
使用索引
coordinates
过滤掉WHERE
子句中边界外的记录
通过"distance" <= 1
WHERE
子句过滤这些记录,但distance
仍然是NULL => 0
!
填充“代码”和“距离”(通过首次调用 UDF)
按“距离”排序(现在已填充)
限制记录
我想让 SQLite 做什么:
-
使用索引
coordinates
过滤掉WHERE
子句中边界外的记录
对于这些记录,通过调用 UDF 填充 code
和 distance
按"distance" <= 1
WHERE
子句过滤记录
按“距离”排序(无需再次调用 UDF)
限制记录
谁能解释我如何让 SQLite 以我想要的方式运行(如果可能的话)?
后记
出于好奇,我尝试对两次调用 UDF 的速度进行基准测试:
SELECT
"code",
geo(38.73311, -9.138707, "geo_latitude", "geo_longitude") AS "distance"
FROM "pt_postal" WHERE 1 = 1
AND "geo_latitude" BETWEEN 38.7241268076 AND 38.7420931924
AND "geo_longitude" BETWEEN -9.15022289523 AND -9.12719110477
AND geo(38.73311, -9.138707, "geo_latitude", "geo_longitude") <= 1
ORDER BY "distance" ASC
LIMIT 2048;
令我惊讶的是,它仍然在大约 0.06 秒内运行 - 它仍然(错误地!)返回 1035 条记录。
似乎第二个geo()
呼叫甚至没有被评估...但是it should,对吧?
【问题讨论】:
请选择一个错误的记录,看看直接使用它的值是否仍然得到相同的结果:SELECT geo(1.2, 3.4, 5.6, 7.8);
@CL。 [geo(1.2, 3.4, 5.6, 7.8)] => 691.995
。当我更改代码时,我注意到我正在通过sprintf()
输出一个带有参数的查询,并且我正在使用 PDO 执行另一个准备好的查询。问题是,我没有将绑定参数传递给准备好的参数! :S 我现在很尴尬,我已经搞砸了好几个小时,之前我都没有发现。很抱歉浪费了您的时间,至少您将我引向了问题的根源。
【参考方案1】:
基本上,我使用sprintf()
来查看正在计算的边界坐标类型,并且由于我无法在 PHP 以外的任何地方运行查询(由于 UDF),我正在生成另一个准备好的查询陈述。问题是,我没有生成最后一个绑定参数(distance <= ?
子句中的公里),我被sprintf()
版本愚弄了。
我想我不应该在困倦时尝试编码。真的很抱歉浪费了您的时间,谢谢大家!
为了完整起见,以下返回(正确!)873 条记录,大约 0.04 秒:
SELECT "code",
geo(38.73311, -9.138707, "geo_latitude", "geo_longitude") AS "distance"
FROM "pt_postal" WHERE 1 = 1
AND "geo_latitude" BETWEEN 38.7241268076 AND 38.7420931924
AND "geo_longitude" BETWEEN -9.15022289523 AND -9.12719110477
AND "distance" <= 1
ORDER BY "distance" ASC
LIMIT 2048;
【讨论】:
【参考方案2】:这也返回 873 条记录,按 distance
在 ~0.04 秒内排序:
SELECT
"code",
geo(38.73311, -9.138707, "geo_latitude", "geo_longitude") AS "distance"
FROM "pt_postal" WHERE 1 = 1
AND "geo_latitude" BETWEEN 38.7241268076 AND 38.7420931924
AND "geo_longitude" BETWEEN -9.15022289523 AND -9.12719110477
GROUP BY "code"
HAVING "distance" <= 1
ORDER BY "distance" ASC
LIMIT 2048;
this page 没有GROUP BY
子句的原因是mysql specific:
HAVING 子句可以引用任何列或别名 select_expr 在 SELECT 列表 或外部子查询中,并 聚合函数。但是,SQL 标准要求 HAVING 必须仅引用 GROUP BY 子句中的列或 聚合函数。为了适应标准 SQL 和 能够引用 SELECT 中的列的 MySQL 特定行为 列表,MySQL 5.0.2 及更高版本允许 HAVING 引用 SELECT 列表,GROUP BY 子句中的列,outer 中的列 子查询和聚合函数。
如果没有可用的主键/唯一键,则以下 hack 也可以使用(虽然有点慢 - ~0.16 秒):
SELECT
"code",
geo(38.73311, -9.138707, "geo_latitude", "geo_longitude") AS "distance"
FROM "pt_postal" WHERE 1 = 1
AND "geo_latitude" BETWEEN 38.7241268076 AND 38.7420931924
AND "geo_longitude" BETWEEN -9.15022289523 AND -9.12719110477
GROUP BY _ROWID_
HAVING "distance" <= 1
ORDER BY "distance" ASC
LIMIT 2048;
【讨论】:
【参考方案3】:这个查询(由@OMGPonies提供):
SELECT *
FROM (
SELECT
"code",
geo(38.73311, -9.138707, "geo_latitude", "geo_longitude") AS "distance"
FROM "pt_postal" WHERE 1 = 1
AND "geo_latitude" BETWEEN 38.7241268076 AND 38.7420931924
AND "geo_longitude" BETWEEN -9.15022289523 AND -9.12719110477
)
WHERE "distance" <= 1
ORDER BY "distance" ASC
LIMIT 2048;
正确返回 873 条记录,按 distance
在 ~0.07 秒内排序。
但是,我仍然想知道为什么 SQLite 不在 WHERE
子句中评估 geo()
,like MySQL...
【讨论】:
我刚刚删除了我的答案,因为我看到它与这个相同。为什么这个答案是由您而不是 OMGPonies 发布的? @MikeSherrill'Catcall':这是我很久以前问过的另一个问题 (***.com/a/2099140/89771),但这个问题更糟糕,我当时对HAVING
子句和那个制造了很多噪音。认为发布另一个问题比重新提出一个令人困惑的问题更合适。【参考方案4】:
我无法判断 from the documentation 是否定义了 sqliteCreateFunction
是否定义了聚合(如 SUM
)或标量(如 sqrt
)。 WHERE
子句中不能引用聚合函数; HAVING
是必需的。
根据 SQLite UDF documentation,您需要知道是否仅填充了 xFunc,或者是否填充了 xStep 和 xFinal。这些是 SQLite 用来了解您正在定义的函数类型的指针,因此是否在 WHERE
子句中尊重它。
【讨论】:
聚合 UDF:php.net/manual/en/pdo.sqlitecreateaggregate.php 在这里。我正在创建一个常规的 UDF - 例如LENGTH
或 MD5
。
好的,好的。所以你发现了一个错误,无论是在 POD 的实现中还是在 SQLite 中。为了我的钱,我会押注 PHP。我已经用 C 编写了 SQLite UDF,但没有看到您报告的问题。以上是关于SQLite - WHERE 子句和 UDF的主要内容,如果未能解决你的问题,请参考以下文章
MySQL UDF 仅适用于 `IF` 中的 `WHERE` 子句
where子句中的mysql udf json_extract - 如何提高性能