unaccent() 不适用于 plpgsql 动态查询中的希腊字母
unaccent() does not work with Greek letters in plpgsql dynamic query
我使用 PostgreSQL 10 并且 运行 CREATE EXTENSION unaccent;
成功。我有一个包含以下内容的 plgsql 函数
whereText := 'lower(unaccent(place.name)) LIKE lower(unaccent())';
稍后,根据用户的选择,可能会在 whereText
中添加更多的条款。
whereText
最终在查询中使用:
placewithkeys := '%'||placename||'%';
RETURN QUERY EXECUTE format('SELECT id, name FROM '||fromText||' WHERE '||whereText)
USING placewithkeys , event, date;
whereText := 'LOWER(unaccent(place.name)) LIKE LOWER(unaccent())';
不起作用,即使我删除了 LOWER
部分。
我做了 select __my_function('Τζι');
但我什么也没得到,即使我应该得到结果,因为在数据库中有名称 Τζίμα
如果我删除 unaccent
并保留 LOWER
它可以工作,但对口音无效:τζ
使 Τζίμα
恢复正常。 unaccent
似乎引起了问题。
我错过了什么?我怎样才能解决这个问题?
由于有关于语法和可能的 SQLi 的评论,我提供了整个函数定义,现在改为在希腊语中不区分重音和不区分大小写:
CREATE FUNCTION __a_search_place
(placename text, eventtype integer, eventdate integer, eventcentury integer, constructiondate integer, constructioncentury integer, arstyle integer, artype integer)
RETURNS TABLE
(place_id bigint, place_name text, place_geom geometry)
AS $$
DECLARE
selectText text;
fromText text;
whereText text;
usingText text;
placewithkeys text;
BEGIN
fromText := '
place
JOIN cep ON place.id = cep.place_id
JOIN event ON cep.event_id = event.id
';
whereText := 'unaccent(place.name) iLIKE unaccent()';
placewithkeys := '%'||placename||'%';
IF constructiondate IS NOT NULL OR constructioncentury IS NOT NULL OR arstyle IS NOT NULL OR artype IS NOT NULL THEN
fromText := fromText || '
JOIN construction ON cep.construction_id = construction.id
JOIN construction_atype ON construction.id = construction_atype.construction_id
JOIN construction_astyle ON construction.id = construction_astyle.construction_id
JOIN atype ON atype.id = construction_atype.atype_id
JOIN astyle ON astyle.id = construction_astyle.astyle_id
';
END IF;
IF eventtype IS NOT NULL THEN
whereText := whereText || 'AND event.type = ';
END IF;
IF eventdate IS NOT NULL THEN
whereText := whereText || 'AND event.date = ';
END IF;
IF eventcentury IS NOT NULL THEN
whereText := whereText || 'AND event.century = ';
END IF;
IF constructiondate IS NOT NULL THEN
whereText := whereText || 'AND construction.date = ';
END IF;
IF constructioncentury IS NOT NULL THEN
whereText := whereText || 'AND construction.century = ';
END IF;
IF arstyle IS NOT NULL THEN
whereText := whereText || 'AND astyle.id = ';
END IF;
IF artype IS NOT NULL THEN
whereText := whereText || 'AND atype.id = ';
END IF;
whereText := whereText || '
GROUP BY place.id, place.geom, place.name
';
RETURN QUERY EXECUTE format('SELECT place.id, place.name, place.geom FROM '||fromText||' WHERE '||whereText)
USING placewithkeys, eventtype, eventdate, eventcentury, constructiondate, constructioncentury, arstyle, artype ;
END;
$$
LANGUAGE plpgsql;
Postgres 12
unaccent()
现在也适用于希腊字母。变音符号已删除:
db<>fiddle here
Allow unaccent to remove accents from Greek characters (Tasos Maschalidis)
Postgres 11 或更早版本
unaccent()
还不适用于希腊字母。来电:
SELECT unaccent('
ἀ ἁ ἂ ἃ ἄ ἅ ἆ ἇ Ἀ Ἁ Ἂ Ἃ Ἄ Ἅ Ἆ Ἇ
ἐ ἑ ἒ ἓ ἔ ἕ Ἐ Ἑ Ἒ Ἓ Ἔ Ἕ
ἠ ἡ ἢ ἣ ἤ ἥ ἦ ἧ Ἠ Ἡ Ἢ Ἣ Ἤ Ἥ Ἦ Ἧ
ἰ ἱ ἲ ἳ ἴ ἵ ἶ ἷ Ἰ Ἱ Ἲ Ἳ Ἴ Ἵ Ἶ Ἷ
ὀ ὁ ὂ ὃ ὄ ὅ Ὀ Ὁ Ὂ Ὃ Ὄ Ὅ
ὐ ὑ ὒ ὓ ὔ ὕ ὖ ὗ Ὑ Ὓ Ὕ Ὗ
ὠ ὡ ὢ ὣ ὤ ὥ ὦ ὧ Ὠ Ὡ Ὢ Ὣ Ὤ Ὥ Ὦ Ὧ
ὰ ά ὲ έ ὴ ή ὶ ί ὸ ό ὺ ύ ὼ ώ
ᾀ ᾁ ᾂ ᾃ ᾄ ᾅ ᾆ ᾇ ᾈ ᾉ ᾊ ᾋ ᾌ ᾍ ᾎ ᾏ
ᾐ ᾑ ᾒ ᾓ ᾔ ᾕ ᾖ ᾗ ᾘ ᾙ ᾚ ᾛ ᾜ ᾝ ᾞ ᾟ
ᾠ ᾡ ᾢ ᾣ ᾤ ᾥ ᾦ ᾧ ᾨ ᾩ ᾪ ᾫ ᾬ ᾭ ᾮ ᾯ
ᾰ ᾱ ᾲ ᾳ ᾴ ᾶ ᾷ Ᾰ Ᾱ Ὰ Ά ᾼ ᾽ ι ᾿
῀ ῁ ῂ ῃ ῄ ῆ ῇ Ὲ Έ Ὴ Ή ῌ ῍ ῎ ῏
ῐ ῑ ῒ ΐ ῖ ῗ Ῐ Ῑ Ὶ Ί ῝ ῞ ῟
ῠ ῡ ῢ ΰ ῤ ῥ ῦ ῧ Ῠ Ῡ Ὺ Ύ Ῥ ῭ ΅ `
ῲ ῳ ῴ ῶ ῷ Ὸ Ό Ὼ Ώ ῼ ´ ῾ ');
... returns 所有字母都没有改变,没有像我们预期的那样删除变音符号。
(我从 Wikipedia page on Greek diacritics 中提取了这个列表。)
db<>fiddle here
看起来像 unaccent module 的缺点。您可以扩展默认的 unaccent
字典或创建您自己的字典。说明书上有说明。我过去创建了几本词典,很简单。你不是首先需要这个:
希腊字符的 Postgres 无重音规则:
用于 Postgres 9.6 的非重音规则加上希腊字符:
不过,您需要对服务器文件系统的写入权限 - 包含 unaccent 文件的目录。所以,在大多数云服务上是不可能的...
或者您可以 report a bug 并要求包括希腊变音符号。
旁白:动态 SQL 和 SQLi
您提供的代码片段不易受SQL注入攻击。 </code> 连接为文字字符串,仅在稍后的 <code>EXECUTE
命令中解析,其中值通过 USING
子句安全传递。所以,那里没有不安全的串联。不过,我会这样做:
RETURN QUERY EXECUTE format(
$q$
SELECT id, name
FROM place ...
WHERE lower(unaccent(place.name)) LIKE '%' || lower(unaccent()) || '%'
$q$
)
USING placename, event, date;
备注:
减少混淆 - 你原来在评论中甚至混淆了 Pavel,该领域的专业人士。
plpgsql 中的赋值稍微昂贵(比其他 PL 中的更高),因此采用赋值较少的编码风格。
将 LIKE
的两个 %
符号直接连接到主查询中,为查询规划器提供模式未锚定到开始或结束的信息,可能有助于制定更有效的计划。只有用户输入(安全地)作为变量传递。
由于您的 WHERE
子句引用了 table place
,因此 FROM
子句无论如何都需要包含此 table。因此,您不能一开始就独立地连接 FROM 子句。最好将所有内容保存在一个 format()
.
中
使用美元引号,这样您就不必另外转义单引号了。
- Insert text with single quotes in PostgreSQL
- What are '$$' used for in PL/pgSQL
也许 只需使用 ILIKE
而不是 lower(...) LIKE lower(...)
。如果你使用三字母索引(对于这个查询来说似乎是最好的):那些也适用于 ILIKE
:
- LOWER LIKE vs iLIKE
我假设您知道您可能需要转义 LIKE
模式中具有特殊含义的字符?
- How to escape string while matching pattern in PostgreSQL
- Escape function for regular expression or LIKE patterns
审核函数
在您提供完整的功能之后...
CREATE OR REPLACE FUNCTION __a_search_place(
placename text
, eventtype int = NULL
, eventdate int = NULL
, eventcentury int = NULL
, constructiondate int = NULL
, constructioncentury int = NULL
, arstyle int = NULL
, artype int = NULL)
RETURNS TABLE(place_id bigint, place_name text, place_geom geometry) AS
$func$
BEGIN
-- RAISE NOTICE '%', concat_ws(E'\n' -- to debug
RETURN QUERY EXECUTE concat_ws(E'\n'
,'SELECT p.id, p.name, p.geom
FROM place p
WHERE unaccent(p.name) ILIKE (''%'' || unaccent() || ''%'')' -- no $-quotes
-- any input besides placename ()
, CASE WHEN NOT (,,,,,,) IS NULL THEN
'AND EXISTS (
SELECT
FROM cep
JOIN event e ON e.id = cep.event_id' END
-- constructiondate, constructioncentury, arstyle, artype
, CASE WHEN NOT (,,,) IS NULL THEN
'JOIN construction con ON cep.construction_id = con.id
JOIN construction_atype ON con.id = construction_atype.construction_id
JOIN construction_astyle ON con.id = construction_astyle.construction_id' END
-- arstyle, artype
, CASE WHEN NOT (,) IS NULL THEN
'JOIN atype ON atype.id = construction_atype.atype_id
JOIN astyle ON astyle.id = construction_astyle.astyle_id' END
, CASE WHEN NOT (,,,,,,) IS NULL THEN
'WHERE cep.place_id = p.id' END
, CASE WHEN eventtype IS NOT NULL THEN 'AND e.type = ' END
, CASE WHEN eventdate IS NOT NULL THEN 'AND e.date = ' END
, CASE WHEN eventcentury IS NOT NULL THEN 'AND e.century = ' END
, CASE WHEN constructiondate IS NOT NULL THEN 'AND con.date = ' END
, CASE WHEN constructioncentury IS NOT NULL THEN 'AND con.century = ' END
, CASE WHEN arstyle IS NOT NULL THEN 'AND astyle.id = ' END
, CASE WHEN artype IS NOT NULL THEN 'AND atype.id = ' END
, CASE WHEN NOT (,,,,,,) IS NULL THEN
')' END
);
USING placename
, eventtype
, eventdate
, eventcentury
, constructiondate
, constructioncentury
, arstyle
, artype;
END
$func$ LANGUAGE plpgsql;
这是 完全重写,有几处改进。应该使功能大幅提升。还有 SQLi-safe(就像你原来的一样)。应该在功能上相同 除了 我加入较少 table 的情况,这可能不会过滤通过加入过滤的行table一个人。
主要特点:
使用 EXISTS()
而不是在外层加上 GROUP BY
的大量连接。这有助于实现更好的性能。相关:
- Search a JSON array for an object containing a value matching a pattern
format()
通常是连接用户输入的 SQL 的不错选择。但是由于您封装了所有代码元素并且只传递标志,因此在这种情况下您不需要它。相反,concat_ws()
是有帮助的。相关:
- How to concatenate columns in a Postgres SELECT?
仅连接您实际需要的 JOIN。
更少的赋值,更短的代码。
参数的默认值。允许缺少参数的简化调用。喜欢:
SELECT __a_search_place('foo', 2, 3, 4);
SELECT __a_search_place('foo');
相关:
- Optional argument in PL/pgSQL function
关于用于测试任何值是否为 NOT NULL
的简短 ROW()
语法:
- Why is IS NOT NULL false when checking a row type?
我使用 PostgreSQL 10 并且 运行 CREATE EXTENSION unaccent;
成功。我有一个包含以下内容的 plgsql 函数
whereText := 'lower(unaccent(place.name)) LIKE lower(unaccent())';
稍后,根据用户的选择,可能会在 whereText
中添加更多的条款。
whereText
最终在查询中使用:
placewithkeys := '%'||placename||'%';
RETURN QUERY EXECUTE format('SELECT id, name FROM '||fromText||' WHERE '||whereText)
USING placewithkeys , event, date;
whereText := 'LOWER(unaccent(place.name)) LIKE LOWER(unaccent())';
不起作用,即使我删除了 LOWER
部分。
我做了 select __my_function('Τζι');
但我什么也没得到,即使我应该得到结果,因为在数据库中有名称 Τζίμα
如果我删除 unaccent
并保留 LOWER
它可以工作,但对口音无效:τζ
使 Τζίμα
恢复正常。 unaccent
似乎引起了问题。
我错过了什么?我怎样才能解决这个问题?
由于有关于语法和可能的 SQLi 的评论,我提供了整个函数定义,现在改为在希腊语中不区分重音和不区分大小写:
CREATE FUNCTION __a_search_place
(placename text, eventtype integer, eventdate integer, eventcentury integer, constructiondate integer, constructioncentury integer, arstyle integer, artype integer)
RETURNS TABLE
(place_id bigint, place_name text, place_geom geometry)
AS $$
DECLARE
selectText text;
fromText text;
whereText text;
usingText text;
placewithkeys text;
BEGIN
fromText := '
place
JOIN cep ON place.id = cep.place_id
JOIN event ON cep.event_id = event.id
';
whereText := 'unaccent(place.name) iLIKE unaccent()';
placewithkeys := '%'||placename||'%';
IF constructiondate IS NOT NULL OR constructioncentury IS NOT NULL OR arstyle IS NOT NULL OR artype IS NOT NULL THEN
fromText := fromText || '
JOIN construction ON cep.construction_id = construction.id
JOIN construction_atype ON construction.id = construction_atype.construction_id
JOIN construction_astyle ON construction.id = construction_astyle.construction_id
JOIN atype ON atype.id = construction_atype.atype_id
JOIN astyle ON astyle.id = construction_astyle.astyle_id
';
END IF;
IF eventtype IS NOT NULL THEN
whereText := whereText || 'AND event.type = ';
END IF;
IF eventdate IS NOT NULL THEN
whereText := whereText || 'AND event.date = ';
END IF;
IF eventcentury IS NOT NULL THEN
whereText := whereText || 'AND event.century = ';
END IF;
IF constructiondate IS NOT NULL THEN
whereText := whereText || 'AND construction.date = ';
END IF;
IF constructioncentury IS NOT NULL THEN
whereText := whereText || 'AND construction.century = ';
END IF;
IF arstyle IS NOT NULL THEN
whereText := whereText || 'AND astyle.id = ';
END IF;
IF artype IS NOT NULL THEN
whereText := whereText || 'AND atype.id = ';
END IF;
whereText := whereText || '
GROUP BY place.id, place.geom, place.name
';
RETURN QUERY EXECUTE format('SELECT place.id, place.name, place.geom FROM '||fromText||' WHERE '||whereText)
USING placewithkeys, eventtype, eventdate, eventcentury, constructiondate, constructioncentury, arstyle, artype ;
END;
$$
LANGUAGE plpgsql;
Postgres 12
unaccent()
现在也适用于希腊字母。变音符号已删除:
db<>fiddle here
Allow unaccent to remove accents from Greek characters (Tasos Maschalidis)
Postgres 11 或更早版本
unaccent()
还不适用于希腊字母。来电:
SELECT unaccent('
ἀ ἁ ἂ ἃ ἄ ἅ ἆ ἇ Ἀ Ἁ Ἂ Ἃ Ἄ Ἅ Ἆ Ἇ
ἐ ἑ ἒ ἓ ἔ ἕ Ἐ Ἑ Ἒ Ἓ Ἔ Ἕ
ἠ ἡ ἢ ἣ ἤ ἥ ἦ ἧ Ἠ Ἡ Ἢ Ἣ Ἤ Ἥ Ἦ Ἧ
ἰ ἱ ἲ ἳ ἴ ἵ ἶ ἷ Ἰ Ἱ Ἲ Ἳ Ἴ Ἵ Ἶ Ἷ
ὀ ὁ ὂ ὃ ὄ ὅ Ὀ Ὁ Ὂ Ὃ Ὄ Ὅ
ὐ ὑ ὒ ὓ ὔ ὕ ὖ ὗ Ὑ Ὓ Ὕ Ὗ
ὠ ὡ ὢ ὣ ὤ ὥ ὦ ὧ Ὠ Ὡ Ὢ Ὣ Ὤ Ὥ Ὦ Ὧ
ὰ ά ὲ έ ὴ ή ὶ ί ὸ ό ὺ ύ ὼ ώ
ᾀ ᾁ ᾂ ᾃ ᾄ ᾅ ᾆ ᾇ ᾈ ᾉ ᾊ ᾋ ᾌ ᾍ ᾎ ᾏ
ᾐ ᾑ ᾒ ᾓ ᾔ ᾕ ᾖ ᾗ ᾘ ᾙ ᾚ ᾛ ᾜ ᾝ ᾞ ᾟ
ᾠ ᾡ ᾢ ᾣ ᾤ ᾥ ᾦ ᾧ ᾨ ᾩ ᾪ ᾫ ᾬ ᾭ ᾮ ᾯ
ᾰ ᾱ ᾲ ᾳ ᾴ ᾶ ᾷ Ᾰ Ᾱ Ὰ Ά ᾼ ᾽ ι ᾿
῀ ῁ ῂ ῃ ῄ ῆ ῇ Ὲ Έ Ὴ Ή ῌ ῍ ῎ ῏
ῐ ῑ ῒ ΐ ῖ ῗ Ῐ Ῑ Ὶ Ί ῝ ῞ ῟
ῠ ῡ ῢ ΰ ῤ ῥ ῦ ῧ Ῠ Ῡ Ὺ Ύ Ῥ ῭ ΅ `
ῲ ῳ ῴ ῶ ῷ Ὸ Ό Ὼ Ώ ῼ ´ ῾ ');
... returns 所有字母都没有改变,没有像我们预期的那样删除变音符号。
(我从 Wikipedia page on Greek diacritics 中提取了这个列表。)
db<>fiddle here
看起来像 unaccent module 的缺点。您可以扩展默认的 unaccent
字典或创建您自己的字典。说明书上有说明。我过去创建了几本词典,很简单。你不是首先需要这个:
希腊字符的 Postgres 无重音规则:
用于 Postgres 9.6 的非重音规则加上希腊字符:
不过,您需要对服务器文件系统的写入权限 - 包含 unaccent 文件的目录。所以,在大多数云服务上是不可能的...
或者您可以 report a bug 并要求包括希腊变音符号。
旁白:动态 SQL 和 SQLi
您提供的代码片段不易受SQL注入攻击。 </code> 连接为文字字符串,仅在稍后的 <code>EXECUTE
命令中解析,其中值通过 USING
子句安全传递。所以,那里没有不安全的串联。不过,我会这样做:
RETURN QUERY EXECUTE format(
$q$
SELECT id, name
FROM place ...
WHERE lower(unaccent(place.name)) LIKE '%' || lower(unaccent()) || '%'
$q$
)
USING placename, event, date;
备注:
减少混淆 - 你原来在评论中甚至混淆了 Pavel,该领域的专业人士。
plpgsql 中的赋值稍微昂贵(比其他 PL 中的更高),因此采用赋值较少的编码风格。
将
LIKE
的两个%
符号直接连接到主查询中,为查询规划器提供模式未锚定到开始或结束的信息,可能有助于制定更有效的计划。只有用户输入(安全地)作为变量传递。由于您的
WHERE
子句引用了 tableplace
,因此FROM
子句无论如何都需要包含此 table。因此,您不能一开始就独立地连接 FROM 子句。最好将所有内容保存在一个format()
. 中
使用美元引号,这样您就不必另外转义单引号了。
- Insert text with single quotes in PostgreSQL
- What are '$$' used for in PL/pgSQL
也许 只需使用
ILIKE
而不是lower(...) LIKE lower(...)
。如果你使用三字母索引(对于这个查询来说似乎是最好的):那些也适用于ILIKE
:- LOWER LIKE vs iLIKE
我假设您知道您可能需要转义
LIKE
模式中具有特殊含义的字符?- How to escape string while matching pattern in PostgreSQL
- Escape function for regular expression or LIKE patterns
审核函数
在您提供完整的功能之后...
CREATE OR REPLACE FUNCTION __a_search_place(
placename text
, eventtype int = NULL
, eventdate int = NULL
, eventcentury int = NULL
, constructiondate int = NULL
, constructioncentury int = NULL
, arstyle int = NULL
, artype int = NULL)
RETURNS TABLE(place_id bigint, place_name text, place_geom geometry) AS
$func$
BEGIN
-- RAISE NOTICE '%', concat_ws(E'\n' -- to debug
RETURN QUERY EXECUTE concat_ws(E'\n'
,'SELECT p.id, p.name, p.geom
FROM place p
WHERE unaccent(p.name) ILIKE (''%'' || unaccent() || ''%'')' -- no $-quotes
-- any input besides placename ()
, CASE WHEN NOT (,,,,,,) IS NULL THEN
'AND EXISTS (
SELECT
FROM cep
JOIN event e ON e.id = cep.event_id' END
-- constructiondate, constructioncentury, arstyle, artype
, CASE WHEN NOT (,,,) IS NULL THEN
'JOIN construction con ON cep.construction_id = con.id
JOIN construction_atype ON con.id = construction_atype.construction_id
JOIN construction_astyle ON con.id = construction_astyle.construction_id' END
-- arstyle, artype
, CASE WHEN NOT (,) IS NULL THEN
'JOIN atype ON atype.id = construction_atype.atype_id
JOIN astyle ON astyle.id = construction_astyle.astyle_id' END
, CASE WHEN NOT (,,,,,,) IS NULL THEN
'WHERE cep.place_id = p.id' END
, CASE WHEN eventtype IS NOT NULL THEN 'AND e.type = ' END
, CASE WHEN eventdate IS NOT NULL THEN 'AND e.date = ' END
, CASE WHEN eventcentury IS NOT NULL THEN 'AND e.century = ' END
, CASE WHEN constructiondate IS NOT NULL THEN 'AND con.date = ' END
, CASE WHEN constructioncentury IS NOT NULL THEN 'AND con.century = ' END
, CASE WHEN arstyle IS NOT NULL THEN 'AND astyle.id = ' END
, CASE WHEN artype IS NOT NULL THEN 'AND atype.id = ' END
, CASE WHEN NOT (,,,,,,) IS NULL THEN
')' END
);
USING placename
, eventtype
, eventdate
, eventcentury
, constructiondate
, constructioncentury
, arstyle
, artype;
END
$func$ LANGUAGE plpgsql;
这是 完全重写,有几处改进。应该使功能大幅提升。还有 SQLi-safe(就像你原来的一样)。应该在功能上相同 除了 我加入较少 table 的情况,这可能不会过滤通过加入过滤的行table一个人。
主要特点:
使用
EXISTS()
而不是在外层加上GROUP BY
的大量连接。这有助于实现更好的性能。相关:- Search a JSON array for an object containing a value matching a pattern
format()
通常是连接用户输入的 SQL 的不错选择。但是由于您封装了所有代码元素并且只传递标志,因此在这种情况下您不需要它。相反,concat_ws()
是有帮助的。相关:- How to concatenate columns in a Postgres SELECT?
仅连接您实际需要的 JOIN。
更少的赋值,更短的代码。
参数的默认值。允许缺少参数的简化调用。喜欢:
SELECT __a_search_place('foo', 2, 3, 4); SELECT __a_search_place('foo');
相关:
- Optional argument in PL/pgSQL function
关于用于测试任何值是否为
NOT NULL
的简短ROW()
语法:- Why is IS NOT NULL false when checking a row type?