【问题标题】:Count the number of rows that contain a letter/number计算包含字母/数字的行数
【发布时间】:2012-05-10 16:07:54
【问题描述】:

我想要实现的目标很简单,但是有点难以解释,我不知道它是否真的可以在 postgres 中实现。我处于相当基本的水平。 SELECT, FROM, WHERE, LEFT JOIN ON, HAVING,等等基本的东西。

我正在尝试计算包含特定字母/数字的行数,并根据字母/数字显示该计数。

即有多少行包含“a/A”(不区分大小写)的条目

我要查询的表是电影名称列表。我要做的就是对“a-z”和“0-9”进行分组和计数,然后输出总数。我可以按顺序运行 36 个查询:

SELECT filmname FROM films WHERE filmname ilike '%a%'
SELECT filmname FROM films WHERE filmname ilike '%b%'
SELECT filmname FROM films WHERE filmname ilike '%c%'

然后在结果上运行 pg_num_rows 以找到我需要的数字,依此类推。

我知道喜欢是多么强烈,而且我更喜欢,所以我宁愿避免这种情况。尽管数据(如下)在数据中有大写和小写,但我希望结果集不区分大小写。即“盯着山羊的男人” a/A、t/T 和 s/S 不会对结果集计算两次。如果它使查询更简单或更易于构造,我可以将表复制到辅助工作表,所有数据都在 strtolower 并处理该组数据以进行查询。

另一种可能是

SELECT sum(length(regexp_replace(filmname, '[^X|^x]', '', 'g'))) FROM films;

对于每个字母组合,但又是 36 个查询、36 个数据集,我希望能在单个查询中获取数据。

这是我的一组 14 部电影的简短数据集(实际上包含 275 行)

District 9
Surrogates
The Invention Of Lying
Pandorum
UP
The Soloist
Cloudy With A Chance Of Meatballs
The Imaginarium of Doctor Parnassus
Cirque du Freak: The Vampires Assistant
Zombieland
9
The Men Who Stare At Goats
A Christmas Carol
Paranormal Activity

如果我手动将每个字母和数字排列在一列中,然后通过在该列中给它一个 x 来注册该字母是否出现在电影标题中,然后将它们计数以产生总数,我会在下面得到这样的结果. x 的每个垂直列都是该电影名称中的字母列表,无论该字母出现多少次或大小写。

上述短集的结果是:

A  x x  xxxx xxx  9 
B       x  x      2 
C x     xxx   xx  6
D x  x  xxxx      6
E  xx  xxxxx x    8
F   x   xxx       4 
G  xx    x   x    4
H   x  xxxx  xx   7
I x x  xxxxx  xx  9
J                 0
K         x       0
L   x  xx  x  xx  6
M    x  xxxx xxx  8
N   xx  xxxx x x  8
O  xxx xxx x xxx  10
P    xx  xx    x  5
Q         x       1
R xx x   xx  xxx  7
S xx   xxxx  xx   8
T xxx  xxxx  xxx  10
U  x xx xxx       6
V   x     x    x  3
W       x    x    2
X                 0 
Y   x   x      x  3
Z          x      1 
0                 0  
1                 0  
2                 0 
3                 0
4                 0
5                 0
6                 0
7                 0
8                 0
9 x         x     1

在上面的例子中,每一列都是一个“电影名” 正如你所看到的,第 5 列只标记了一个“u”和一个“p”,而第 11 列只标记了一个“9”。最后一列是每个字母的计数。

我想以某种方式构建一个查询,以某种方式为我提供结果行:A 9、B 2、C 6、D 6、E 8 等,同时考虑到从我的电影列中提取的每个行条目。如果该字母没有出现在任何行中,我想要一个零。

我不知道这是否可能,或者是否在 php 中系统地进行 36 个查询是唯一的可能性。

在当前数据集中有 275 个条目,并且每月增长约 8.33 个(每年 100 个)。我预测到 2019 年它将达到大约 1000 行,届时我无疑将使用一个完全不同的系统,因此我无需担心使用庞大的数据集进行拖网。

目前最长的标题是 50 个字符的“Percy Jackson & the Olympians: The Lightning Thief”(是的,我知道这部电影很烂;-),最短的是 1,“9”。

我正在运行 9.0.0 版的 Postgres。

抱歉,如果我以多种方式多次说过同一件事,我会尽量提供尽可能多的信息,以便您知道我想要达到的目标。

如果您需要任何说明或更大的数据集进行测试,请直接询问,我会根据需要进行编辑。

非常欢迎提出建议。

编辑 1

Erwin 感谢您的编辑/标签/建议。全部同意。

修复了 Erwin 建议的缺失的“9”错字。我的手动转录错误。

kgrittn,感谢您的建议,但我无法从 9.0.0 更新版本。我已询问我的提供商是否会尝试更新。

回应

感谢您的出色回复欧文

对于延迟回复深表歉意,但我一直在努力让您的查询正常工作并学习新关键字以理解您创建的查询。

我调整了查询​​以适应我的表结构,但结果集与预期不符(全为零),因此我直接复制了您的行并得到了相同的结果。

虽然两种情况下的结果集都列出了所有 36 行并带有适当的字母/数字,但所有行都显示为零作为计数 (ct)。

我试图解构查询以查看它可能在哪里崩溃。


的结果

SELECT DISTINCT id, unnest(string_to_array(lower(film), NULL)) AS letter
FROM  films


是“未找到行”。也许从更广泛的查询中提取它应该是,我不确定。

当我删除 unnest 函数时,结果是 14 行都带有“NULL”

如果我调整功能

COALESCE(y.ct, 0) to COALESCE(y.ct, 4)<br />

然后我的数据集对每个字母都以 4 来响应,而不是前面解释的零。

在 COALESCE 上简要阅读了“4”作为替代值,我猜测 y.ct 是 NULL 并被第二个值替换(这是为了覆盖序列中字母不匹配的行,即如果没有电影包含“q”,那么“q”列将具有零值而不是 NULL?)

我尝试使用的数据库是 SQL_ASCII,我想知道这是否是个问题,但我在一个运行 UTF-8 的 8.4.0 版本上得到了相同的结果。

如果我犯了一个明显的错误,但我无法返回我需要的数据集,我们深表歉意。

有什么想法吗?

再次感谢您的详细回复和解释。

【问题讨论】:

  • 你的结果表明显错了,'9'出现在两部电影中,但只有一个被标记为'x'。
  • @George:请阅读此页面,然后更新到更新的错误修复级别 9.0:postgresql.org/support/versioning

标签: sql postgresql count aggregate-functions


【解决方案1】:

这个查询应该可以完成这项工作:

测试用例:

CREATE TEMP TABLE films (id serial, film text);
INSERT INTO films (film) VALUES
 ('District 9')
,('Surrogates')
,('The Invention Of Lying')
,('Pandorum')
,('UP')
,('The Soloist')
,('Cloudy With A Chance Of Meatballs')
,('The Imaginarium of Doctor Parnassus')
,('Cirque du Freak: The Vampires Assistant')
,('Zombieland')
,('9')
,('The Men Who Stare At Goats')
,('A Christmas Carol')
,('Paranormal Activity');

查询:

SELECT l.letter, COALESCE(y.ct, 0) AS ct
FROM  (
    SELECT chr(generate_series(97, 122)) AS letter  -- a-z in UTF8!
    UNION ALL
    SELECT generate_series(0, 9)::text              -- 0-9
    ) l
LEFT JOIN (
    SELECT letter, count(id) AS ct
    FROM  (
        SELECT DISTINCT  -- count film once per letter
               id, unnest(string_to_array(lower(film), NULL)) AS letter
        FROM   films
        ) x
    GROUP  BY 1
    ) y  USING (letter)
ORDER  BY 1;

更改 string_to_array() 以便 NULL 分隔符将字符串拆分为 字符(Pavel Stehule)

以前这返回一个空值。

  • 您可以使用 regexp_split_to_table(lower(film), ''),而不是 unnest(string_to_array(lower(film), NULL))(在 9.1 之前的版本中工作!),但它通常会慢一些,并且长字符串时性能会下降。

  • 我使用generate_series()[a-z0-9] 生成为单独的行。并 LEFT JOIN 到查询中,因此每个字母都在结果中表示。

  • 使用DISTINCT 计算每部电影一次。

  • 不必担心 1000 行。这对于现代硬件上的现代 PostgreSQL 来说只是小菜一碟。

【讨论】:

  • 您可能还对扩展名unaccent感兴趣。
  • 幸运的是,我在这个例子中的表格永远不会有任何非 (a-z0-9) 字符的数据,但是谢谢你指出这个扩展。我不知道它,我可以看到它在未来很有用。谢谢。
  • 不幸的是,正如我在对上述问题的编辑/回复中所解释的那样,这将为我返回计数全为零的行。有什么想法吗?
  • @George:我很抱歉。我错过了我的解决方案需要 PostgreSQL 9.1。 regexp_split_to_table() 的替代方法适用于旧版本,并且速度稍慢。我相应地修改了我的答案。
  • 再次感谢您。我将尝试将我的安装更新到 9.1 或更高版本。与此同时,我将适应 regexp_split_to_table 解决方案。干杯!普罗斯特!
【解决方案2】:

下面是一个相当简单的解决方案,只需要一次表扫描。

SELECT 
    'a', SUM( (title ILIKE '%a%')::integer),
    'b', SUM( (title ILIKE '%b%')::integer),
    'c', SUM( (title ILIKE '%c%')::integer)
FROM film

我把其他 33 个字符留给你作为打字练习 :)

顺便说一句,1000 行对于 postgresql 数据库来说是很小的。当数据库大于服务器中的内存时,它开始变大。

编辑:有一个更好的主意

SELECT chars.c, COUNT(title)
FROM (VALUES ('a'), ('b'), ('c')) as chars(c)
    LEFT JOIN film ON title ILIKE ('%' || chars.c || '%')
GROUP BY chars.c
ORDER BY chars.c 

您还可以将 (VALUES ('a'), ('b'), ('c')) as chars(c) 部分替换为对包含您感兴趣的字符列表的表的引用。

【讨论】:

    【解决方案3】:

    这将在一行中为您提供结果,每个匹配的字母和数字对应一列。

    SELECT
      SUM(CASE WHEN POSITION('a' IN filmname) > 0 THEN 1 ELSE 0 END) AS "A",
      SUM(CASE WHEN POSITION('b' IN filmname) > 0 THEN 1 ELSE 0 END) AS "B",
      SUM(CASE WHEN POSITION('c' IN filmname) > 0 THEN 1 ELSE 0 END) AS "C",
      ...
      SUM(CASE WHEN POSITION('z' IN filmname) > 0 THEN 1 ELSE 0 END) AS "Z",
      SUM(CASE WHEN POSITION('0' IN filmname) > 0 THEN 1 ELSE 0 END) AS "0",
      SUM(CASE WHEN POSITION('1' IN filmname) > 0 THEN 1 ELSE 0 END) AS "1",
      ...
      SUM(CASE WHEN POSITION('9' IN filmname) > 0 THEN 1 ELSE 0 END) AS "9"
    FROM films;
    

    【讨论】:

      【解决方案4】:

      类似 Erwins 的方法,但从长远来看可能更舒服:

      为您感兴趣的每个字符创建一个表格:

      CREATE TABLE char (name char (1), id serial);
      INSERT INTO char (name) VALUES ('a');
      INSERT INTO char (name) VALUES ('b');
      INSERT INTO char (name) VALUES ('c');
      

      然后对它的值进行分组很容易:

      SELECT char.name, COUNT(*) 
        FROM char, film 
        WHERE film.name ILIKE '%' || char.name || '%' 
        GROUP BY char.name 
        ORDER BY char.name;
      

      不用担心 ILIKE。

      我对使用关键字“char”作为表格标题并不是 100% 满意,但到目前为止还没有遇到过不好的经历。另一方面,它是自然名称。也许如果你把它翻译成另一种语言——比如德语中的“zeichen”,你就可以避免歧义。

      【讨论】:

        猜你喜欢
        • 2011-01-24
        • 2017-11-22
        • 2019-11-12
        • 2021-07-30
        • 2011-06-01
        • 1970-01-01
        • 2023-02-21
        • 2022-01-14
        • 2020-11-11
        相关资源
        最近更新 更多