【问题标题】:Splitting up large data sets into smaller pieces for web viewing将大型数据集拆分为较小的部分以供 Web 查看
【发布时间】:2020-04-18 09:00:27
【问题描述】:

背景

我正在为网络应用程序开发一个网络平台。该平台的关键功能之一是用户管理。因此,我实现了一个登录系统,使注册用户能够登录平台和应用程序。这是使用 MySQL 数据库实现的,该数据库包含登录凭据、用户 ID、联系信息等...

问题

我遇到的问题是用户表。该表可能会非常大。因此,如果管理员想要编辑特定用户的信息,取决于用户在列表中的位置,管理员将不得不滚动浏览可能的数千条记录以找到正确的记录。我正在寻找一种算法,可以将其分成大约 N 大小的组,并将这些组显示为管理员可以点击的范围:#0-B、C-F、G-L、M-R 等......

研究

这是我目前所做的研究:

Database partition - Better done by PHP or MySQL?

1 very large table or 3 large table? MySQL Performance

关于分区,我在网上找到了很多文章来对数据库表进行分区,但没有任何内容能解决我想要做的事情。

到目前为止我做了什么

我编写了一个从 Web 服务器离线运行的程序,它返回每个存储桶 0-9 和 A-Z 的计数。对于小于阈值的计数,它将存储桶组合成一个范围并将其保存到数据库中。因此,如果桶 0-9 和 A、B 的总计数小于阈值,则范围变为 #0-B。如果桶的大小大于阈值,我在试图弄清楚如何从桶中获取范围时遇到问题。

我考虑过以下解决方案:

  1. 一个递归函数,它不断向下钻取用户名,直到它在阈值范围内。如果我可以让它工作,这将是理想的。
  2. 由于这是脱机运行,我正在考虑的另一个解决方案是将整个存储桶加载到内存中并从那里拆分。这样做的缺点是可能会占用内存。
  3. 另一个有趣的想法是#2 的变体,它将存储桶的一部分加载到内存中并对其进行处理。这会将内存使用量减少到或多或少的固定量,但需要更多时间来处理。

编辑 2020 年 5 月 11 日:

基于唯一答案和 cmets,我可以有一个搜索字段,当条目数缩小到阈值以下时,我可以使用 JSON 填充列表(这是个好主意)。但是,我确实有代码,我将与您分享:

此代码有效,但使用测试数据,它会在分区表中创建约 17,500 个条目,并且有相当多的条目计数为零。

function part_recurse2($prefix)
{
    global $CONFIGVAR;
    global $charList;
    global $charCount;

    $list = dbCountScan($prefix);
    for ($i = 0; $i < count($list); $i++)
    {
        $char = substr($charList, $i, 1);
        if ($list[$i] >= $CONFIGVAR['user_partition_max_size']['value'])
        {
            part_recurse2($prefix . $char);
        }
        else
        {
            writeRange($prefix . $char, $prefix . $char, $list[$i],
                substr($prefix . $char, 0, 1));
        }
    }
}

还有来自数据库的结果...

mysql> select sum(`count`) from userpart;
+--------------+
| sum(`count`) |
+--------------+
|       100004 |
+--------------+
1 row in set (0.01 sec)

mysql> select count(*) from userpart where count = 0;
+----------+
| count(*) |
+----------+
|     1139 |
+----------+
1 row in set (0.01 sec)

此代码部分有效,但计数不相加,并且其中也有零。正确的用户计数是 100,004,但下面的函数产生的总计数是 105,234,这比实际用户表中的条目多 5,230 个。这个版本的好处是它将组合范围直到达到阈值。这就是我想要开始的工作。

function part_recurse($prefix)
{
    global $charCount;
    global $charList;
    global $CONFIGVAR;

    $list = dbCountScan($prefix);
    $acc = 0;
    $start = substr($charList, 0, 1);
    for ($i = 0; $i < count($list); $i++)
    {
        if ($list[$i] == 0) continue;
        if ($list[$i] > $CONFIGVAR['user_partition_max_size']['value'])
        {
            // If the number of entries > threshold...
            if ($i == 0)
            {
                // Only for the first bucket.
                $start = substr($charList, 1, 1);
            }
            else
            {
                // All other entries.
                if ($i >= $charCount - 1)
                {
                    // Last entry
                    $end = substr($charList, $i - 1, 1);
                    $acc += $list[$i];
                    writeRange($prefix . $start, $prefix . $end, $acc,
                        substr($prefix . substr($charList, $i, 1), 0, 1));
                    $acc = 0;
                }
                else
                {
                    $end = substr($charList, $i - 1, 1);
                    writeRange($prefix . $start, $prefix . $end, $acc,
                        substr($prefix . substr($charList, $i + 1, 1), 0, 1));
                    $acc = 0;
                    $start = substr($charList, $i, 1);
                }
            }
            part_recurse($prefix . substr($charList, $i, 1));
        }
        else
        {
            if (($acc + $list[$i]) >= $CONFIGVAR['user_partition_max_size']['value'])
            {
                $end = substr($charList, $i - 1, 1);
                writeRange($prefix . $start, $prefix . $end, $acc,
                    substr($prefix . substr($charList, $i, 1), 0, 1));
                $start = substr($charList, $i, 1);
                $acc = $list[$i];
            }
            else
            {
                $acc += $list[$i];
            }
        }
    }

    // Write the final entry.
    if ($acc > 0)
    {
        $end = substr($charList, $charCount - 1, 1);
        $bucket = substr($prefix . substr($charList, $i, 1), 0, 1);
        writeRange($prefix . $start, $prefix . $end, $acc, $bucket);
    }
}

以及它的数据库结果:

mysql> select sum(`count`) from userpart;
+--------------+
| sum(`count`) |
+--------------+
|       105234 |
+--------------+
1 row in set (0.00 sec)
mysql> select count(*) from userpart where count = 0;
+----------+
| count(*) |
+----------+
|      316 |
+----------+
1 row in set (0.00 sec)

正确的条目数是 100,004,没有零计数。我会不断完善代码,但如果有人看到我做错了什么,请赐教。书外函数具有以下属性:

dbCountScan($prefix): 此函数使用 SELECT COUNT(*) FROM USERS WHERE username like '?% 遍历字符 0-9 和 A-Z ';在哪里 ?是 $prefix 和 for 循环中当前字母的串联。

WriteRange($start, $end, $count, $bucket): 该函数将范围写入数据库。 $start 和 $end 是范围的开始和结束。 $count 是范围内的条目数。而 $bucket 是这个范围所属的顶级存储桶(0-9,A-Z)。

这是数据库表用户部分:

CREATE TABLE `userpart` (
  `rangest` varchar(16) NOT NULL COMMENT 'Database query starting range.',
  `rangeed` varchar(16) NOT NULL COMMENT 'Database query ending range.',
  `count` int unsigned NOT NULL COMMENT 'Count in range.',
  `bucket` varchar(5) DEFAULT NULL COMMENT 'Primary bucket.'
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='Table that is used to partition usernames.'

【问题讨论】:

  • 这不就是简单的分页吗?
  • “如果管理员想要编辑特定用户的信息”...您不打算提供搜索功能以用户友好的方式启用这种功能吗?正如草莓所说,您所描述的其余内容似乎只是常规分页。我认为您可能过度设计了您提出的解决方案。
  • @Strawberry 如果系统上有 100,000 个用户,那么分页确实行不通。
  • @ADyson 我没有考虑包含搜索字段。我已经弄清楚了它是如何工作的。至于分页,我不确定系统上有 100,000 个帐户是否可行。即使是 10,000 也无法完全控制。
  • 随着用户数量的增加,我认为任何类型的列表都可能是徒劳的,无论是分区、分页还是其他。搜索功能(可能相对灵活,因此它可以根据属性组合和字段的部分匹配等找到用户)对我来说似乎是最好的选择。

标签: php mysql partitioning


【解决方案1】:

滚动可能有数千条记录

编写查询(可能是临时查询)以过滤到更少的记录。如果管理员需要滚动浏览超过几十条记录,那么您(和数据库)会让他失望。

分页——不要使用OFFSET; “记住你离开的地方”:http://mysql.rjweb.org/doc.php/pagination

分区 -- 否;你没有提出一个可以从中受益的案例。您可以简单地说 WHERE name LIKE 'J%'WHERE name &gt;= 'P' AND name &lt; 'T' (for p,q,r,s) 并有一个以 name 开头的 INDEX;但我怀疑这是否真的对管理员有帮助。

“不断向下钻取的递归函数”——这就是 BTree 索引已经为您做的事情。只需执行WHERE name &gt; $leftoff ORDER BY name LIMIT 20。即使用LIMIT作为桶大小;同时不要费心预先定义存储桶边界。

“潜在内存使用”——通常最好让数据库拥有大部分可用内存。

1 表 vs 3 -- 请详细说明这些表中的内容。

搜索

正如其他人所说,使用某种搜索机制可能是获得所需记录的最快方法。不要让管理员滚动数千行。

提供一个表单,其中包含一项或多项内容供管理员填写

  • 完整的用户名。 (问题:拼写错误/不知道确切的拼写)
  • 通配符部分名称。例:'丹%';然后显示所有以 Dan 开头的用户名。
  • 大量超链接,每个用户一个。这最多可以达到几千个;我建议不要这样做。
  • 用户的一个属性——开始日期、没有活动等等。然后搜索该属性。

对于这些部分情况,拒绝显示超过 100 个用户名;如果不止于此,请要求管理员提供更多详细信息。不要为分页带来的额外复杂性而烦恼。

我已经实施了各种这些和其他机制。回想起来,我唯一一次对几十个项目使用分页是当我需要对所有项目采取行动时。示例:从旅行中挑选一千张照片中的哪一张放入“相册”。这涉及到足够长的时间查看每张图片以选择或拒绝每张图片。此外,我使用 AJAX 进行单击即可完成所有需要的操作。

【讨论】:

  • 我用更多信息更新了这个问题。至于链接的问题 1 表与 3。我也不确定这些表中的内容。现在,我有两张桌子。将用户名链接到用户 ID 的主用户表指定帐户是否处于活动状态、登录方法、个人资料 ID 以及该用户的组织 ID。另一个表是用户部分,我已经发布了架构。
  • @DanielRudy - 我得和其他人一起推动一些搜索机制。 (我添加到我的答案中。)同时,我在你关于计数的讨论中迷失了。
  • 计数有点像红鲱鱼。它们用于跟踪存储桶中有多少用户,并在我开发平台时用于诊断目的。
  • 我不久前通过在数据库中存储一棵树解决了这个问题。我正在使用 FK 作为父节点和节点的路径。如果数据发生变化,则清空整个表并重新创建数据。使用这种方法,我现在可以深入了解用户列表。实现它的 RDBMS 方法是第一个答案,方法 3,位于此处:stackoverflow.com/questions/3362669/…
猜你喜欢
  • 1970-01-01
  • 2014-02-21
  • 2021-03-28
  • 1970-01-01
  • 2019-03-26
  • 1970-01-01
  • 1970-01-01
  • 2014-09-18
  • 1970-01-01
相关资源
最近更新 更多