inviqa-com-kztbkj.txt (26014B)
1 [1] Skip to main content 2 Search [2][ ] 3 [3][Search] 4 [4] Home 5 Main navigation Menu 6 7 • [6]Who we are 8 □ [7]About Inviqa 9 □ [8]About Havas 10 □ [9]Our Sustainability Journey 11 • [10]What we do 12 □ [11]Digital Strategy Consulting 13 □ [12]Digital Roadmap Development 14 □ [13]Digital Product Design 15 □ [14]User Research 16 □ [15]Usability Testing 17 □ [16]Technical Architecture Consulting & Development 18 □ [17]Digital Platform Implementation 19 □ [18]Experience Optimisation 20 □ [19]All services 21 • [20]Case studies 22 □ [21]B2B case studies 23 □ [22]Fashion & Luxury case studies 24 □ [23]Not-For-Profit case studies 25 □ [24]Retail & DTC case studies 26 □ [25]Sport, Leisure & Entertainment case studies 27 □ [26]Travel & Hotels case studies 28 □ [27]All case studies 29 • [28]Partners 30 □ [29]Akeneo 31 □ [30]BigCommerce 32 □ [31]Drupal 33 □ [32]Magento / Adobe Commerce 34 □ [33]Spryker 35 □ [34]All partners 36 • [35]Careers 37 □ [36]Life at Inviqa 38 □ [37]Current Vacancies 39 • [38]Insights 40 □ [39]DTC Ecommerce Report 2023 41 □ [40]PIM Readiness Framework 42 □ [41]Retail Optimisation Whitepaper 43 □ [42]Blog 44 □ [43]All insights 45 • [44]Contact 46 □ [45]Get in Touch 47 48 • [46] EN 49 • [47] DE 50 51 Storing graphs in the database: SQL meets social network 52 53 By Lorenzo Alberton 54 7 September 2009 [48]Technology engineering 55 56 Graphs are ubiquitous. Social or P2P networks, thesauri, route planning 57 systems, recommendation systems, collaborative filtering, even the World Wide 58 Web itself is ultimately a graph! 59 60 Given their importance, it's surely worth spending some time in studying some 61 algorithms and models to represent and work with them effectively. In this 62 short article, we're going to see how we can store a graph in a DBMS. Given how 63 much attention my talk about storing a tree data structure in the db received, 64 it's probably going to be interesting to many. Unfortunately, the Tree models/ 65 techniques do not apply to generic graphs, so let's discover how we can deal 66 with them. 67 68 What's a graph 69 70 A graph is a set of nodes (vertices) interconnected by links (edges). When the 71 edges have no orientation, the graph is called an undirected graph. In 72 contrast, a graph where the edges have a specific orientation from a node to 73 another is called directed: 74 75 " " 76 77 A graph is called complete when there's an edge between any two nodes, dense 78 when the number of edges is close to the maximal number of edges, and sparse 79 when it has only a few edges: 80 81 " " 82 83 Representing a graph 84 85 Two main data structures for the representation of graphs are used in practice. 86 The first is called an adjacency list, and is implemented as an array with one 87 linked list for each source node, containing the destination nodes of the edges 88 that leave each node. The second is a two-dimensional boolean adjacency matrix, 89 in which the rows and columns are the source and destination vertices, and 90 entries in the array indicate whether an edge exists between the vertices. 91 Adjacency lists are preferred for sparse graphs; otherwise, an adjacency matrix 92 is a good choice. [1] 93 94 " "" " 95 96 When dealing with databases, most of the times the adjacency matrix is not a 97 viable option, for two reasons: there is a hard limit in the number of columns 98 that a table can have, and adding or removing a node requires a DDL statement. 99 100 Joe Celko dedicates a short chapter to graphs in his '[49]SQL for Smarties' 101 book, but the topic is treated in a quite hasty way, which is surprising given 102 his usual high standards. 103 104 One of the basic rules of a successful representation is to separate the nodes 105 and the edges, to avoid [50]DKNF problems. Thus, we create two tables: 106 107 CREATE TABLE nodes ( 108 id INTEGER PRIMARY KEY, 109 name VARCHAR(10) NOT NULL, 110 feat1 CHAR(1), -- e.g., age 111 feat2 CHAR(1) -- e.g., school attended or company 112 ); 113 114 CREATE TABLE edges ( 115 a INTEGER NOT NULL REFERENCES nodes(id) ON UPDATE CASCADE ON DELETE CASCADE, 116 b INTEGER NOT NULL REFERENCES nodes(id) ON UPDATE CASCADE ON DELETE CASCADE, 117 PRIMARY KEY (a, b) 118 ); 119 120 CREATE INDEX a_idx ON edges (a); 121 CREATE INDEX b_idx ON edges (b); 122 123 The first table (nodes) contains the actual node payload, with all the 124 interesting information we need to store about a node (in the example, feat1 125 and feat2 represent two node features, like the age of the person, or the 126 location, etc.). 127 128 If we want to represent an undirected graph, we need to add a CHECK constraint 129 on the uniqueness of the pair. 130 131 Since the SQL standard does not allow a subquery in the CHECK constraint, we 132 first create a function and then we use it in the CHECK constraint (this 133 example is for PostgreSQL, but can be easily ported to other DBMS): 134 135 CREATE FUNCTION check_unique_pair(IN id1 INTEGER, IN id2 INTEGER) RETURNS INTEGER AS $body$ 136 DECLARE retval INTEGER DEFAULT 0; 137 BEGIN 138 SELECT COUNT(*) INTO retval FROM ( 139 SELECT * FROM edges WHERE a = id1 AND b = id2 140 UNION ALL 141 SELECT * FROM edges WHERE a = id2 AND b = id1 142 ) AS pairs; 143 RETURN retval; 144 END 145 $body$ 146 LANGUAGE 'plpgsql'; 147 148 ALTER TABLE edges ADD CONSTRAINT unique_pair CHECK (check_unique_pair(a, b) < 1); 149 150 NB: a UDF in a CHECK constraint might be a bit slow [4]. An alternative is to 151 have a materialized view [5] or force an order in the node pair (i.e. "CHECK (a 152 < b)", and then using a stored procedure to insert the nodes in the correct 153 order). 154 155 If we also want to prevent self-loops (i.e. a node linking to itself), we can 156 add another CHECK constraint: 157 158 ALTER TABLE edges ADD CONSTRAINT no_self_loop CHECK (a <> b) 159 160 " "" " 161 162 Traversing the graph 163 164 Now that we know how to store the graph, we might want to know which nodes are 165 connected. Listing the directly connected nodes is very simple: 166 167 SELECT * 168 FROM nodes n 169 LEFT JOIN edges e ON n.id = e.b 170 WHERE e.a = 1; -- retrieve nodes connected to node 1 171 172 or, in the case of undirected edges: 173 174 SELECT * FROM nodes WHERE id IN ( 175 SELECT a FROM edges WHERE b = 1 176 UNION 177 SELECT b FROM edges WHERE a = 1 178 ); 179 180 -- or alternatively: 181 182 SELECT * FROM nodes where id IN ( 183 SELECT CASE WHEN a = 1 THEN b ELSE a END 184 FROM edges 185 WHERE 1 IN (a, b) 186 ); 187 188 Traversing the full graph usually requires more than a query: we can either 189 loop through the connected nodes, one level a time, or we can create a 190 temporary table holding all the possible paths between two nodes. 191 192 We could use Oracle’s CONNECT BY syntax or SQL standard’s Common Table 193 Expressions (CTEs) to recurse through the nodes, but since the graph can 194 contain loops, we’d get errors (unless we’re very careful, as we’ll see in a 195 moment). 196 197 Kendall Willets [2] proposes a way of traversing (BFS) the graph using a 198 temporary table. It is quite robust, since it doesn’t fail on graphs with 199 cycles (and when dealing with trees, he shows there are better algorithms 200 available). His solution is just one of the many available, but quite good. 201 202 The problem with temporary tables holding all the possible paths is it has to 203 be maintained. Depending on how frequently the data is accessed and updated it 204 might still be worth it, but it’s quite expensive. If you do resort to such a 205 solution, these references may be of use [13] [14]. 206 207 Before going further in our analysis, we need to introduce a new concept: the 208 transitive closure of a graph. 209 210 Transitive closure 211 212 The transitive closure of a graph G = (V,E) is a graph G* = (V,E*) such that E* 213 contains an edge (u,v) if and only if G contains a path from u to v. 214 215 In other words, the transitive closure of a graph is a graph which contains an 216 edge (u,v) whenever there is a directed path from u to v. 217 218 " " 219 220 Graph: transitive closure 221 222 As already mentioned, SQL has historically been unable [3] to express recursive 223 functions needed to maintain the transitive closure of a graph without an 224 auxiliary table. There are many solutions to solve this problem with a 225 temporary table (some even elegant [2]), but I still haven't found one to do it 226 dynamically. 227 228 Here's my clumsy attempt at a possible solution using CTEs 229 230 First, this is how we can write the WITH RECURSIVE statement for a Directed 231 (Cyclic) Graph: 232 233 WITH RECURSIVE transitive_closure(a, b, distance, path_string) AS 234 ( SELECT a, b, 1 AS distance, 235 a || '.' || b || '.' AS path_string 236 FROM edges 237 238 UNION ALL 239 240 SELECT tc.a, e.b, tc.distance + 1, 241 tc.path_string || e.b || '.' AS path_string 242 FROM edges AS e 243 JOIN transitive_closure AS tc 244 ON e.a = tc.b 245 WHERE tc.path_string NOT LIKE '%' || e.b || '.%' 246 ) 247 SELECT * FROM transitive_closure 248 ORDER BY a, b, distance; 249 250 Notice the WHERE condition, which stops the recursion in the presence of loops. 251 This is very important to avoid errors. 252 253 Sample output: 254 255 " " 256 257 This is a slightly modified version of the same query to deal with Undirected 258 graphs (NB: this is probably going to be rather slow if done in real time): 259 260 -- DROP VIEW edges2; 261 CREATE VIEW edges2 (a, b) AS ( 262 SELECT a, b FROM edges 263 UNION ALL 264 SELECT b, a FROM edges 265 ); 266 267 WITH RECURSIVE transitive_closure(a, b, distance, path_string) AS 268 ( SELECT a, b, 1 AS distance, 269 a || '.' || b || '.' AS path_string 270 FROM edges2 271 272 UNION ALL 273 274 SELECT tc.a, e.b, tc.distance + 1, 275 tc.path_string || e.b || '.' AS path_string 276 FROM edges2 AS e 277 JOIN transitive_closure AS tc ON e.a = tc.b 278 WHERE tc.path_string NOT LIKE '%' || e.b || '.%' 279 ) 280 SELECT * FROM transitive_closure 281 ORDER BY a, b, distance; 282 283 Linkedin: Degrees of separation 284 285 One of the fundamental characteristics of networks (or graphs in general) is 286 connectivity. We might want to know how to go from A to B, or how two people 287 are connected, and we also want to know how many "hops" separate two nodes, to 288 have an idea about the distance. 289 290 For instance, social networks like LinkedIN show our connections or search 291 results sorted by degree of separation, and trip planning sites show how many 292 flights you have to take to reach your destination, usually listing direct 293 connections first. 294 295 There are some database extensions or hybrid solutions like SPARQL on Virtuoso 296 [11] that add a TRANSITIVE clause [12] to make this kind of queries both easy 297 and efficient, but we want to see how to reach the same goal with standard SQL. 298 299 As you might guess, this becomes really easy once you have the transitive 300 closure of the graph, we only have to add a WHERE clause specifying what our 301 source and destination nodes are: 302 303 WITH RECURSIVE transitive_closure(a, b, distance, path_string) AS 304 ( SELECT a, b, 1 AS distance, 305 a || '.' || b || '.' AS path_string 306 FROM edges 307 WHERE a = 1 -- source 308 309 UNION ALL 310 311 SELECT tc.a, e.b, tc.distance + 1, 312 tc.path_string || e.b || '.' AS path_string 313 FROM edges AS e 314 JOIN transitive_closure AS tc ON e.a = tc.b 315 WHERE tc.path_string NOT LIKE '%' || e.b || '.%' 316 ) 317 SELECT * FROM transitive_closure 318 WHERE b=6 -- destination 319 ORDER BY a, b, distance; 320 321 " " 322 323 If we're showing the trip planning results, then we have a list of all possible 324 travel solutions; instead of sorting by distance, we might sort by price or 325 other parameters with little changes. 326 327 If we're showing how two people are connected (LinkedIN), then we can limit the 328 result set to the first row, since we're probably interested in showing the 329 shortest distance only and not all the other alternatives. 330 331 Instead of adding a LIMIT clause, it's probably more efficient to add "AND 332 tc.distance = 0" to the WHERE clause of the recursive part of the CTE, or a 333 GROUP BY clause as follows: 334 335 WITH RECURSIVE transitive_closure(a, b, distance, path_string) 336 AS 337 ( SELECT a, b, 1 AS distance, 338 a || '.' || b || '.' AS path_string 339 FROM edges2 340 341 UNION ALL 342 343 SELECT tc.a, e.b, tc.distance + 1, 344 tc.path_string || e.b || '.' AS path_string 345 FROM edges2 AS e 346 JOIN transitive_closure AS tc ON e.a = tc.b 347 WHERE tc.path_string NOT LIKE '%' || e.b || '.%' 348 ) 349 SELECT a, b, min(distance) AS dist FROM transitive_closure 350 --WHERE a = 1 AND b=6 351 GROUP BY a, b 352 ORDER BY a, dist, b; 353 354 " " 355 356 If you are interested in the immediate connections of a certain node, then 357 specify the starting node and a distance equals to one (by limiting the 358 recursion at the first level) 359 360 WITH RECURSIVE transitive_closure(a, b, distance, path_string) AS 361 ( SELECT a, b, 1 AS distance, a || '.' || b || '.' AS path_string 362 FROM edges2 363 WHERE a = 1 -- set the starting node 364 365 UNION ALL 366 367 SELECT tc.a, e.b, tc.distance + 1, 368 tc.path_string || e.b || '.' AS path_string 369 FROM edges2 AS e 370 JOIN transitive_closure AS tc ON e.a = tc.b 371 WHERE tc.path_string NOT LIKE '%' || e.b || '.%' 372 AND tc.distance = 0 -- limit recursion at the first level 373 ) 374 SELECT b FROM transitive_closure; 375 376 Of course to get the immediate connections there's no need for a recursive 377 query (just use the one presented at the previous paragraph), but I thought I'd 378 show it anyway as a first step towards more complex queries. 379 380 LinkedIN has a nice feature to show "How this user is connected to you" for non 381 directly connected nodes. 382 383 If the distance between the two nodes is equal to 2, you can show the shared 384 connections: 385 386 SELECT b FROM ( 387 388 WITH RECURSIVE transitive_closure(a, b, distance, path_string) AS 389 ( SELECT a, b, 1 AS distance, a || '.' || b || '.' AS path_string 390 FROM edges2 391 WHERE a = 1 -- set the starting node 392 393 UNION ALL 394 395 SELECT tc.a, e.b, tc.distance + 1, 396 tc.path_string || e.b || '.' AS path_string 397 FROM edges2 AS e 398 JOIN transitive_closure AS tc ON e.a = tc.b 399 WHERE tc.path_string NOT LIKE '%' || e.b || '.%' 400 AND tc.distance = 0 401 ) 402 SELECT b FROM transitive_closure 403 404 UNION ALL 405 406 (WITH RECURSIVE transitive_closure(a, b, distance, path_string) AS 407 ( SELECT a, b, 1 AS distance, a || '.' || b || '.' AS path_string 408 FROM edges2 409 WHERE a = 4 -- set the target node 410 411 UNION ALL 412 413 SELECT tc.a, e.b, tc.distance + 1, 414 tc.path_string || e.b || '.' AS path_string 415 FROM edges2 AS e 416 JOIN transitive_closure AS tc ON e.a = tc.b 417 WHERE tc.path_string NOT LIKE '%' || e.b || '.%' 418 AND tc.distance = 0 419 ) 420 SELECT b FROM transitive_closure 421 )) AS immediate_connections 422 GROUP BY b 423 HAVING COUNT(b) > 1; 424 425 In the above query, we select the immediate connections of the two nodes 426 separately, and then select the shared ones. 427 428 For nodes having a distance equals to 3, the approach is slightly different. 429 430 First, you check that the two nodes are indeed at a minimum distance of 3 nodes 431 (you're probably not interested in showing the relationship between two nodes 432 when the distance is bigger): 433 434 WITH RECURSIVE transitive_closure(a, b, distance, path_string) AS 435 ( SELECT a, b, 1 AS distance, 436 a || '.' || b || '.' AS path_string 437 FROM edges2 438 WHERE a = 1 -- set the starting node 439 440 UNION ALL 441 442 SELECT tc.a, e.b, tc.distance + 1, 443 tc.path_string || e.b || '.' AS path_string 444 FROM edges2 AS e 445 JOIN transitive_closure AS tc ON e.a = tc.b 446 WHERE tc.path_string NOT LIKE '%' || e.b || '.%' 447 AND tc.distance < 3 -- stop the recursion after 3 levels 448 ) 449 SELECT a, b, min(distance) FROM transitive_closure 450 WHERE b=4 -- set the target node 451 GROUP BY a, b 452 HAVING min(distance) = 3; --set the minimum distance 453 454 Then you select the paths between those nodes. 455 456 But there's a different approach which is more generic and efficient, and can 457 be used for all the nodes whose distance is bigger than 2. 458 459 The idea is to select the immediate neighbours of the starting node that are 460 also in the path to the other node. 461 462 Depending on the distance, you can have either the shared nodes (distance = 2), 463 or the connections that could lead to the other node (distance > 2). In the 464 latter case, you could for instance show how A is connected to B: 465 466 " " 467 468 Linkedin 469 470 WITH RECURSIVE transitive_closure(a, b, distance, path_string) AS 471 ( SELECT a, b, 1 AS distance, 472 a || '.' || b || '.' AS path_string, 473 b AS direct_connection 474 FROM edges2 475 WHERE a = 1 -- set the starting node 476 477 UNION ALL 478 479 SELECT tc.a, e.b, tc.distance + 1, 480 tc.path_string || e.b || '.' AS path_string, 481 tc.direct_connection 482 FROM edges2 AS e 483 JOIN transitive_closure AS tc ON e.a = tc.b 484 WHERE tc.path_string NOT LIKE '%' || e.b || '.%' 485 AND tc.distance < 3 486 ) 487 SELECT * FROM transitive_closure 488 --WHERE b=3 -- set the target node 489 ORDER BY a,b,distance 490 491 " " 492 493 Facebook: You might also know 494 495 A similar but slightly different requirement is to find those nodes that are 496 most strongly related, but not directly connected yet. In other words, it's 497 interesting to find out which and how many connected nodes are shared between 498 any two nodes, i.e. how many 'friends' are shared between two individuals. Or 499 better yet, to find those nodes sharing a certain (minimum) number of nodes 500 with the current one. 501 502 This could be useful to suggest a new possible friend, or in the case of 503 recommendation systems, to suggest a new item/genre that matches the user's 504 interests. 505 506 There are many ways of doing this. In theory, this is bordering on the domain 507 of collaborative filtering [6][7][8], so using Pearson's correlation [9] or a 508 similar distance measure with an appropriate algorithm [10] is going to 509 generate the best results. Collaborative filtering is an incredibly interesting 510 topic on its own, but outside the scope of this article. 511 512 A rough and inexpensive alternative is to find the nodes having distance equals 513 to 2, and filter those that either have a common characteristic with the source 514 node (went to the same school / worked at the same company, belong to the same 515 interest group / are items of the same genre) or have several mutual 'friends'. 516 517 " " 518 519 Facebook 520 521 This, again, is easily done once you have the transitive closure of the graph: 522 523 SELECT a AS you, 524 b AS mightknow, 525 shared_connection, 526 CASE 527 WHEN (n1.feat1 = n2.feat1 AND n1.feat1 = n3.feat1) THEN 'feat1 in common' 528 WHEN (n1.feat2 = n2.feat2 AND n1.feat2 = n3.feat2) THEN 'feat2 in common' 529 ELSE 'nothing in common' 530 END AS reason 531 FROM ( 532 WITH RECURSIVE transitive_closure(a, b, distance, path_string) AS 533 ( SELECT a, b, 1 AS distance, 534 a || '.' || b || '.' AS path_string, 535 b AS direct_connection 536 FROM edges2 537 WHERE a = 1 -- set the starting node 538 539 UNION ALL 540 541 SELECT tc.a, e.b, tc.distance + 1, 542 tc.path_string || e.b || '.' AS path_string, 543 tc.direct_connection 544 FROM edges2 AS e 545 JOIN transitive_closure AS tc ON e.a = tc.b 546 WHERE tc.path_string NOT LIKE '%' || e.b || '.%' 547 AND tc.distance < 2 548 ) 549 SELECT a, 550 b, 551 direct_connection AS shared_connection 552 FROM transitive_closure 553 WHERE distance = 2 554 ) AS youmightknow 555 LEFT JOIN nodes AS n1 ON youmightknow.a = n1.id 556 LEFT JOIN nodes AS n2 ON youmightknow.b = n2.id 557 LEFT JOIN nodes AS n3 ON youmightknow.shared_connection = n3.id 558 WHERE (n1.feat1 = n2.feat1 AND n1.feat1 = n3.feat1) 559 OR (n1.feat2 = n2.feat2 AND n1.feat2 = n3.feat2); 560 561 " " 562 563 Once you have selected these nodes, you can filter those recurring more often, 564 or give more importance to those having a certain feature in common, or pick 565 one randomly (so you don't end up suggesting the same node over and over). 566 567 Conclusion 568 569 In this article I had some fun with the new and powerful CTEs, and showed some 570 practical examples where they can be useful. I also showed some approaches at 571 solving the challenges faced by any social network or recommendation system. 572 573 You are advised that depending on the size of the graph and the performance 574 requirements of your application, the above queries might be too slow to run in 575 realtime. Caching is your friend. 576 577 Update: Many of the queries in this article have been revised, so please refer 578 to [51]http://www.slideshare.net/quipo/rdbms-in-the-social-networks-age for 579 changes. 580 581 References 582 583 [1] [52]http://willets.org/sqlgraphs.html 584 585 [2] [53]http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.48.53 586 587 [3] [54]http://sqlblog.com/blogs/alexander_kuznetsov/archive/2009/06/25/ 588 scalar-udfs-wrapped-in-check-constraints-are-very-slow-and-may-fail-for-multirow-updates.aspx 589 590 [4] [55]http://www.dbazine.com/oracle/or-articles/tropashko8 591 592 [5] [56]http://en.wikipedia.org/wiki/Collaborative_filtering 593 594 [6] [57]http://en.wikipedia.org/wiki/Slope_One 595 596 [7] blog.charliezhu.com/2008/07/21/implementing-slope-one-in-t-sql/ 597 598 [8] bakara.eng.tau.ac.il/~semcomm/slides7/grouplensAlgs-Kahn.pps 599 600 [9] [58]http://www.slideshare.net/denisparra/ 601 evaluation-of-collaborative-filtering-algorithms-for-recommending-articles-on-citeulike 602 603 [10] [59]http://virtuoso.openlinksw.com/ 604 605 [11] [60]http://www.openlinksw.com/weblog/oerling/?id=1433 606 607 [12] [61]http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.48.53 608 609 [13] [62]http://en.wikipedia.org/wiki/Transitive_reduction 610 611 You might also like... 612 613 [63] 614 A woman reviews code on her laptop 615 616 617 Headless commerce: everything you need to know 618 619 What the heck is headless? Discover the what, why, and when of headless 620 architectures with our guide to headless commerce. 621 622 [64] 623 Drupal consulting and web development at Inviqa 624 625 626 The Drupal 9 upgrade Config Split issue and how to fix it 627 628 In this article we look back at an issue we’ve encountered with Drupal Config 629 Split when upgrading Drupal 8 to 9 – and we share how to fix it, so you don’t 630 have to run into the same issue when upgrading to Drupal 9. 631 632 Inviqa, winner of Webby Awards The Webby Awards winner 633 Inviqa named one of Top 100 Agencies in Econsultancy Top 100 Digital Agencies 634 Inviqa UXUK Awards winner UXUK Awards winner 635 DADI Award winner of 'Best UX / Usability category' DADI Award winner 636 Footer Main Navigation 637 638 • [65]Home 639 • [66]Who we are 640 • [67]What we do 641 • [68]Case studies 642 • [69]Careers 643 • [70]Insights 644 • [71]Contact 645 • [72]Accessibility statement 646 647 About us 648 649 Together with your teams, we shape the digital products, teams, processes, and 650 software systems you need to meet diverse customer needs and accelerate your 651 business growth. 652 653 © 2007-2024, Inviqa UK Ltd. Registered No. 06278367. Registered Office: Havas 654 House, Hermitage Court, Hermitage Lane, Maidstone, ME16 9NT, UK. 655 656 Footer Legal Links 657 658 • [73]Covid-19 659 • [74]Privacy policy 660 • [75]Sitemap 661 662 663 References: 664 665 [1] https://inviqa.com/blog/storing-graphs-database-sql-meets-social-network#main-content 666 [4] https://inviqa.com/ 667 [6] https://inviqa.com/who-we-are 668 [7] https://inviqa.com/who-we-are 669 [8] https://www.havas.com/ 670 [9] https://inviqa.com/digital-sustainability-journey 671 [10] https://inviqa.com/what-we-do 672 [11] https://inviqa.com/what-we-do/digital-strategy-consulting-and-development 673 [12] https://inviqa.com/what-we-do/digital-roadmap-development 674 [13] https://inviqa.com/what-we-do/digital-product-design 675 [14] https://inviqa.com/what-we-do/user-research 676 [15] https://inviqa.com/what-we-do/usability-testing 677 [16] https://inviqa.com/what-we-do/technical-architecture-consulting-and-development 678 [17] https://inviqa.com/what-we-do/digital-platform-consulting-and-implementation 679 [18] https://inviqa.com/what-we-do/experience-optimisation 680 [19] https://inviqa.com/what-we-do 681 [20] https://inviqa.com/case-studies 682 [21] https://inviqa.com/case-studies?category=b2b 683 [22] https://inviqa.com/case-studies#fashion 684 [23] https://inviqa.com/case-studies#charity 685 [24] https://inviqa.com/case-studies?category=retail 686 [25] https://inviqa.com/case-studies#leisure 687 [26] https://inviqa.com/case-studies?category=travel 688 [27] https://inviqa.com/case-studies 689 [28] https://inviqa.com/partners 690 [29] https://inviqa.com/akeneo-pim-consulting-and-implementation 691 [30] https://inviqa.com/blog/bigcommerce-7-best-sites 692 [31] https://inviqa.com/drupal-consulting-and-web-development 693 [32] https://inviqa.com/magento-consulting-and-web-development 694 [33] https://inviqa.com/blog/spryker-commerce-platform-introduction 695 [34] https://inviqa.com/partners 696 [35] https://careers.inviqa.com/ 697 [36] https://careers.inviqa.com/ 698 [37] https://careers.inviqa.com/jobs 699 [38] https://inviqa.com/insights 700 [39] https://inviqa.com/insights/dtc-ecommerce-report-2023 701 [40] https://inviqa.com/insights/PIM-readiness-framework 702 [41] https://inviqa.com/insights/retail-optimisation-guide-2023 703 [42] https://inviqa.com/blog 704 [43] https://inviqa.com/insights 705 [44] https://inviqa.com/contact 706 [45] https://inviqa.com/contact 707 [46] https://inviqa.com/ 708 [47] https://inviqa.de/ 709 [48] https://inviqa.com/blog#Technology%20engineering 710 [49] https://www.amazon.com/Joe-Celkos-SQL-Smarties-Programming/dp/0123693799/157-5667933-6571053?ie=UTF8&redirect=true&tag=postcarfrommy-20 711 [50] https://en.wikipedia.org/wiki/Domain-key_normal_form 712 [51] http://www.slideshare.net/quipo/rdbms-in-the-social-networks-age 713 [52] http://willets.org/sqlgraphs.html 714 [53] http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.48.53 715 [54] http://sqlblog.com/blogs/alexander_kuznetsov/archive/2009/06/25/scalar-udfs-wrapped-in-check-constraints-are-very-slow-and-may-fail-for-multirow-updates.aspx 716 [55] http://www.dbazine.com/oracle/or-articles/tropashko8/ 717 [56] https://en.wikipedia.org/wiki/Collaborative_filtering 718 [57] https://en.wikipedia.org/wiki/Slope_One 719 [58] http://www.slideshare.net/denisparra/evaluation-of-collaborative-filtering-algorithms-for-recommending-articles-on-citeulike 720 [59] http://virtuoso.openlinksw.com/ 721 [60] http://www.openlinksw.com/weblog/oerling/?id=1433 722 [61] http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.48.53 723 [62] https://en.wikipedia.org/wiki/Transitive_reduction 724 [63] https://inviqa.com/blog/headless-commerce-everything-you-need-know 725 [64] https://inviqa.com/blog/drupal-9-upgrade-config-split-issue-and-how-fix-it 726 [65] https://inviqa.com/we-craft-game-changing-digital-experiences 727 [66] https://inviqa.com/who-we-are 728 [67] https://inviqa.com/what-we-do 729 [68] https://inviqa.com/case-studies 730 [69] https://careers.inviqa.com/ 731 [70] https://inviqa.com/insights 732 [71] https://inviqa.com/contact 733 [72] https://inviqa.com/accessibility-statement 734 [73] https://inviqa.com/covid-19-measures 735 [74] https://inviqa.com/privacy-policy-UK 736 [75] https://inviqa.com/sitemap