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" &lt;= 1874 条记录。

原始查询返回的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" &lt;= 1 WHERE 子句过滤这些记录,distance 仍然是NULL =&gt; 0! 填充“代码”和“距离”(通过首次调用 UDF) 按“距离”排序(现在已填充) 限制记录

我想让 SQLite 做什么:

    使用索引coordinates过滤掉WHERE子句中边界外的记录 对于这些记录,通过调用 UDF 填充 codedistance"distance" &lt;= 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)] =&gt; 691.995。当我更改代码时,我注意到我正在通过sprintf() 输出一个带有参数的查询,并且我正在使用 PDO 执行另一个准备好的查询。问题是,我没有将绑定参数传递给准备好的参数! :S 我现在很尴尬,我已经搞砸了好几个小时,之前我都没有发现。很抱歉浪费了您的时间,至少您将我引向了问题的根源。 【参考方案1】:

基本上,我使用sprintf() 来查看正在计算的边界坐标类型,并且由于我无法在 PHP 以外的任何地方运行查询(由于 UDF),我正在生成另一个准备好的查询陈述。问题是,我没有生成最后一个绑定参数(distance &lt;= ? 子句中的公里),我被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,或者是否填充了 xStepxFinal。这些是 SQLite 用来了解您正在定义的函数类型的指针,因此是否在 WHERE 子句中尊重它。

【讨论】:

聚合 UDF:php.net/manual/en/pdo.sqlitecreateaggregate.php 在这里。我正在创建一个常规的 UDF - 例如 LENGTHMD5 好的,好的。所以你发现了一个错误,无论是在 POD 的实现中还是在 SQLite 中。为了我的钱,我会押注 PHP。我已经用 C 编写了 SQLite UDF,但没有看到您报告的问题。

以上是关于SQLite - WHERE 子句和 UDF的主要内容,如果未能解决你的问题,请参考以下文章

MySQL UDF 仅适用于 `IF` 中的 `WHERE` 子句

SQLite基础-7.子句

使用 WHERE 子句的 SQLite 更新和增量

where子句中的mysql udf json_extract - 如何提高性能

where子句中的mysql udf json_extract - 如何提高性能

《SQLite3 — 子句》