-
Notifications
You must be signed in to change notification settings - Fork 849
/
Copy pathmultiset-and-map.sql
223 lines (186 loc) · 5.33 KB
/
multiset-and-map.sql
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
/*
The MULTISET operators are fantastic, declarative, set-oriented functionality
for nested tables. But if you have a nested table of object types, then you will
also need to provide a MAP method for that object type if your MULTISET needs
to do a comparison. Which is true for pretty much every variation on MULTISET
except for MULTISET UNION [ALL].
For full explanation, see my blog post:
https://stevenfeuersteinonplsql.blogspot.com/2018/10/why-wont-multiset-work-for-me.html
*/
CREATE TABLE limbs
(
nm VARCHAR2 (100),
avg_len NUMBER
);
BEGIN
INSERT INTO limbs VALUES ('arm', 1);
INSERT INTO limbs VALUES ('leg', 2);
INSERT INTO limbs VALUES ('tail', 3);
COMMIT;
END;
/
CREATE OR REPLACE TYPE limb_ot AUTHID DEFINER
IS OBJECT
(
nm VARCHAR2 (100),
avg_len NUMBER
)
/
-- Works Just Fine
-- MULTISET UNION = MULTISET UNION ALL, which means no checking for duplicates,
-- which means no need to compare, so no problem!
DECLARE
TYPE limbs_t IS TABLE OF limb_ot;
l_limbs limbs_t;
BEGIN
SELECT limb_ot (l.nm, l.avg_len)
BULK COLLECT INTO l_limbs
FROM limbs l
ORDER BY l.nm;
l_limbs := l_limbs MULTISET UNION l_limbs;
DBMS_OUTPUT.put_line ('Lots of limbs! ' || l_limbs.COUNT);
END;
/
-- Even Works for %ROWTYPE Elements!
-- Again, no need to make a comparison, so we are good.
DECLARE
TYPE limbs_t IS TABLE OF limbs%ROWTYPE;
l_limbs limbs_t;
BEGIN
SELECT l.nm, l.avg_len
BULK COLLECT INTO l_limbs
FROM limbs l
ORDER BY l.nm;
l_limbs := l_limbs MULTISET UNION l_limbs;
DBMS_OUTPUT.put_line ('Lots of limbs! ' || l_limbs.COUNT);
END;
/
-- EXCEPT Requires Comparison
-- Now we get an error. MULTISET EXCEPT can't do its job without comparing
-- contents of the object type. The limb_ot object type has no map method (yet!), so it fails.
DECLARE
TYPE limbs_t IS TABLE OF limb_ot;
l_limbs limbs_t;
BEGIN
SELECT limb_ot (l.nm, l.avg_len)
BULK COLLECT INTO l_limbs
FROM limbs l
ORDER BY l.nm;
l_limbs := l_limbs MULTISET EXCEPT l_limbs;
DBMS_OUTPUT.put_line ('Lots of limbs! ' || l_limbs.COUNT);
END;
/
-- UNION DISTINCT - Compares
-- I add the DISTINCT modifier and now MULTISET UNION doesn't work.
DECLARE
TYPE limbs_t IS TABLE OF limb_ot;
l_limbs limbs_t;
BEGIN
SELECT limb_ot (l.nm, l.avg_len)
BULK COLLECT INTO l_limbs
FROM limbs l
ORDER BY l.nm;
l_limbs := l_limbs MULTISET UNION DISTINCT l_limbs;
DBMS_OUTPUT.put_line ('Lots of limbs! ' || l_limbs.COUNT);
END;
/
-- Add a Map Method!
CREATE OR REPLACE TYPE limb_ot AUTHID DEFINER
IS OBJECT
(
nm VARCHAR2 (100),
avg_len NUMBER,
MAP MEMBER FUNCTION limb_map
RETURN NUMBER
)
/
-- It's a silly mapping algorithm - only looks at the length of the name.
-- When you create your map method, make sure it reflects the logic of your data.
CREATE OR REPLACE TYPE BODY limb_ot
IS
MAP MEMBER FUNCTION limb_map
RETURN NUMBER
IS
BEGIN
RETURN LENGTH (self.nm);
END;
END;
/
-- Now UNION DISTINCT Works
/*
With the map method in place, MULTISET operations that require comparisons
now work. But wait - only 2 elements in the resulting UNION DISTINCT? But all
three rows are distinct - what's going on? Hint: see mapping algorithm.
*/
DECLARE
TYPE limbs_t IS TABLE OF limb_ot;
l_limbs limbs_t;
BEGIN
SELECT limb_ot (l.nm, l.avg_len)
BULK COLLECT INTO l_limbs
FROM limbs l
ORDER BY l.nm;
l_limbs := l_limbs MULTISET UNION DISTINCT l_limbs;
DBMS_OUTPUT.put_line ('Lots of limbs! ' || l_limbs.COUNT);
END;
/
-- Now MULTISET EXCEPT works - and nothing is left when you "minus" something from itself.
DECLARE
TYPE limbs_t IS TABLE OF limb_ot;
l_limbs limbs_t;
BEGIN
SELECT limb_ot (l.nm, l.avg_len)
BULK COLLECT INTO l_limbs
FROM limbs l
ORDER BY l.nm;
l_limbs := l_limbs MULTISET EXCEPT l_limbs;
DBMS_OUTPUT.put_line ('Lots of limbs! ' || l_limbs.COUNT);
END;
/
-- But Still Not with %ROWTYPE
-- Sorry, there is no way currently in PL/SQL to either create an analogue of
-- a mapping method to a record type or compare records generally.
DECLARE
TYPE limbs_t IS TABLE OF limbs%ROWTYPE;
l_limbs limbs_t;
BEGIN
SELECT l.nm, l.avg_len
BULK COLLECT INTO l_limbs
FROM limbs l
ORDER BY l.nm;
l_limbs := l_limbs MULTISET EXCEPT l_limbs;
DBMS_OUTPUT.put_line ('Lots of limbs! ' || l_limbs.COUNT);
END;
/
-- Tweak Mapping Algorithm
/*
Remember that statement that showed only 2 rows were distinct?
That's not right - well, wait, but it was correct, given the simplistic
mapping algorithm. The names of two limbs were the same: 3, so they mapped
to the "same" as for as DISTINCT goes. Let's improve that algorithm!
*/
CREATE OR REPLACE TYPE BODY limb_ot
IS
MAP MEMBER FUNCTION limb_map
RETURN NUMBER
IS
BEGIN
RETURN LENGTH (self.nm) + self.avg_len;
END;
END;
/
-- Mapping Algorithms Matter
-- With the new algorithm, the DISTINCT operation now gets a different value
-- for each row and so 3 elements are assigned to l_limbs.
DECLARE
TYPE limbs_t IS TABLE OF limb_ot;
l_limbs limbs_t;
BEGIN
SELECT limb_ot (l.nm, l.avg_len)
BULK COLLECT INTO l_limbs
FROM limbs l
ORDER BY l.nm;
l_limbs := l_limbs MULTISET UNION DISTINCT l_limbs;
DBMS_OUTPUT.put_line ('Lots of limbs! ' || l_limbs.COUNT);
END;
/