commit 6110b4223506901a3c024af77d7b5d0607b0463f
parent 527eed3b75a5a77c0c1cb46e83755af8d8e745fe
Author: David Eisinger <[email protected]>
Date: Wed, 3 Jan 2024 16:44:57 -0500
add friends ref
Diffstat:
2 files changed, 752 insertions(+), 0 deletions(-)
diff --git a/content/elsewhere/friends-undirected-graph-connections-in-rails/index.md b/content/elsewhere/friends-undirected-graph-connections-in-rails/index.md
@@ -5,6 +5,11 @@ draft: false
canonical_url: https://www.viget.com/articles/friends-undirected-graph-connections-in-rails/
featured: true
exclude_music: true
+references:
+- title: "Storing graphs in the database: SQL meets social network - Inviqa"
+ url: https://inviqa.com/blog/storing-graphs-database-sql-meets-social-network
+ date: 2024-01-03T21:44:24Z
+ file: inviqa-com-kztbkj.txt
---
No, sorry, not THOSE friends. But if you're interested in how to do
diff --git a/static/archive/inviqa-com-kztbkj.txt b/static/archive/inviqa-com-kztbkj.txt
@@ -0,0 +1,747 @@
+ #[1]alternate
+
+ IFRAME: [2]https://www.googletagmanager.com/ns.html?id=GTM-NBN52P
+
+ [3]Skip to main content
+
+ Search ______________________________
+ Search
+ [4]Home
+
+ Main navigation (BUTTON) Menu
+ * [5]Who we are
+ + [6]About Inviqa
+ + [7]About Havas
+ + [8]Our Sustainability Journey
+ * [9]What we do
+ + [10]Digital Strategy Consulting
+ + [11]Digital Roadmap Development
+ + [12]Digital Product Design
+ + [13]User Research
+ + [14]Usability Testing
+ + [15]Technical Architecture Consulting & Development
+ + [16]Digital Platform Implementation
+ + [17]Experience Optimisation
+ + [18]All services
+ * [19]Case studies
+ + [20]B2B case studies
+ + [21]Fashion & Luxury case studies
+ + [22]Not-For-Profit case studies
+ + [23]Retail & DTC case studies
+ + [24]Sport, Leisure & Entertainment case studies
+ + [25]Travel & Hotels case studies
+ + [26]All case studies
+ * [27]Partners
+ + [28]Akeneo
+ + [29]BigCommerce
+ + [30]Drupal
+ + [31]Magento / Adobe Commerce
+ + [32]Spryker
+ + [33]All partners
+ * [34]Careers
+ + [35]Life at Inviqa
+ + [36]Current Vacancies
+ * [37]Insights
+ + [38]DTC Ecommerce Report 2023
+ + [39]PIM Readiness Framework
+ + [40]Retail Optimisation Whitepaper
+ + [41]Blog
+ + [42]All insights
+ * [43]Contact
+ + [44]Get in Touch
+
+ * [45]EN
+ * [46]DE
+
+Storing graphs in the database: SQL meets social network
+
+ By Lorenzo Alberton
+ 7 September 2009 [47]Technology engineering
+
+ Graphs are ubiquitous. Social or P2P networks, thesauri, route planning
+ systems, recommendation systems, collaborative filtering, even the
+ World Wide Web itself is ultimately a graph!
+
+ Given their importance, it's surely worth spending some time in
+ studying some algorithms and models to represent and work with them
+ effectively. In this short article, we're going to see how we can store
+ a graph in a DBMS. Given how much attention my talk about storing a
+ tree data structure in the db received, it's probably going to be
+ interesting to many. Unfortunately, the Tree models/techniques do not
+ apply to generic graphs, so let's discover how we can deal with them.
+
+What's a graph
+
+ A graph is a set of nodes (vertices) interconnected by links (edges).
+ When the edges have no orientation, the graph is called an undirected
+ graph. In contrast, a graph where the edges have a specific orientation
+ from a node to another is called directed:
+ " "
+
+ A graph is called complete when there's an edge between any two
+ nodes, dense when the number of edges is close to the maximal number of
+ edges, and sparse when it has only a few edges:
+ " "
+
+Representing a graph
+
+ Two main data structures for the representation of graphs are used in
+ practice. The first is called an adjacency list, and is implemented as
+ an array with one linked list for each source node, containing the
+ destination nodes of the edges that leave each node. The second is a
+ two-dimensional boolean adjacency matrix, in which the rows and columns
+ are the source and destination vertices, and entries in the array
+ indicate whether an edge exists between the vertices. Adjacency lists
+ are preferred for sparse graphs; otherwise, an adjacency matrix is a
+ good choice. [1]
+ " " " "
+
+ When dealing with databases, most of the times the adjacency matrix is
+ not a viable option, for two reasons: there is a hard limit in the
+ number of columns that a table can have, and adding or removing a node
+ requires a DDL statement.
+
+ Joe Celko dedicates a short chapter to graphs in his '[48]SQL for
+ Smarties' book, but the topic is treated in a quite hasty way, which is
+ surprising given his usual high standards.
+
+ One of the basic rules of a successful representation is to separate
+ the nodes and the edges, to avoid [49]DKNF problems. Thus, we create
+ two tables:
+CREATE TABLE nodes (
+ id INTEGER PRIMARY KEY,
+ name VARCHAR(10) NOT NULL,
+ feat1 CHAR(1), -- e.g., age
+ feat2 CHAR(1) -- e.g., school attended or company
+);
+
+CREATE TABLE edges (
+ a INTEGER NOT NULL REFERENCES nodes(id) ON UPDATE CASCADE ON DELETE CASCADE,
+ b INTEGER NOT NULL REFERENCES nodes(id) ON UPDATE CASCADE ON DELETE CASCADE,
+ PRIMARY KEY (a, b)
+);
+
+CREATE INDEX a_idx ON edges (a);
+CREATE INDEX b_idx ON edges (b);
+
+ The first table (nodes) contains the actual node payload, with all the
+ interesting information we need to store about a node (in the
+ example, feat1 and feat2 represent two node features, like the age of
+ the person, or the location, etc.).
+
+ If we want to represent an undirected graph, we need to add a CHECK
+ constraint on the uniqueness of the pair.
+
+ Since the SQL standard does not allow a subquery in the CHECK
+ constraint, we first create a function and then we use it in the CHECK
+ constraint (this example is for PostgreSQL, but can be easily ported to
+ other DBMS):
+CREATE FUNCTION check_unique_pair(IN id1 INTEGER, IN id2 INTEGER) RETURNS INTEGE
+R AS $body$
+DECLARE retval INTEGER DEFAULT 0;
+BEGIN
+SELECT COUNT(*) INTO retval FROM (
+ SELECT * FROM edges WHERE a = id1 AND b = id2
+ UNION ALL
+ SELECT * FROM edges WHERE a = id2 AND b = id1
+) AS pairs;
+RETURN retval;
+END
+$body$
+LANGUAGE 'plpgsql';
+
+ALTER TABLE edges ADD CONSTRAINT unique_pair CHECK (check_unique_pair(a, b) < 1)
+;
+
+ NB: a UDF in a CHECK constraint might be a bit slow [4]. An alternative
+ is to have a materialized view [5] or force an order in the node pair
+ (i.e. "CHECK (a < b)", and then using a stored procedure to insert the
+ nodes in the correct order).
+
+ If we also want to prevent self-loops (i.e. a node linking to itself),
+ we can add another CHECK constraint:
+ALTER TABLE edges ADD CONSTRAINT no_self_loop CHECK (a <> b)
+
+ " " " "
+
+Traversing the graph
+
+ Now that we know how to store the graph, we might want to know which
+ nodes are connected. Listing the directly connected nodes is very
+ simple:
+SELECT *
+ FROM nodes n
+ LEFT JOIN edges e ON n.id = e.b
+ WHERE e.a = 1; -- retrieve nodes connected to node 1
+
+ or, in the case of undirected edges:
+SELECT * FROM nodes WHERE id IN (
+ SELECT a FROM edges WHERE b = 1
+ UNION
+ SELECT b FROM edges WHERE a = 1
+);
+
+-- or alternatively:
+
+SELECT * FROM nodes where id IN (
+ SELECT CASE WHEN a = 1 THEN b ELSE a END
+ FROM edges
+ WHERE 1 IN (a, b)
+);
+
+ Traversing the full graph usually requires more than a query: we can
+ either loop through the connected nodes, one level a time, or we can
+ create a temporary table holding all the possible paths between two
+ nodes.
+
+ We could use Oracle’s CONNECT BY syntax or SQL standard’s Common Table
+ Expressions (CTEs) to recurse through the nodes, but since the graph
+ can contain loops, we’d get errors (unless we’re very careful, as we’ll
+ see in a moment).
+
+ Kendall Willets [2] proposes a way of traversing (BFS) the graph using
+ a temporary table. It is quite robust, since it doesn’t fail on graphs
+ with cycles (and when dealing with trees, he shows there are better
+ algorithms available). His solution is just one of the many available,
+ but quite good.
+
+ The problem with temporary tables holding all the possible paths is it
+ has to be maintained. Depending on how frequently the data is accessed
+ and updated it might still be worth it, but it’s quite expensive. If
+ you do resort to such a solution, these references may be of use [13]
+ [14].
+
+ Before going further in our analysis, we need to introduce a new
+ concept: the transitive closure of a graph.
+
+Transitive closure
+
+ The transitive closure of a graph G = (V,E) is a graph G* = (V,E*) such
+ that E* contains an edge (u,v) if and only if G contains a path from u
+ to v.
+
+ In other words, the transitive closure of a graph is a graph which
+ contains an edge (u,v) whenever there is a directed path from u to v.
+ " "
+
+ Graph: transitive closure
+
+ As already mentioned, SQL has historically been unable [3] to express
+ recursive functions needed to maintain the transitive closure of a
+ graph without an auxiliary table. There are many solutions to solve
+ this problem with a temporary table (some even elegant [2]), but I
+ still haven't found one to do it dynamically.
+
+ Here's my clumsy attempt at a possible solution using CTEs
+
+ First, this is how we can write the WITH RECURSIVE statement for a
+ Directed (Cyclic) Graph:
+WITH RECURSIVE transitive_closure(a, b, distance, path_string) AS
+( SELECT a, b, 1 AS distance,
+ a || '.' || b || '.' AS path_string
+ FROM edges
+
+ UNION ALL
+
+ SELECT tc.a, e.b, tc.distance + 1,
+ tc.path_string || e.b || '.' AS path_string
+ FROM edges AS e
+ JOIN transitive_closure AS tc
+ ON e.a = tc.b
+ WHERE tc.path_string NOT LIKE '%' || e.b || '.%'
+)
+SELECT * FROM transitive_closure
+ORDER BY a, b, distance;
+
+ Notice the WHERE condition, which stops the recursion in the presence
+ of loops. This is very important to avoid errors.
+
+ Sample output:
+ " "
+
+ This is a slightly modified version of the same query to deal with
+ Undirected graphs (NB: this is probably going to be rather slow if done
+ in real time):
+-- DROP VIEW edges2;
+CREATE VIEW edges2 (a, b) AS (
+ SELECT a, b FROM edges
+ UNION ALL
+ SELECT b, a FROM edges
+);
+
+WITH RECURSIVE transitive_closure(a, b, distance, path_string) AS
+( SELECT a, b, 1 AS distance,
+ a || '.' || b || '.' AS path_string
+ FROM edges2
+
+ UNION ALL
+
+ SELECT tc.a, e.b, tc.distance + 1,
+ tc.path_string || e.b || '.' AS path_string
+ FROM edges2 AS e
+ JOIN transitive_closure AS tc ON e.a = tc.b
+ WHERE tc.path_string NOT LIKE '%' || e.b || '.%'
+)
+SELECT * FROM transitive_closure
+ORDER BY a, b, distance;
+
+Linkedin: Degrees of separation
+
+ One of the fundamental characteristics of networks (or graphs in
+ general) is connectivity. We might want to know how to go from A to B,
+ or how two people are connected, and we also want to know how many
+ "hops" separate two nodes, to have an idea about the distance.
+
+ For instance, social networks like LinkedIN show our connections or
+ search results sorted by degree of separation, and trip planning sites
+ show how many flights you have to take to reach your destination,
+ usually listing direct connections first.
+
+ There are some database extensions or hybrid solutions like SPARQL on
+ Virtuoso [11] that add a TRANSITIVE clause [12] to make this kind of
+ queries both easy and efficient, but we want to see how to reach the
+ same goal with standard SQL.
+
+ As you might guess, this becomes really easy once you have the
+ transitive closure of the graph, we only have to add a WHERE clause
+ specifying what our source and destination nodes are:
+WITH RECURSIVE transitive_closure(a, b, distance, path_string) AS
+( SELECT a, b, 1 AS distance,
+ a || '.' || b || '.' AS path_string
+ FROM edges
+ WHERE a = 1 -- source
+
+ UNION ALL
+
+ SELECT tc.a, e.b, tc.distance + 1,
+ tc.path_string || e.b || '.' AS path_string
+ FROM edges AS e
+ JOIN transitive_closure AS tc ON e.a = tc.b
+ WHERE tc.path_string NOT LIKE '%' || e.b || '.%'
+)
+ SELECT * FROM transitive_closure
+ WHERE b=6 -- destination
+ORDER BY a, b, distance;
+
+ " "
+
+ If we're showing the trip planning results, then we have a list of all
+ possible travel solutions; instead of sorting by distance, we might
+ sort by price or other parameters with little changes.
+
+ If we're showing how two people are connected (LinkedIN), then we can
+ limit the result set to the first row, since we're probably interested
+ in showing the shortest distance only and not all the other
+ alternatives.
+
+ Instead of adding a LIMIT clause, it's probably more efficient to add
+ "AND tc.distance = 0" to the WHERE clause of the recursive part of the
+ CTE, or a GROUP BY clause as follows:
+WITH RECURSIVE transitive_closure(a, b, distance, path_string)
+AS
+( SELECT a, b, 1 AS distance,
+ a || '.' || b || '.' AS path_string
+ FROM edges2
+
+ UNION ALL
+
+ SELECT tc.a, e.b, tc.distance + 1,
+ tc.path_string || e.b || '.' AS path_string
+ FROM edges2 AS e
+ JOIN transitive_closure AS tc ON e.a = tc.b
+ WHERE tc.path_string NOT LIKE '%' || e.b || '.%'
+)
+SELECT a, b, min(distance) AS dist FROM transitive_closure
+--WHERE a = 1 AND b=6
+GROUP BY a, b
+ORDER BY a, dist, b;
+
+ " "
+
+ If you are interested in the immediate connections of a certain node,
+ then specify the starting node and a distance equals to one (by
+ limiting the recursion at the first level)
+WITH RECURSIVE transitive_closure(a, b, distance, path_string) AS
+( SELECT a, b, 1 AS distance, a || '.' || b || '.' AS path_string
+ FROM edges2
+ WHERE a = 1 -- set the starting node
+
+ UNION ALL
+
+ SELECT tc.a, e.b, tc.distance + 1,
+ tc.path_string || e.b || '.' AS path_string
+ FROM edges2 AS e
+ JOIN transitive_closure AS tc ON e.a = tc.b
+ WHERE tc.path_string NOT LIKE '%' || e.b || '.%'
+ AND tc.distance = 0 -- limit recursion at the first level
+)
+SELECT b FROM transitive_closure;
+
+ Of course to get the immediate connections there's no need for a
+ recursive query (just use the one presented at the previous paragraph),
+ but I thought I'd show it anyway as a first step towards more complex
+ queries.
+
+ LinkedIN has a nice feature to show "How this user is connected to you"
+ for non directly connected nodes.
+
+ If the distance between the two nodes is equal to 2, you can show the
+ shared connections:
+SELECT b FROM (
+
+WITH RECURSIVE transitive_closure(a, b, distance, path_string) AS
+( SELECT a, b, 1 AS distance, a || '.' || b || '.' AS path_string
+ FROM edges2
+ WHERE a = 1 -- set the starting node
+
+ UNION ALL
+
+ SELECT tc.a, e.b, tc.distance + 1,
+ tc.path_string || e.b || '.' AS path_string
+ FROM edges2 AS e
+ JOIN transitive_closure AS tc ON e.a = tc.b
+ WHERE tc.path_string NOT LIKE '%' || e.b || '.%'
+ AND tc.distance = 0
+)
+SELECT b FROM transitive_closure
+
+UNION ALL
+
+(WITH RECURSIVE transitive_closure(a, b, distance, path_string) AS
+( SELECT a, b, 1 AS distance, a || '.' || b || '.' AS path_string
+ FROM edges2
+ WHERE a = 4 -- set the target node
+
+ UNION ALL
+
+ SELECT tc.a, e.b, tc.distance + 1,
+ tc.path_string || e.b || '.' AS path_string
+ FROM edges2 AS e
+ JOIN transitive_closure AS tc ON e.a = tc.b
+ WHERE tc.path_string NOT LIKE '%' || e.b || '.%'
+ AND tc.distance = 0
+)
+SELECT b FROM transitive_closure
+)) AS immediate_connections
+GROUP BY b
+HAVING COUNT(b) > 1;
+
+ In the above query, we select the immediate connections of the two
+ nodes separately, and then select the shared ones.
+
+ For nodes having a distance equals to 3, the approach is slightly
+ different.
+
+ First, you check that the two nodes are indeed at a minimum distance of
+ 3 nodes (you're probably not interested in showing the relationship
+ between two nodes when the distance is bigger):
+WITH RECURSIVE transitive_closure(a, b, distance, path_string) AS
+( SELECT a, b, 1 AS distance,
+ a || '.' || b || '.' AS path_string
+ FROM edges2
+ WHERE a = 1 -- set the starting node
+
+ UNION ALL
+
+ SELECT tc.a, e.b, tc.distance + 1,
+ tc.path_string || e.b || '.' AS path_string
+ FROM edges2 AS e
+ JOIN transitive_closure AS tc ON e.a = tc.b
+ WHERE tc.path_string NOT LIKE '%' || e.b || '.%'
+ AND tc.distance < 3 -- stop the recursion after 3 levels
+)
+SELECT a, b, min(distance) FROM transitive_closure
+WHERE b=4 -- set the target node
+GROUP BY a, b
+HAVING min(distance) = 3; --set the minimum distance
+
+ Then you select the paths between those nodes.
+
+ But there's a different approach which is more generic and efficient,
+ and can be used for all the nodes whose distance is bigger than 2.
+
+ The idea is to select the immediate neighbours of the starting node
+ that are also in the path to the other node.
+
+ Depending on the distance, you can have either the shared nodes
+ (distance = 2), or the connections that could lead to the other node
+ (distance > 2). In the latter case, you could for instance show how A
+ is connected to B:
+ " "
+
+ Linkedin
+WITH RECURSIVE transitive_closure(a, b, distance, path_string) AS
+( SELECT a, b, 1 AS distance,
+ a || '.' || b || '.' AS path_string,
+ b AS direct_connection
+ FROM edges2
+ WHERE a = 1 -- set the starting node
+
+ UNION ALL
+
+ SELECT tc.a, e.b, tc.distance + 1,
+ tc.path_string || e.b || '.' AS path_string,
+ tc.direct_connection
+ FROM edges2 AS e
+ JOIN transitive_closure AS tc ON e.a = tc.b
+ WHERE tc.path_string NOT LIKE '%' || e.b || '.%'
+ AND tc.distance < 3
+)
+SELECT * FROM transitive_closure
+--WHERE b=3 -- set the target node
+ORDER BY a,b,distance
+
+ " "
+
+Facebook: You might also know
+
+ A similar but slightly different requirement is to find those nodes
+ that are most strongly related, but not directly connected yet. In
+ other words, it's interesting to find out which and how many connected
+ nodes are shared between any two nodes, i.e. how many 'friends' are
+ shared between two individuals. Or better yet, to find those nodes
+ sharing a certain (minimum) number of nodes with the current one.
+
+ This could be useful to suggest a new possible friend, or in the case
+ of recommendation systems, to suggest a new item/genre that matches the
+ user's interests.
+
+ There are many ways of doing this. In theory, this is bordering on the
+ domain of collaborative filtering [6][7][8], so using Pearson's
+ correlation [9] or a similar distance measure with an appropriate
+ algorithm [10] is going to generate the best results. Collaborative
+ filtering is an incredibly interesting topic on its own, but outside
+ the scope of this article.
+
+ A rough and inexpensive alternative is to find the nodes having
+ distance equals to 2, and filter those that either have a common
+ characteristic with the source node (went to the same school / worked
+ at the same company, belong to the same interest group / are items of
+ the same genre) or have several mutual 'friends'.
+ " "
+
+ Facebook
+
+ This, again, is easily done once you have the transitive closure of the
+ graph:
+SELECT a AS you,
+ b AS mightknow,
+ shared_connection,
+ CASE
+ WHEN (n1.feat1 = n2.feat1 AND n1.feat1 = n3.feat1) THEN 'feat1 in commo
+n'
+ WHEN (n1.feat2 = n2.feat2 AND n1.feat2 = n3.feat2) THEN 'feat2 in commo
+n'
+ ELSE 'nothing in common'
+ END AS reason
+ FROM (
+WITH RECURSIVE transitive_closure(a, b, distance, path_string) AS
+( SELECT a, b, 1 AS distance,
+ a || '.' || b || '.' AS path_string,
+ b AS direct_connection
+ FROM edges2
+ WHERE a = 1 -- set the starting node
+
+ UNION ALL
+
+ SELECT tc.a, e.b, tc.distance + 1,
+ tc.path_string || e.b || '.' AS path_string,
+ tc.direct_connection
+ FROM edges2 AS e
+ JOIN transitive_closure AS tc ON e.a = tc.b
+ WHERE tc.path_string NOT LIKE '%' || e.b || '.%'
+ AND tc.distance < 2
+)
+SELECT a,
+ b,
+ direct_connection AS shared_connection
+ FROM transitive_closure
+ WHERE distance = 2
+) AS youmightknow
+LEFT JOIN nodes AS n1 ON youmightknow.a = n1.id
+LEFT JOIN nodes AS n2 ON youmightknow.b = n2.id
+LEFT JOIN nodes AS n3 ON youmightknow.shared_connection = n3.id
+WHERE (n1.feat1 = n2.feat1 AND n1.feat1 = n3.feat1)
+ OR (n1.feat2 = n2.feat2 AND n1.feat2 = n3.feat2);
+
+ " "
+
+ Once you have selected these nodes, you can filter those recurring more
+ often, or give more importance to those having a certain feature in
+ common, or pick one randomly (so you don't end up suggesting the same
+ node over and over).
+
+Conclusion
+
+ In this article I had some fun with the new and powerful CTEs, and
+ showed some practical examples where they can be useful. I also showed
+ some approaches at solving the challenges faced by any social network
+ or recommendation system.
+
+ You are advised that depending on the size of the graph and the
+ performance requirements of your application, the above queries might
+ be too slow to run in realtime. Caching is your friend.
+
+ Update: Many of the queries in this article have been revised, so
+ please refer
+ to [50]http://www.slideshare.net/quipo/rdbms-in-the-social-networks-age
+ for changes.
+
+References
+
+ [1] [51]http://willets.org/sqlgraphs.html
+
+ [2] [52]http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.48.53
+
+ [3] [53]http://sqlblog.com/blogs/alexander_kuznetsov/archive/2009/06/25
+ /scalar-udfs-wrapped-in-check-constraints-are-very-slow-and-may-fail-fo
+ r-multirow-updates.aspx
+
+ [4] [54]http://www.dbazine.com/oracle/or-articles/tropashko8
+
+ [5] [55]http://en.wikipedia.org/wiki/Collaborative_filtering
+
+ [6] [56]http://en.wikipedia.org/wiki/Slope_One
+
+ [7] blog.charliezhu.com/2008/07/21/implementing-slope-one-in-t-sql/
+
+ [8] bakara.eng.tau.ac.il/~semcomm/slides7/grouplensAlgs-Kahn.pps
+
+ [9] [57]http://www.slideshare.net/denisparra/evaluation-of-collaborativ
+ e-filtering-algorithms-for-recommending-articles-on-citeulike
+
+ [10] [58]http://virtuoso.openlinksw.com/
+
+ [11] [59]http://www.openlinksw.com/weblog/oerling/?id=1433
+
+ [12] [60]http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.48.53
+
+ [13] [61]http://en.wikipedia.org/wiki/Transitive_reduction
+
+You might also like...
+
+ A woman reviews code on her laptop
+
+Headless commerce: everything you need to know
+
+ What the heck is headless? Discover the what, why, and when of headless
+ architectures with our guide to headless commerce.
+
+ Drupal consulting and web development at Inviqa
+
+The Drupal 9 upgrade Config Split issue and how to fix it
+
+ In this article we look back at an issue we’ve encountered with Drupal
+ Config Split when upgrading Drupal 8 to 9 – and we share how to fix it,
+ so you don’t have to run into the same issue when upgrading to Drupal
+ 9.
+
+ Inviqa, winner of Webby Awards The Webby Awards winner
+ Inviqa named one of Top 100 Agencies in Econsultancy Top 100 Digital
+ Agencies
+ Inviqa UXUK Awards winner UXUK Awards winner
+ DADI Award winner of 'Best UX / Usability category' DADI Award winner
+
+ Footer Main Navigation
+ * [62]Home
+ * [63]Who we are
+ * [64]What we do
+ * [65]Case studies
+ * [66]Careers
+ * [67]Insights
+ * [68]Contact
+ * [69]Accessibility statement
+
+About us
+
+ Together with your teams, we shape the digital products, teams,
+ processes, and software systems you need to meet diverse customer needs
+ and accelerate your business growth.
+
+ © 2007-2024, Inviqa UK Ltd. Registered No. 06278367. Registered Office:
+ Havas House, Hermitage Court, Hermitage Lane, Maidstone, ME16 9NT, UK.
+
+ Footer Legal Links
+ * [70]Covid-19
+ * [71]Privacy policy
+ * [72]Sitemap
+
+References
+
+ Visible links:
+ 1. https://inviqa.com/blog/storing-graphs-database-sql-meets-social-network
+ 2. https://www.googletagmanager.com/ns.html?id=GTM-NBN52P
+ 3. file:///var/folders/q9/qlz2w5251kzdfgn0np7z2s4c0000gn/T/L84755-9551TMP.html#main-content
+ 4. https://inviqa.com/
+ 5. https://inviqa.com/who-we-are
+ 6. https://inviqa.com/who-we-are
+ 7. https://www.havas.com/
+ 8. https://inviqa.com/digital-sustainability-journey
+ 9. https://inviqa.com/what-we-do
+ 10. https://inviqa.com/what-we-do/digital-strategy-consulting-and-development
+ 11. https://inviqa.com/what-we-do/digital-roadmap-development
+ 12. https://inviqa.com/what-we-do/digital-product-design
+ 13. https://inviqa.com/what-we-do/user-research
+ 14. https://inviqa.com/what-we-do/usability-testing
+ 15. https://inviqa.com/what-we-do/technical-architecture-consulting-and-development
+ 16. https://inviqa.com/what-we-do/digital-platform-consulting-and-implementation
+ 17. https://inviqa.com/what-we-do/experience-optimisation
+ 18. https://inviqa.com/what-we-do
+ 19. https://inviqa.com/case-studies
+ 20. https://inviqa.com/case-studies?category=b2b
+ 21. https://inviqa.com/case-studies#fashion
+ 22. https://inviqa.com/case-studies#charity
+ 23. https://inviqa.com/case-studies?category=retail
+ 24. https://inviqa.com/case-studies#leisure
+ 25. https://inviqa.com/case-studies?category=travel
+ 26. https://inviqa.com/case-studies
+ 27. https://inviqa.com/partners
+ 28. https://inviqa.com/akeneo-pim-consulting-and-implementation
+ 29. https://inviqa.com/blog/bigcommerce-7-best-sites
+ 30. https://inviqa.com/drupal-consulting-and-web-development
+ 31. https://inviqa.com/magento-consulting-and-web-development
+ 32. https://inviqa.com/blog/spryker-commerce-platform-introduction
+ 33. https://inviqa.com/partners
+ 34. https://careers.inviqa.com/
+ 35. https://careers.inviqa.com/
+ 36. https://careers.inviqa.com/jobs
+ 37. https://inviqa.com/insights
+ 38. https://inviqa.com/insights/dtc-ecommerce-report-2023
+ 39. https://inviqa.com/insights/PIM-readiness-framework
+ 40. https://inviqa.com/insights/retail-optimisation-guide-2023
+ 41. https://inviqa.com/blog
+ 42. https://inviqa.com/insights
+ 43. https://inviqa.com/contact
+ 44. https://inviqa.com/contact
+ 45. https://inviqa.com/
+ 46. https://inviqa.de/
+ 47. https://inviqa.com/blog#Technology engineering
+ 48. https://www.amazon.com/Joe-Celkos-SQL-Smarties-Programming/dp/0123693799/157-5667933-6571053?ie=UTF8&redirect=true&tag=postcarfrommy-20
+ 49. https://en.wikipedia.org/wiki/Domain-key_normal_form
+ 50. http://www.slideshare.net/quipo/rdbms-in-the-social-networks-age
+ 51. http://willets.org/sqlgraphs.html
+ 52. http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.48.53
+ 53. 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
+ 54. http://www.dbazine.com/oracle/or-articles/tropashko8/
+ 55. https://en.wikipedia.org/wiki/Collaborative_filtering
+ 56. https://en.wikipedia.org/wiki/Slope_One
+ 57. http://www.slideshare.net/denisparra/evaluation-of-collaborative-filtering-algorithms-for-recommending-articles-on-citeulike
+ 58. http://virtuoso.openlinksw.com/
+ 59. http://www.openlinksw.com/weblog/oerling/?id=1433
+ 60. http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.48.53
+ 61. https://en.wikipedia.org/wiki/Transitive_reduction
+ 62. https://inviqa.com/we-craft-game-changing-digital-experiences
+ 63. https://inviqa.com/who-we-are
+ 64. https://inviqa.com/what-we-do
+ 65. https://inviqa.com/case-studies
+ 66. https://careers.inviqa.com/
+ 67. https://inviqa.com/insights
+ 68. https://inviqa.com/contact
+ 69. https://inviqa.com/accessibility-statement
+ 70. https://inviqa.com/covid-19-measures
+ 71. https://inviqa.com/privacy-policy-UK
+ 72. https://inviqa.com/sitemap
+
+ Hidden links:
+ 74. https://inviqa.com/blog/headless-commerce-everything-you-need-know
+ 75. https://inviqa.com/blog/drupal-9-upgrade-config-split-issue-and-how-fix-it