【问题标题】:How to get Count for large tables?如何获得大表的计数?
【发布时间】:2019-04-05 11:59:27
【问题描述】:

样本表:

+----+-------+-------+-------+-------+-------+---------------+
| id | col1  | col2  | col3  | col4  | col5  | modifiedTime  |
+----+-------+-------+-------+-------+-------+---------------+
|  1 | temp1 | temp2 | temp3 | temp4 | temp5 | 1554459626708 |
+----+-------+-------+-------+-------+-------+---------------+ 

上表有5000万条记录

  1. (col1、col2、col3、col4、col5 这些是 VARCHAR 列)
  2. (id 是 PK)
  3. (修改时间)

每一列都有索引

例如:我的网站中有两个标签。

FirstTab - 我使用以下标准打印上表的计数 [col1 like "value1%" and col2 like "value2%"]

SeocndTab - 我使用以下标准打印上表的计数 [col3 like "value3%"]


由于我有 5000 万条记录,因此按照这些标准进行计数需要花费太多时间才能得到结果。

注意:有时我会更改记录数据(表中的行)。插入新行。删除不需要的记录。

我需要一个可行的解决方案,而不是查询整个表。例如:喜欢缓存旧计数。有这样的可能吗?

【问题讨论】:

  • 请不要使用不适用于您的问题的标签。我删除了数据库标签,因为不清楚您实际使用的是哪一个。请添加only你实际使用的数据库的标签
  • Stack Overflow不是免费的代码编写服务。您应该尝试自己编写代码。在doing more research 之后,如果您有问题,您可以发布您尝试过的内容,并清楚地解释什么不起作用并提供Minimal, Complete, and Verifiable example。我建议阅读How to Ask 一个好问题和the perfect question。另外,请务必将tour阅读this
  • 如果我在任何数据库中找到了解决方案,那很好。
  • @JohnConde 在做了一些研究之后,我才来到这里。我想不出任何可能的解决方案。
  • @vinieth 是的,具体取决于您的写入模式。但是您需要更详细地描述您的约束条件。这是一个无法通过一次计数关闭的银行应用程序,还是某个夜间批处理作业执行一些通用报告等等等。

标签: mysql sql database postgresql performance


【解决方案1】:

虽然我确信 MySQL 可以,但这里有一个使用触发器的 Postgres 解决方案。

Count 存储在另一个表中,每个插入/更新/删除都有一个触发器,用于检查新行是否满足条件,如果满足,则将计数加 1。触发器的另一部分检查旧行是否满足条件,如果满足,则减 1。

以下是使用temp2 = '5' 计算行数的触发器的基本代码:

CREATE OR REPLACE FUNCTION updateCount() RETURNS TRIGGER AS 
$func$
BEGIN
   IF TG_OP = 'INSERT' OR TG_OP = 'UPDATE' THEN
      EXECUTE 'UPDATE someTableCount SET cnt = cnt + 1 WHERE 1 = (SELECT 1 FROM (VALUES($1.*)) x(id, temp1, temp2, temp3) WHERE x.temp2 = ''5'')'
      USING NEW;
   END IF;
   IF TG_OP = 'DELETE' OR TG_OP = 'UPDATE' THEN
      EXECUTE 'UPDATE someTableCount SET cnt = cnt - 1 WHERE 1 = (SELECT 1 FROM (VALUES($1.*)) x(id, temp1, temp2, temp3) WHERE x.temp2 = ''5'')'
      USING OLD;
   END IF;
   RETURN new;
END
$func$ LANGUAGE plpgsql;

Here's a working example on dbfiddle.

您当然可以修改触发器代码,使其具有动态 where 表达式并为每个表中的每个存储计数:

CREATE TABLE someTableCount
(
   whereExpr text,
   cnt INT
);

INSERT INTO someTableCount VALUES ('temp2 = ''5''', 0);

然后在触发器中循环遍历条件并进行相应更新。

【讨论】:

  • 触发器的另一部分检查旧行是否满足条件,如果满足,则减去 1 你能在这一行中解释一下吗.
  • 所以触发器中的第一个IF,当操作为INSERTUPDATE时检查新行(NEW指的是插入的行或更新后的行)看是否符合您的条件。如果是这样,它会将计数加 1。第二个IF,当操作为DELETEUPDATE时会检查旧行(OLD是指删除的行,或者更新之前的行)是否也满足条件。如果是这样,这意味着它需要从计数中删除,因为它不再存在,所以我们从计数中减去 1。
【解决方案2】:

FirstTab - 我使用以下标准打印上表的计数 [col1 like "value1%" and col2 like "value2%"]

这将受益于“复合”索引:

INDEX(col1, col2)

因为它会“覆盖”。 (也就是说,查询中需要的所有列都在一个索引中。)

SeocndTab - 我使用以下标准打印上表的计数 [col3 like "value3%"]

您显然已经有了最佳(覆盖)索引:

INDEX(col3)

现在,让我们从不同的角度来看它。您是否注意到搜索引擎不再为您提供匹配的行数?您正在找出原因——无论使用什么技术,计数都需要很长时间。

由于“col1”没有让我知道你的应用程序,也不知道计算了什么,我只能抛出一些一般性的建议:

  • 不要数数。
  • 预先计算计数,将它们保存在某处并提供“陈旧”值。如果只计算几个不同的“值”,这会很方便。对于任意字符串可能不实用。
  • 在输出中说“关于 nnnn”。
  • 玩一些技巧来决定是计算确切值还是只说“大约”是否可行。
  • 说“超过 1000”。

如果您想描述应用程序和列,也许我可以提供一些巧妙的技巧。

您对“插入速度”表示担忧。这通常不是问题,拥有 SELECTs 的“正确”索引的好处超过了 INSERTs. 的轻微性能损失

【讨论】:

  • Rick 为我的应用程序展示准确信息对我来说很重要。然后我不能在这里创建复合索引。因为如果假设我有 50 列。如果我为选项卡中的所有不同标准列创建复合索引。我认为创建如此大的索引是不可行的。那么未来标签的数量可能不会增加。
  • @vinieth 有什么办法可以让你分开桌子吗?如果任何列相互关联,您可以将它们拆分为不同的表,这将有助于使用复合索引并减小表的整体大小,使其更易于管理/搜索
  • 有些问题在 SQL 中是难以解决的。您必须放宽一些标准——需要通配符、精确计数、最新计数或其他。任何对大表中任意列集的尝试都必须(对于大多数查询)扫描整个表; 这需要时间
【解决方案3】:

听起来您在需要螺丝刀时尝试使用锤子。如果您不想运行批量计算,我建议您在添加或删除记录时使用 Flink 或 Samza 等流式处理框架来添加和减去您的计数。这正是构建这些框架的目的。

如果您致力于使用 SQL,您可以设置一个作业,在每个给定时间窗口执行所需的计数操作,并将值存储到第二个表中。这样您就不必在同一行中重复计数。

【讨论】:

    【解决方案4】:

    作为优化的一般经验法则(是的,每个表需要 1 个 SQL 服务器节点@50mio 条目!),这里列出了一些可能的优化技术,一些相当容易实现,其他可能需要更认真的修改:

    • 优化您的 MYSQL 字段类型和大小,例如。如果数据可以用数字表示,则使用 INT 而不是 VARCHAR,使用 SMALL INT 而不是 BIG INT,等等。如果您确实需要 VARCHAR,则使用尽可能小的每个字段的长度,

    • 查看您的数据集;是否有任何重复值?假设您的任何字段在 50mio 行中只有 5 个唯一值,然后将这些值保存到单独的表中,然后将 PK 链接到此示例表,

    • MYSQL 分区,基本的理解见this link,所以大体思路就是这样实现某种分区方案,例如CRONJOB 每天在“晚上”创建新分区,此时服务器利用率最低,或者当您达到另外 50k INSERT 左右时(顺便说一句,不同分区上的 UPDATE/DELETE 操作也需要一些额外的努力),

    • 缓存是另一种非常简单有效的方法,因为请求(几乎)相同的数据(我假设您的 value1%、value2%、value3% 始终相同?)再次。所以每隔一段时间做一次SELECT COUNT(),然后使用差分索引计数来获取实际选中的行数,

    • 内存数据库可以与传统的 SQL DB 一起使用来获取经常需要的数据:简单的键值对样式就足够了:Redis、Memcached、VoltDB、MemSQL 只是他们中有一些。还有,MYSQL也知道in-memory engine

    • 使用其他类型的数据库,例如 MongoDB 等 NoSQL 数据库,如果您的数据集/系统可以使用不同的概念。

    【讨论】:

      【解决方案5】:

      如果您正在寻找聚合性能并且并不真正关心插入时间,我会考虑将您的 Row DBMS 更改为 Column DBMS

      列 RDBMS 将数据存储为列,这意味着每列的索引独立于其他列。这允许更快的聚合,我已经从 Postgres 切换到 MonetDB(一个开源列 DBMS),并将 6 百万行表中的一个字段从大约 60 秒下降到 50 毫秒。我选择了 MonetDB,因为它支持 SQL 查询和 odbc 连接,这对我的用例来说是一个加分项,但您将体验到与其他列 DBMS 类似的性能改进。

      列存储有一个缺点,即您在插入、更新和删除查询时会损失性能,但从您所说的来看,我相信它不会对您产生太大影响。

      【讨论】:

      • 而且 MariaDB 有 ColumnStore。
      【解决方案6】:

      这将起作用:

      select count(*) from (
      select * from tablename where col1 like 'value1%' and col2 like 'value2%' and col3 
      like'value3%')
      where REGEXP_LIKE(col1,'^value1(.*)$') and REGEXP_LIKE(col2,'^value2(.*)$') and 
      REGEXP_LIKE(col1,'^value2(.*)$');
      

      尽量不要对所有列应用索引,因为它会减慢 sql 的处理速度 查询并将其仅放在必需的列中。

      【讨论】:

      • 嗨 nikhil,您能解释一下这个查询将如何提高计数计算的速度吗? :-)。
      【解决方案7】:

      在 Postgres 中,您可以从查询计划器管理的内部统计信息中获得估计的行数:

      SELECT reltuples AS approximate_row_count FROM pg_class WHERE relname = 'mytable';
      

      这里有更多详细信息:https://wiki.postgresql.org/wiki/Count_estimate

      您可以先创建一个物化视图。像这样的:

      CREATE MATERIALIZED VIEW mytable AS SELECT * FROM the_table WHERE col1 like "value1%" and col2 like "value2%";`
      

      您也可以直接实现计数查询。如果您有 10 个选项卡,那么您应该必须实现 10 个视图:

      CREATE MATERIALIZED VIEW count_tab1 AS SELECT count(*) FROM the_table WHERE col1 like "value1%" and col2 like "value2%";`
      CREATE MATERIALIZED VIEW count_tab2 AS SELECT count(*) FROM the_table WHERE col2 like "value2%" and col3 like "value3%";`
      ...
      

      每次插入后,您应该刷新视图(异步):

      REFRESH MATERIALIZED VIEW count_tab1
      REFRESH MATERIALIZED VIEW count_tab2
      ...
      

      【讨论】:

      • 是的,它就在那里。但我的问题是,我需要计算我应用的标准。
      • 我曾想过使用物化视图,但问题出现了。如果我有 10 个标签,每个标签都有自己的标准。因此,对于每种类型的标准,我们都需要 MVTIME 列。所以 mvtime 列数的数量将等于选项卡的数量,这是不好的。
      • 您将始终将相同数量的计数结果存储在任何位置(缓存、、实体化视图、表格行等)作为计数标准。我不明白为什么它不好。
      【解决方案8】:

      正如评论中所指出的,您还没有发布您尝试过的内容。所以我会假设问题的限制正是你发布的。所以请报告这么多的结果

      1. 您为问题的子集花费的当前时间是多少,即 [col1 like "value1%" 和 col2 like "value2%"] 和 2nd [col3 like "value3%] 的计数
      2. 诀窍是扫描数据源一次并通过创建索引使数据源更小。所以首先在 col1,col2,col3,id 上创建一个索引。 col3 和 id 的目的是使数据库只扫描索引。我会在同一个 SQL 中得到这两个计数
      select sum
             (
                 case 
                     when col1 like 'value1%' and col2 like 'value2%' then 1
                     else 0
                 end
             ) cnt_condition_1,
             sum
             (
                 case 
                     when col3 like 'value3%' then 1
                     else 0
                 end
             ) cnt_condition_2
      from table
      where (col1 like 'value1%' and col2 like 'value2%') or
            (col3 like 'value3%')
      ```
      So the 50M row table is probably very wide right now.  This should trim it down - on a reasonable server I would expect above to return in a few seconds.  If it does not and each condition returns < 10% of the table, second option will be to create multiple indexes for each scenario and do count for each so that index is used in each case.
      

      【讨论】:

      • OR,至少在 MySQL 中,通常是不可优化的,而且这个查询会导致对 50M 行的全表扫描。
      • 如果 mysql 不会优化 or 那么联合可以作为内联视图或 with 完成。但他需要发布结果,看看这种方法是否有效
      【解决方案9】:

      如果您的系统中没有发生批量插入/批量更新,您可以尝试在表中进行垂直分区吗?通过垂直分区,可以将col1、col2的数据块与表中的其他数据分开,从而减少搜索空间。

      此外,对每一列进行索引似乎并不是最好的方法。在绝对需要的地方建立索引。在这种情况下,我会说 Index(col1,col2) 和 Index(col3)。

      即使在建立索引之后,您也需要查看这些索引的碎片并相应地对其进行修改以获得最佳结果。因为,有时一列的 5000 万个索引可以作为一个巨大的块,这将限制您的 SQL 服务器的多处理能力。

      【讨论】:

        【解决方案10】:

        每个数据库在如何“增强”其 RDBMS 方面都有自己的特点。我不能代表 MySQL 或 SQL Server,但对于 PostgreSQL,您应该考虑将您搜索的索引设为基于 GIN(通用倒排索引)的索引。

        CREATE INDEX name ON table USING gin(col1);
        CREATE INDEX name ON table USING gin(col2);
        CREATE INDEX name ON table USING gin(col3);
        

        更多信息可以在here找到。

        -HTH

        【讨论】:

          猜你喜欢
          • 2013-10-15
          • 2015-03-02
          • 1970-01-01
          • 1970-01-01
          • 2011-07-07
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2018-04-01
          相关资源
          最近更新 更多