在数据库中存储多维数组:关系数据还是多维数据?

时间:2016-09-23 08:54:32

标签: sql database performance postgresql multidimensional-array

我已经阅读了很多关于多维到单维多维数据库等等的帖子,但是没有一个答案有帮助。我确实在Google上找到了很多文档,但这些文档只提供了背景信息,并没有回答手头的问题。

我有很多相互关联的字符串。它们在PHP脚本中是必需的。结构是分层的。这是一个例子。

A:
  AA:
    AAA
    AAC
  AB
  AE:
    AEA
    AEE:
      AEEB
B:
  BA:
    BAA
  BD:
    BDC:
      BDCB
      BDCE
    BDD:
      BDDA
  BE:
    BED:
      BEDA
C:
  CC:
    CCB:
      CCBC
      CCBE
    CCC:
      CCCA
      CCCE
  CE

每个缩进都会在多维数组中设置一个新级别。

目标是使用PHP及其所有后代检索元素。例如,如果我查询A,我想收到一个包含array('A', 'AA', 'AAA', 'AAC', 'AB', 'AE', 'AEA', 'AEE', 'AEEB')的字符串数组。问题'是也可以对较低级别的元素进行查询。如果我查询AEE,我想获得array('AEE', 'AEEB')

正如我理解关系数据库的概念,这意味着我不能使用关系数据库,因为没有共同的关键数据库'元素之间。我认为可能的解决方案是将PARENT元素分配给每个单元格。所以,在表格中:

CELL | PARENT
A      NULL
AA     A
AAA    AA
AAC    AA
AB     A
AE     A
AEA    AE
AEE    AE
AEEB   AEE

通过这样做,我认为您应该能够查询给定的字符串以及共享此父项的所有项目,然后递归地沿着此路径向下,直到找不到更多项目。 然而,这对我来说似乎相当缓慢,因为需要在每个级别上查看整个搜索空间 - 这正是您在多维数组中不想要的。

所以我有点茫然。请注意,实际上大约有100,000个字符串以这种方式构建,因此速度很重要。幸运的是,数据库是静态的,不会改变。如何在数据库中存储这样的数据结构而不必处理长循环和搜索时间? 哪种数据库软件和数据类型最适合这个?我注意到PostgreSQL已经出现在我们的服务器上,所以我宁愿坚持下去。

正如我所说,我是数据库的新手,但我非常渴望学习。因此,我正在寻找一个详尽的答案,并提供某种方法的优点和缺点。表现是关键。预期的答案将包含此用例的最佳数据库类型和语言,并且还使用该语言编写脚本以构建此类结构。

6 个答案:

答案 0 :(得分:14)

  

目标是使用PHP及其所有后代检索元素。

如果这就是您所需要的,您可以使用LIKE搜索

SELECT *
FROM Table1
WHERE CELL LIKE 'AEE%';

索引以CELL开头,这是一个范围检查,速度很快。

如果您的数据看起来不像那样,您可以创建一个path列,它看起来像一个目录路径,并且包含所有节点"在路径/路径上#34;从根到元素。

| id | CELL | parent_id | path     |
|====|======|===========|==========|
|  1 | A    |      NULL | 1/       |
|  2 | AA   |         1 | 1/2/     |
|  3 | AAA  |         2 | 1/2/3/   |
|  4 | AAC  |         2 | 1/2/4/   |
|  5 | AB   |         1 | 1/5/     |
|  6 | AE   |         1 | 1/6/     | 
|  7 | AEA  |         6 | 1/6/7/   |
|  8 | AEE  |         6 | 1/6/8/   |
|  9 | AEEB |         8 | 1/6/8/9/ |

检索' AE'的所有后代。 (包括它自己)你的查询将是

SELECT *
FROM tree t
WHERE path LIKE '1/6/%';

或(MySQL特定级联)

SELECT t.*
FROM tree t
CROSS JOIN tree r -- root
WHERE r.CELL = 'AE'
  AND t.path LIKE CONCAT(r.path, '%');

结果:

| id | CELL | parent_id |     path |
|====|======|===========|==========|
|  6 | AE   |         1 | 1/6/     |
|  7 | AEA  |         6 | 1/6/7/   |
|  8 | AEE  |         6 | 1/6/8/   |
|  9 | AEEB |         8 | 1/6/8/9/ |

Demo

效果

我使用以下脚本使用MariaDBsequence plugin上创建了100K行虚假数据:

drop table if exists tree;
CREATE TABLE tree (
  `id` int primary key,
  `CELL` varchar(50),
  `parent_id` int,
  `path` varchar(255),
  unique index (`CELL`),
  unique index (`path`)
);

DROP TRIGGER IF EXISTS `tree_after_insert`;
DELIMITER //
CREATE TRIGGER `tree_after_insert` BEFORE INSERT ON `tree` FOR EACH ROW BEGIN
    if new.id = 1 then
        set new.path := '1/';
    else    
        set new.path := concat((
            select path from tree where id = new.parent_id
        ), new.id, '/');
    end if;
END//
DELIMITER ;

insert into tree
    select seq as id
        , conv(seq, 10, 36) as CELL
        , case 
            when seq = 1 then null
            else floor(rand(1) * (seq-1)) + 1 
        end as parent_id
        , null as path
    from seq_1_to_100000
;
DROP TRIGGER IF EXISTS `tree_after_insert`;
-- runtime ~ 4 sec.

测试

计算根目录下的所有元素:

SELECT count(*)
FROM tree t
CROSS JOIN tree r -- root
WHERE r.CELL = '1'
  AND t.path LIKE CONCAT(r.path, '%');
-- result: 100000
-- runtime: ~ 30 ms

获取特定节点下的子树元素:

SELECT t.*
FROM tree t
CROSS JOIN tree r -- root
WHERE r.CELL = '3B0'
  AND t.path LIKE CONCAT(r.path, '%');
-- runtime: ~ 30 ms

结果:

| id    | CELL | parent_id | path                                |
|=======|======|===========|=====================================|
|  4284 | 3B0  |       614 | 1/4/11/14/614/4284/                 |
|  6560 | 528  |      4284 | 1/4/11/14/614/4284/6560/            |
|  8054 | 67Q  |      6560 | 1/4/11/14/614/4284/6560/8054/       |
| 14358 | B2U  |      6560 | 1/4/11/14/614/4284/6560/14358/      |
| 51911 | 141Z |      4284 | 1/4/11/14/614/4284/51911/           |
| 55695 | 16Z3 |      4284 | 1/4/11/14/614/4284/55695/           |
| 80172 | 1PV0 |      8054 | 1/4/11/14/614/4284/6560/8054/80172/ |
| 87101 | 1V7H |     51911 | 1/4/11/14/614/4284/51911/87101/     |

的PostgreSQL

这也适用于PostgreSQL。只需要更改字符串连接语法:

SELECT t.*
FROM tree t
CROSS JOIN tree r -- root
WHERE r.CELL = 'AE'
  AND t.path LIKE r.path || '%';

演示: sqlfiddle - rextester

搜索工作如何

如果查看测试示例,您会发现结果中的所有路径均以1/4/11/14/614/4284 /'开头。这是具有CELL='3B0'的子树根的路径。如果path列已编制索引,则引擎将全部有效地找到它们,因为索引按path排序。就像你想找到以' pol'开头的所有单词一样。在一个有100K字的字典中。你不需要阅读整本字典。

答案 1 :(得分:4)

性能

正如其他人已经提到的,只要您使用合适的索引主键并确保关系使用外键,性能就不应该成为问题。通常,RDBMS经过高度优化,可以有效地对索引列执行连接,参照完整性也可以提供防止孤立的优势。 100,000可能听起来很多行,但只要表结构和查询设计得很好,就不会扩展RDBMS。

RDBMS的选择

回答这个问题的一个因素在于选择一个能够通过公用表表达式(CTE)执行递归查询的数据库,如果有查询没有查询,这对于保持查询紧凑或必不可少非常有用。限制被遍历的后代数。

既然你已经表明你可以自由选择RDBMS但它必须在Linux下运行,那么我将把PostgreSQL作为一个建议扔出去,因为它具有这个功能并且是免费提供的。 (这个选择当然是非常主观的,每个都有优点和缺点,但是我想要排除的其他一些竞争者是自it doesn't currently support CTEs之后的MySQL,自it doesn't currently support *recursive* CTEs后的MariaDB,SQL自it doesn't currently support Linux以来的服务器.Oracle等其他可能性可能依赖于预算/现有资源。)

SQL

以下是您编写的SQL的示例,用于执行查找“A”的所有后代的第一个示例:

WITH RECURSIVE rcte AS (
   SELECT id, letters
   FROM cell 
   WHERE letters = 'A'
   UNION ALL
   SELECT c.id, c.letters
   FROM cell c
   INNER JOIN rcte r
   ON c.parent_cell_id = r.id
)
SELECT letters
FROM rcte
ORDER BY letters;

<强>解释

上面的SQL设置了一个“公用表表达式”,即只要引用其别名(在本例中为SELECT)就运行rcte。发生递归是因为它本身就是引用的。 UNION的第一部分选择层次结构顶部的单元格。它的后代都是通过在UNION的第二部分继续加入儿童而找到的,直到找不到进一步的记录。

演示

以上查询可以在此处查看示例数据:http://rextester.com/HVY63888

答案 2 :(得分:1)

你绝对可以做到(如果我已经正确地阅读了你的问题)。

根据您的RDBMS,您可能需要选择不同的方式。

拥有父母的基本结构是正确的。

SQL Server使用递归公用表表达式(CTE)来锚定开始和向下工作

https://technet.microsoft.com/en-us/library/ms186243(v=sql.105).aspx

编辑:对于Linux,在PostgreSQL中使用相同的https://www.postgresql.org/docs/current/static/queries-with.html

Oracle采用了不同的方法,但我认为您也可以使用CTE。

https://oracle-base.com/articles/misc/hierarchical-queries

对于100k行,我不会想象性能会成为一个问题,尽管我仍然将PK&amp; FK,因为这是正确的事情。如果您真的关心速度,那么将其读入内存并构建链表的哈希表可能会有效。

优点&amp;缺点 - 它几乎归结为您的RDBMS的可读性和适用性。

这是一个已经解决的问题(再次,假设我没有错过任何东西),所以你会没事的。

答案 3 :(得分:1)

我有两个字给你...... “RANGE KEYS”

您可能会发现这种技术非常强大且灵活。您将能够轻松地导航层次结构,并支持变量深度聚合,而无需递归。

在下面的演示中,我们将通过递归CTE构建层次结构。对于更大的层次结构150K +,我愿意在需要的时候分享更快的构建。

由于您的层次结构移动缓慢(如我的),我倾向于将它们存储在规范化结构中并根据需要重建。

一些实际代码怎么样?

Declare @YourTable table (ID varchar(25),Pt varchar(25))
Insert into @YourTable values 
('A'   ,NULL),
('AA'  ,'A'),
('AAA' ,'AA'),
('AAC' ,'AA'),
('AB'  ,'A'),
('AE'  ,'A'),
('AEA' ,'AE'),
('AEE' ,'AE'),
('AEEB','AEE')


Declare @Top  varchar(25) = null     --<<  Sets top of Hier Try 'AEE'
Declare @Nest varchar(25) ='|-----'  --<<  Optional: Added for readability

IF OBJECT_ID('TestHier') IS NOT NULL 
Begin
    Drop Table TestHier
End

;with cteHB as (
      Select Seq  = cast(1000+Row_Number() over (Order by ID) as varchar(500))
            ,ID
            ,Pt
            ,Lvl=1
            ,Title = ID
      From   @YourTable 
      Where  IsNull(@Top,'TOP') = case when @Top is null then isnull(Pt,'TOP') else ID end
      Union  All
      Select cast(concat(cteHB.Seq,'.',1000+Row_Number() over (Order by cteCD.ID)) as varchar(500))
            ,cteCD.ID
            ,cteCD.Pt
            ,cteHB.Lvl+1
            ,cteCD.ID
      From   @YourTable cteCD 
      Join   cteHB on cteCD.Pt = cteHB.ID)
     ,cteR1 as (Select Seq,ID,R1=Row_Number() over (Order By Seq) From cteHB)
     ,cteR2 as (Select A.Seq,A.ID,R2=Max(B.R1) From cteR1 A Join cteR1 B on (B.Seq like A.Seq+'%') Group By A.Seq,A.ID )
Select B.R1  
      ,C.R2
      ,A.ID
      ,A.Pt
      ,A.Lvl
      ,Title = Replicate(@Nest,A.Lvl-1) + A.Title
 Into dbo.TestHier
 From cteHB A
 Join cteR1 B on A.ID=B.ID
 Join cteR2 C on A.ID=C.ID
 Order By B.R1

显示整个Hier 我添加了标题和嵌套以提高可读性

Select * from TestHier Order By R1

enter image description here

只是说明显而易见,范围键是R1和R2。您可能还注意到R1维护了演示序列。叶节点是R1 = R2,父节点或汇总定义所有权范围。

显示所有后代

Declare @GetChildrenOf varchar(25) = 'AE'
Select A.*
  From TestHier A
  Join TestHier B on B.ID=@GetChildrenOf and A.R1 Between B.R1 and B.R2
  Order By R1

enter image description here

显示路径

Declare @GetParentsOf varchar(25) = 'AEEB'
Select A.*
  From TestHier A
  Join TestHier B on B.ID=@GetParentsOf and B.R1 Between A.R1 and A.R2
  Order By R1

enter image description here

显然,这些都是相当简单的插图。随着时间的推移,我创建了一系列辅助函数,包括标量函数和表值函数。我还应该声明,你不应该在你的工作中硬编码范围键,因为它们会改变。

摘要

如果你有一个点(甚至是一系列点),你将拥有它的范围,因此你会立即知道它所在的位置以及它的内容。

答案 4 :(得分:0)

对于您的方案,我建议您使用Nested Sets Approach in PostgreSQL。它是基于XML标签的使用Relational数据库进行查询。

<强>性能

如果您对 lft rgt 列进行索引,那么您不需要递归查询来获取数据。即使数据看起来很大,检索也会非常快。

<强>示例

/*1A:
2  AA:
3    AAA
4    AAC
5  AB
6  AE:
7   AEA
8   AEE:
9     AEEB
10B:
*/

CREATE TABLE tree(id int, CELL varchar(4), lft int, rgt int);

INSERT INTO tree
    ("id", CELL, "lft", "rgt")
VALUES
    (1, 'A', 1, 9),
    (2, 'AA', 2, 4),
    (3, 'AAA', 3, 3),
    (4, 'AAC', 4, 4),
    (5, 'AB', 5, 5),
    (6, 'AE', 6, 9),
    (7, 'AEA', 7, 7),
    (8, 'AEE', 8, 8),
    (9, 'AEEB', 9, 9)
;


SELECT  hc.*
FROM    tree hp
JOIN    tree hc
ON      hc.lft BETWEEN hp.lft AND hp.rgt
WHERE   hp.id = 2

<强>演示

Querying using Nested Sets approach

答案 5 :(得分:0)

此方法不依赖于路径或父列的存在。它是关系而不是递归的。

由于该表是静态的,因此创建一个仅包含叶子的materialized view以便更快地进行搜索:

create materialized view leave as
select cell
from (
    select cell,
        lag(cell,1,cell) over (order by cell desc) not like cell || '%' as leave
    from t
) s
where leave;

table leave;
 cell 
------
 CCCE
 CCCA
 CCBE
 CCBC
 BEDA
 BDDA
 BDCE
 BDCB
 BAA
 AEEB
 AEA
 AB
 AAC
 AAA

物化视图在创建时计算一次,而不是像普通视图那样在每个查询中计算。创建索引以加快速度:

create index cell_index on leave(cell);

如果最终更改了源表,只需刷新视图:

refresh materialized view leave;

搜索功能接收文本并返回文本数组:

create or replace function get_descendants(c text)
returns text[] as $$
    select array_agg(distinct l order by l)
    from (
        select left(cell, generate_series(length(c), length(cell))) as l
        from leave
        where cell like c || '%'
    ) s;
$$ language sql immutable strict;

将所需的匹配传递给函数:

select get_descendants('A');
          get_descendants          
-----------------------------------
 {A,AA,AAA,AAC,AB,AE,AEA,AEE,AEEB}

select get_descendants('AEE');
 get_descendants 
-----------------
 {AEE,AEEB}

测试数据:

create table t (cell text);
insert into t (cell) values
('A'),
('AA'),
('AAA'),
('AAC'),
('AB'),
('AE'),
('AEA'),
('AEE'),
('AEEB'),
('B'),
('BA'),
('BAA'),
('BD'),
('BDC'),
('BDCB'),
('BDCE'),
('BDD'),
('BDDA'),
('BE'),
('BED'),
('BEDA'),
('C'),
('CC'),
('CCB'),
('CCBC'),
('CCBE'),
('CCC'),
('CCCA'),
('CCCE'),
('CE');