1

介绍

我有以下 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;

它在 ~0.05 秒内返回 1035 条记录distance但是最后一条记录的“距离”为1.353km(大于我在 last 中定义为最大值的 1 km WHERE)。

如果我删除以下条款:

AND "geo_latitude" BETWEEN 38.7241268076 AND 38.7420931924
AND "geo_longitude" BETWEEN -9.15022289523 AND -9.12719110477

现在查询需要将近 6 秒,并返回 2048 条记录(我的LIMIT),按distance. 它应该花费这么长时间,但它应该只返回874 条记录"distance" <= 1

原始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 正在做:

  1. 使用索引过滤掉子句coordinates中边界之外的记录WHERE
  2. "distance" <= 1 WHERE按子句过滤这些记录,distance仍然是NULL => 0
  3. 填充“代码”和“距离”(通过第一次调用 UDF)
  4. 按“距离”排序(现在已填充)
  5. 限制记录

我希望 SQLite 做什么:

  1. 使用索引过滤掉子句coordinates中边界之外的记录WHERE
  2. 对于这些记录,填充codedistance通过调用 UDF
  3. "distance" <= 1 WHERE按子句过滤记录
  4. 按“距离”排序(无需再次调用 UDF)
  5. 限制记录

谁能解释我如何使 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()电话甚至没有被评估......但它应该,对吧?

4

4 回答 4

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;
于 2013-05-13T08:37:16.413 回答
0

此查询(@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 不在子句中进行评估geo()WHERE例如MySQL ...

于 2013-05-12T14:12:33.147 回答
0

这也返回 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;

此页面没有GROUP BY子句的原因是MySQL 特定的:

HAVING 子句可以引用 SELECT 列表或外部子查询中的 select_expr 中命名的任何列或别名,以及聚合函数。但是,SQL 标准要求 HAVING 必须仅引用 GROUP BY 子句中的列或聚合函数中使用的列。为了适应标准 SQL 和能够引用 SELECT 列表中的列的 MySQL 特定行为,MySQL 5.0.2 及更高版本允许 HAVING 引用 SELECT 列表中的列、GROUP BY 子句中的列、外部子查询中的列, 并聚合函数。


如果没有可用的主键/唯一键,则以下 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;
于 2013-05-12T15:48:23.190 回答
0

我无法从文档中判断是否sqliteCreateFunction定义了一个聚合,比如SUM,或者一个标量,比如sqrt。不能在WHERE子句中引用聚合函数;HAVING是必须的。

根据 SQLite UDF 文档,您需要知道是否仅填充了xFunc,或者是否填充了 xStepxFinal。这些是 SQLite 用来了解您正在定义的函数类型的指针,因此是否在WHERE子句中尊重它。

于 2013-05-13T04:01:08.480 回答